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

 

끝!

+ Recent posts