실제 프로젝트 개발 과정에서 DTO 데이터 전송 객체 및 데이터 객체 DO와 같은 소스 객체의 속성 정보를 변경하지 않고 소스 객체의 속성 정보를 기반으로 후속 작업을 수행하기 위해 서로 다른 두 객체 인스턴스를 속성으로 복사해야 하는 경우가 많으며, DO 객체의 속성을 DTO로 복사해야 하지만 객체 형식이 다르기 때문에 객체의 속성 값을 한 유형에서 다른 유형으로 변환하기 위해 매핑 코드를 작성해야 했었다.
Object 의 copy
두 Bean Utils를 구체적으로 소개하기 전에 몇 가지 기본적인 부분을 짚어넘어가 보자.두 가지 도구는 본질적으로 객체 복사 도구이고 deep coyp & shallow copy로 나뉜다.
shallow copy : 기본 데이터 유형에 대한 값 전달, 인용 데이터 유형에 대한 인용 전달과 같은 카피
deep coyp : 기본 데이터 유형에 대한 값 전달, 참조 데이터 유형에 대한 새로운 개체 생성 및 내용 복사
Spring에 내장된 BeanUtils도 copyProperties 방법을 사용하여 복사하지만 구현 방식은 매우 간단하며 두 객체의 동일한 이름의 속성에 대한 간단한 get/set을 수행하여 속성의 접근성만 확인합니다.구체적으로는 다음과 같다.
privatestaticvoidcopyProperties(Object source,Object target, @NullableClass<?> editable, @NullableString...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)) {thrownewIllegalArgumentException("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) {thrownewFatalBeanException("Could not copy property '"+targetPd.getName() +"' from source to target", ex); } } } } } }
코드에서 알수 있듯이 멤버 변수 할당은 대상 객체의 멤버 목록을 기반으로 하며, ignore를 건너뛰고 원본 객체에 존재하지 않기 때문에 이 방법은 안전하다고 볼수 있다. 두 객체 간의 구조적 차이로 인한 오류는 발생하지 않지만, 같은 이름의 두 멤버 변수 유형이 동일해야 한다.
맺으면서...
오늘 위의 두 가지 BeanUtils를 간략히 분석하였는데 Apache에 있는 BeanUtils는 성능이 좋지 않기 때문에 권장하지 않으며, Spring의 BeanUtils를 사용하거나, Dozer, ModelMapper 등과 같은 다른 copy library를 고민해볼수도 있겠다.