728x90

소스 점검 하다가 아래와 같은 코드를 봤다.

//... 생략

	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 같은 금융사같은 회사들을 대상으로 코드점검을 나오면 과대료 대상이다.

그래서 아래와 같이 2가지 방법을 제시하는 글들을 작성해 보았다.

방법1: 

<where> 태그를 사용한다.

SELECT 컬럼1, 컬럼2
FROM 테이블명
<where>
    <if test="컬럼1 != null">AND 컬럼1 = #{컬럼1} </if>
    <if test="컬럼2 != null">AND 컬럼2 = #{컬럼2} </if>
</where>

 

방법2

<trim> 태그 사용한다.

SELECT 컬럼1, 컬럼2
FROM 테이블
<trim prefix="WHERE" prefixOverrides="AND">
    <if test="컬럼1 != null">
        AND 컬럼1 = #{컬럼1}
    </if>
    <if test="컬럼2 != null">
        AND 컬럼2 = #{컬럼2}
    </if>
</trim>

여기서 약간의 문제점이 있어 보인다. 컬럼1,컬럼2 가 null 일시 테이블에 대한 전체 데이터를 조회하게 될거다.

그래서 <trim> 태그 사용 방식을 아래와 같이 조금 더 개선해 본다. 바로 Exception 을 유도 하는 것!

SELECT 컬럼1, 컬럼2
FROM member_test
WHERE
<trim prefixOverrides="AND">
    <if test="컬럼1 != null">
        AND 컬럼1 = #{컬럼1}
    </if>
    <if test="컬럼2 != null">
        AND 컬럼2 = #{컬럼2}
    </if>
</trim>

뭐 Exception 을 일부러 유도하는것 자체가 찝찝? 할수 있지만 우리는 table의 전체 조회를 막기위해서는 괜찮은 방법일듯 하다.

 

끝!

728x90

총3가지 방법 요약

CorsFilter 설정

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();
    }

끝!

728x90

들어가며...

실제 프로젝트 개발 과정에서 DTO 데이터 전송 객체 및 데이터 객체 DO와 같은 소스 객체의 속성 정보를 변경하지 않고 소스 객체의 속성 정보를 기반으로 후속 작업을 수행하기 위해 서로 다른 두 객체 인스턴스를 속성으로 복사해야 하는 경우가 많으며, DO 객체의 속성을 DTO로 복사해야 하지만 객체 형식이 다르기 때문에 객체의 속성 값을 한 유형에서 다른 유형으로 변환하기 위해 매핑 코드를 작성해야 했었다.

Object 의 copy

두 Bean Utils를 구체적으로 소개하기 전에 몇 가지 기본적인 부분을 짚어넘어가 보자.두 가지 도구는 본질적으로 객체 복사 도구이고 deep coyp & shallow copy로 나뉜다.

  • shallow copy : 기본 데이터 유형에 대한 값 전달, 인용 데이터 유형에 대한 인용 전달과 같은 카피
  • deep coyp : 기본 데이터 유형에 대한 값 전달, 참조 데이터 유형에 대한 새로운 개체 생성 및 내용 복사

Apache BeanUtils 예제

publicclass PersonSource {
    private Integer id;
    private String username;
    private String password;
    private Integer age;
    // getters/setters omiited
}
publicclass PersonDest {
    private Integer id;
    private String username;
    private Integer age;
    // getters/setters omiited
}
publicclass TestApacheBeanUtils {
    public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {
       
        PersonSource personSource = new PersonSource(1, "pjmike", "12345", 21);
        PersonDest personDest = new PersonDest();
        BeanUtils.copyProperties(personDest,personSource);
        System.out.println("persondest: "+personDest);
    }
}
persondest: PersonDest{id=1, username='pjmike', age=21}

위 예제처럼 사용방법은 매우 간단하다. 일반적인 BeanUtils 흔히 쓰는 방법은 아래와 같다.

public static void copyProperties(Object dest, Object orig) throws IllegalAccessException, InvocationTargetException {
    BeanUtilsBean.getInstance().copyProperties(dest, orig);
}

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를 고민해볼수도 있겠다.

 

끝!

728x90

java.util.stream 이 성능, 가독성 , 그리고 쓰기도 편하지만 많은 사람들이 실제 현업에서 쓰기를 꺼려한다. 왜냐면 디버깅이 쉽지 않기때문에...

 

하지만 그것은 옛날 얘기고 , 그리고 요즘 나오는 IDE 자체 plugin 들이 다 잘 되어있어서 이런 불편들은 해결가능하다.

아래 코드가 있다.

public class StreamTest {

    @Test
    void test() {
        List<String> list = List.of("blog.blakespace.com", "strea-demo.com", "stream-api.cn",
            "rainsister.tistory.com");

        List<String> result = list.stream()
            .filter(e -> e.contains("blakespace.com"))
            .filter(e -> e.length() > 15 )
            .toList();

        System.out.println("result"+result);
    }

}

끝!

+ Recent posts