실제 프로젝트 개발 과정에서 DTO 데이터 전송 객체 및 데이터 객체 DO와 같은 소스 객체의 속성 정보를 변경하지 않고 소스 객체의 속성 정보를 기반으로 후속 작업을 수행하기 위해 서로 다른 두 객체 인스턴스를 속성으로 복사해야 하는 경우가 많으며, DO 객체의 속성을 DTO로 복사해야 하지만 객체 형식이 다르기 때문에 객체의 속성 값을 한 유형에서 다른 유형으로 변환하기 위해 매핑 코드를 작성해야 했었다.
Object 의 copy
두 Bean Utils를 구체적으로 소개하기 전에 몇 가지 기본적인 부분을 짚어넘어가 보자.두 가지 도구는 본질적으로 객체 복사 도구이고 deep coyp & shallow copy로 나뉜다.
shallow copy : 기본 데이터 유형에 대한 값 전달, 인용 데이터 유형에 대한 인용 전달과 같은 카피
deep coyp : 기본 데이터 유형에 대한 값 전달, 참조 데이터 유형에 대한 새로운 개체 생성 및 내용 복사
Apache BeanUtils 예제
위 예제처럼 사용방법은 매우 간단하다. 일반적인 BeanUtils 흔히 쓰는 방법은 아래와 같다.
org.apache.commons.beanutils.BeanUtils 은 얕은 복사(shallow copy) 다. 때문에 많은 코딩규칙 툴 혹은 plugin 에서는 해당 클래스 사용을 권장하지 않고 있다.
commons-beantutils는 객체의 카피에 대해 validation 과정을 거치는 데, 타입전환 을 비롯하여 객체가 속한 클래스의 접근성까지 체크하기 때문에, 상당히 복잡하다. 이런 이유로 해당 유틸은 형편없는 성능을 보장?한다. ^^
대충 봐도 복잡하고 어렵다.
Spring 의 BeanUtils
아래와 같은 사용 예제코드를 보자
Spring에 내장된 BeanUtils도 copyProperties 방법을 사용하여 복사하지만 구현 방식은 매우 간단하며 두 객체의 동일한 이름의 속성에 대한 간단한 get/set을 수행하여 속성의 접근성만 확인합니다.구체적으로는 다음과 같다.
코드에서 알수 있듯이 멤버 변수 할당은 대상 객체의 멤버 목록을 기반으로 하며, ignore를 건너뛰고 원본 객체에 존재하지 않기 때문에 이 방법은 안전하다고 볼수 있다. 두 객체 간의 구조적 차이로 인한 오류는 발생하지 않지만, 같은 이름의 두 멤버 변수 유형이 동일해야 한다.
맺으면서...
오늘 위의 두 가지 BeanUtils를 간략히 분석하였는데 Apache에 있는 BeanUtils는 성능이 좋지 않기 때문에 권장하지 않으며, Spring의 BeanUtils를 사용하거나, Dozer, ModelMapper 등과 같은 다른 copy library를 고민해볼수도 있겠다.
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
}
}
}
}
}
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);
}
}
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);
}
}
}
}
}
}