//... 생략
FROM TOMS_CO_M CM
) T1
WHERE 1 = 1
<if test='param.srchCorpCd != null and param.srchCorpCd != ""'>
AND CORP_CD = #{param.srchCorpCd}
</if>
<if test='param.srchCoCrtDtmFr != null and param.srchCoCrtDtmFr != ""'>
AND CO_CRT_DTM BETWEEN TO_DATE(#{param.srchCoCrtDtmFr}, 'yyyyMMdd') AND TO_DATE('99991231', 'yyyyMMdd')
</if>
참 성의 없는 코드다. 왜냐 ? 제목에서 처럼 1=1 를 써놨기때문이다. 물론 우리가 급식시절에 대충 게시판 하나 만들면서 CRUD 구현에 목숨걸고 있을시절에 자주 쓰고했다. 어? 그래 맞다 현재 SI 개발자들도 아니 그 흔한 고수급 스타트업 개발자들도 이런 코드를 자주 쓰더라.
그럼 오늘 이 where 1=1를 제거 해보자 케이스는 2가지가 있다.(내가 아니는 한도내에서, 또 다른 방법들이 있겠지만...)
이 쿼리의 문제 점을 생각해봐라 : 만일 corp_cd , co_crt_dtm 이런 값들이 모두 null 로 들어온다고하면? 그리고 KISA 같은 금융사같은 회사들을 대상으로 코드점검을 나오면 과대료 대상이다.
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@SpringBootConfiguration
public class WebGlobalConfig {
@Bean
public CorsFilter corsFilter() {
//CorsConfiguration 개체 생성 후 설정 추가
CorsConfiguration config = new CorsConfiguration();
//내보낼 원본 도메인 설정
config.addAllowedOrigin("*");
//원본 요청 헤더 정보 내보내기
config.addAllowedHeader("*");
//header 의 노출 정보
config.addExposedHeader("*");
//허용할 요청 항목들
config.addAllowedMethod("GET"); //get
config.addAllowedMethod("PUT"); //put
config.addAllowedMethod("POST"); //post
config.addAllowedMethod("DELETE"); //delete
//corsConfig.addAllowedMethod("*"); //모두허용
// Cookie 전송여부
config.setAllowCredentials(true);
//2. 매핑 경로 추가
UrlBasedCorsConfigurationSource corsConfigurationSource =
new UrlBasedCorsConfigurationSource();
corsConfigurationSource.registerCorsConfiguration("/**", config);
return new CorsFilter(corsConfigurationSource);
}
}
SpringBoot2.4.4 이후 버전사용시 아래 에러 나올수 있음.
java.lang.IllegalArgumentException: When allowCredentials is true, allowedOrigins cannot contain the special value "*" since that cannot be set on the "Access-Control-Allow-Origin" response header. To allow credentials to a set of origins, list them explicitly or consider using "allowedOriginPatterns" instead. at org.springframework.web.cors.CorsConfiguration.validateAllowCredentials(CorsConfiguration.java:453) ~[spring-web-5.3.6.jar:5.3.6]
allow Credentials가 true일 때 alloed Origins는 이 값을 'Access-Control-Allow-Origin' 응답 헤드에서 설정할 수 없기 때문에 특수 값 '*'를 포함할 수 없음. 인증 정보 액세스를 허용하려면 소스를 명시적으로 나열하거나 'Allowed Origin Patterns'로 변경.
해결방법: config.addAllowedOrigin("*"); --> config.addAllowedOriginPattern("*"); 로 변경.
WebMvcConfigurer 재정의하는 방법
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@SpringBootConfiguration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
//매핑경로 추가
registry.addMapping("/**")
//Cookie 전송여부
.allowCredentials(true)
//내보낼 원본 도메인 설정 SpringBoot2.4.4 이하 버전은 .allowedOrigins("*") 사용.
.allowedOriginPatterns("*")
//요청 방식을 허용
.allowedMethods(new String[]{"GET", "POST", "PUT", "DELETE"})
//.allowedMethods("*") //요청 방식을 전부 허용
//원본 요청 헤더 정보 내보내기
.allowedHeaders("*")
//원본 요청 헤더 정보 노출
.exposedHeaders("*");
}
}
@CrossOrigin 어노테이션으로 부분적용
@Controller
@RequestMapping("/crostest")
@CrossOrigin(originPatterns = "*", methods = {RequestMethod.GET, RequestMethod.POST})
public class ShopController {
@GetMapping("/")
@ResponseBody
public Map<String, Object> findAll() {
return DataSchool.getStudents();
}
}
혹은 사용되는 각각의 메소드에 붙여줘도 된다.
@Controller
@RequestMapping("/crostest")
public class ShopController {
@GetMapping("/")
@ResponseBody
@CrossOrigin(originPatterns = "http://localhost:8080")
public Map<String, Object> findAll() {
return DataSchool.getStudents();
}
실제 프로젝트 개발 과정에서 DTO 데이터 전송 객체 및 데이터 객체 DO와 같은 소스 객체의 속성 정보를 변경하지 않고 소스 객체의 속성 정보를 기반으로 후속 작업을 수행하기 위해 서로 다른 두 객체 인스턴스를 속성으로 복사해야 하는 경우가 많으며, DO 객체의 속성을 DTO로 복사해야 하지만 객체 형식이 다르기 때문에 객체의 속성 값을 한 유형에서 다른 유형으로 변환하기 위해 매핑 코드를 작성해야 했었다.
Object 의 copy
두 Bean Utils를 구체적으로 소개하기 전에 몇 가지 기본적인 부분을 짚어넘어가 보자.두 가지 도구는 본질적으로 객체 복사 도구이고 deep coyp & shallow copy로 나뉜다.
shallow copy : 기본 데이터 유형에 대한 값 전달, 인용 데이터 유형에 대한 인용 전달과 같은 카피
deep coyp : 기본 데이터 유형에 대한 값 전달, 참조 데이터 유형에 대한 새로운 개체 생성 및 내용 복사
org.apache.commons.beanutils.BeanUtils 은 얕은 복사(shallow copy) 다. 때문에 많은 코딩규칙 Tool 혹은 Plugin 에서는 해당 클래스 사용을 권장하지 않고 있다.
commons-beantutils는 객체를 copy시 validation 과정을 거치는 데, 타입전환 을 비롯하여 객체가 속한 클래스의 접근성까지 체크하기 때문에, 상당히 복잡하다. 바로 이런 이유때문에 형편없는 성능을 보여준다.
public void copyProperties(final Object dest, final Object orig)
throws IllegalAccessException, InvocationTargetException {
// Validate existence of the specified beans
if (dest == null) {
thrownew IllegalArgumentException
("No destination bean specified");
}
if (orig == null) {
thrownew IllegalArgumentException("No origin bean specified");
}
if (log.isDebugEnabled()) {
log.debug("BeanUtils.copyProperties(" + dest + ", " +
orig + ")");
}
// Copy the properties, converting as necessary
if (orig instanceof DynaBean) {
final DynaProperty[] origDescriptors =
((DynaBean) orig).getDynaClass().getDynaProperties();
for (DynaProperty origDescriptor : origDescriptors) {
final String name = origDescriptor.getName();
// Need to check isReadable() for WrapDynaBean
// (see Jira issue# BEANUTILS-61)
if (getPropertyUtils().isReadable(orig, name) &&
getPropertyUtils().isWriteable(dest, name)) {
final Object value = ((DynaBean) orig).get(name);
copyProperty(dest, name, value);
}
}
} elseif (orig instanceof Map) {
@SuppressWarnings("unchecked")
final
// Map properties are always of type <String, Object>
Map<String, Object> propMap = (Map<String, Object>) orig;
for (final Map.Entry<String, Object> entry : propMap.entrySet()) {
final String name = entry.getKey();
if (getPropertyUtils().isWriteable(dest, name)) {
copyProperty(dest, name, entry.getValue());
}
}
} else/* if (orig is a standard JavaBean) */ {
final PropertyDescriptor[] origDescriptors =
getPropertyUtils().getPropertyDescriptors(orig);
for (PropertyDescriptor origDescriptor : origDescriptors) {
final String name = origDescriptor.getName();
if ("class".equals(name)) {
continue; // No point in trying to set an object's class
}
if (getPropertyUtils().isReadable(orig, name) &&
getPropertyUtils().isWriteable(dest, name)) {
try {
final Object value =
getPropertyUtils().getSimpleProperty(orig, name);
copyProperty(dest, name, value);
} catch (final NoSuchMethodException e) {
// Should not happen
}
}
}
}
}
대충 봐도 복잡하고 어렵다.
Spring 의 BeanUtils
아래와 같은 사용 예제코드를 보자
publicclass TestSpringBeanUtils {
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {
PersonSource personSource = new PersonSource(1, "pjmike", "12345", 21);
PersonDest personDest = new PersonDest();
BeanUtils.copyProperties(personSource,personDest);
System.out.println("persondest: "+personDest);
}
}
Spring에 내장된 BeanUtils도 copyProperties 방법을 사용하여 복사하지만 구현 방식은 매우 간단하며 두 객체의 동일한 이름의 속성에 대한 간단한 get/set을 수행하여 속성의 접근성만 확인한다. 구체적 코드를 살펴보면 다음과 같다.
private static void copyProperties(Object source, Object target, @Nullable Class<?> editable,
@Nullable String... ignoreProperties) throws BeansException {
Assert.notNull(source, "Source must not be null");
Assert.notNull(target, "Target must not be null");
Class<?> actualEditable = target.getClass();
if (editable != null) {
if (!editable.isInstance(target)) {
throw new IllegalArgumentException("Target class [" + target.getClass().getName() +
"] not assignable to Editable class [" + editable.getName() + "]");
}
actualEditable = editable;
}
PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
List<String> ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null);
for (PropertyDescriptor targetPd : targetPds) {
Method writeMethod = targetPd.getWriteMethod();
if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
if (sourcePd != null) {
Method readMethod = sourcePd.getReadMethod();
if (readMethod != null &&
ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
try {
if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
readMethod.setAccessible(true);
}
Object value = readMethod.invoke(source);
if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
writeMethod.setAccessible(true);
}
writeMethod.invoke(target, value);
}
catch (Throwable ex) {
throw new FatalBeanException(
"Could not copy property '" + targetPd.getName() + "' from source to target", ex);
}
}
}
}
}
}
코드에서 알수 있듯이 멤버 변수 할당은 대상 객체의 멤버 목록을 기반으로 하며, ignore를 건너뛰고 원본 객체에 존재하지 않기 때문에 이 방법은 안전하다고 볼수 있다. 두 객체 간의 구조적 차이로 인한 오류는 발생하지 않지만, 같은 이름의 두 멤버 변수 유형이 동일해야 한다.
마치면서
오늘 위의 두 가지 BeanUtils를 간략히 분석하였는데 Apache에 있는 BeanUtils는 성능이 좋지 않기 때문에 권장하지 않으며, Spring의 BeanUtils를 사용하거나, Dozer, ModelMapper 등과 같은 다른 copy library를 고민해볼수도 있겠다.