Programming/Spring Boot

BeanUtils.copyProperties를 사용한 객체 필드값 복사

Jan92 2023. 11. 25. 23:02

BeanUtils.copyProperties를 사용한 객체 필드값 복사

BeanUtils.copyProperties 필드 값 복사

 

프로젝트를 진행하다 보면 Dto -> Entity 또는 Entity -> Dto 변환하는 과정이 필요한데요.

(그 외에도 객체 간의 필드 값을 복사해야 하는 경우)

 

개인적으로는 지금까지 Entity 또는 Dto에서 builder를 통해 변환된 객체를 반환하는 method를 만들어 사용했으며, 동료 분의 코드에서 'BeanUtils.copyProperties' 기능을 사용하시는 것을 보고 해당 기능에 대해 알아보게 되었습니다.

(데이터 복사에는 BeanUtils 외에도 ModelMapper, MapStruct 등의 라이브러리를 사용할 수 있으며, 관련된 부분도 아래 내용을 통해 언급됩니다.)

 

***

아래 내용에서는 'Entity와 Dto가 각각 어떤 기능까지 가지고 있어야 할지', '내부적으로 리플렉션이 사용되는 BeanUtils.copyProperties가 성능상 문제는 없을지', 'Entity에 set 메서드가 생기는 것에 대해 어떻게 생각하는지' 등의 고민해야 될 부분이 있으며, 이는 정답이 있는 것이 아니라 개발자마다 의견이 다를 수 있는 부분이기 때문에 이점 참고해서 봐주시면 좋을 것 같습니다.

(개인적인 의견을 댓글로 남겨주시면 저를 포함한 이 내용을 보시는 분들께 도움이 될 것 같습니다.)

 


get, set 사용하는 방법

@AllArgsConstructor
@Getter
@Entity
public class Source {

    @Id
    private Long id;
    private String name;
    private String value;
    private Boolean isUsed;
}

(Source class)

 

@Getter
@Setter
public class Target {

    private Long id;
    private String name;
    private String value;
    private Boolean isUsed;
}

(Target class)

 

@Test
void copyTest_getset() {
    Source source = new Source(1L, "testName", "testValue", true);

    Target target = new Target();
    target.setId(source.getId());
    target.setName(source.getName());
    target.setValue(source.getValue());
    target.setIsUsed(source.getIsUsed());
}

(get, set example)

 

먼저 객체 간의 필드 값을 복사할 때 사용할 수 있는 가장 기본적인 방법인 get, set 메서드를 사용하는 방법입니다.

 

이 방법의 경우 멤버 변수가 많으면 많을수록 get, set 메서드를 사용하는 코드의 길이가 길어지며, 유지보수에도 불편하다는 문제가 있습니다.

또한 타겟이 되는 객체의 멤버 변수에 set 메서드가 존재해야 하기 때문에 Dto -> Entity 변환의 경우 Entity의 멤버 변수에 set 메서드를 만들어야 한다는 문제가 생기게 됩니다.

 

하지만 아래서 살펴볼 다른 기능들의 경우 내부적으로 리플렉션을 사용하거나 외부 의존성을 추가해야 하며, 속도적인 측면에서는 get, set을 사용하는 것이 가장 빠릅니다.

 

 


BeanUtils.copyProperties 사용하는 방법

BeanUtils.copyProperties method

이어서 BeanUtils.copyProperties를 통해 객체 필드 값을 복사하는 방법입니다.

(BeanUtils 클래스는 org.springframework.beans 패키지에 속해있습니다.)

 

@Test
void copyTest_copyProperties1() {
    Source source = new Source(1L, "testName", "testValue", true);

    Target target = new Target();
    BeanUtils.copyProperties(source, target);
}


@Test
void copyTest_copyProperties2() {
    Source source = new Source(1L, "testName", "testValue", true);

    Target target = new Target();
    BeanUtils.copyProperties(source, target, "name", "value");
}

(copyProperties example)

 

사용법은 간단한데요. 위 get, set 메서드를 사용한 예시와 동일한 Source, Target을 가지고 위 코드와 같이 copyProperties 메서드를 통해 객체의 필드 값을 복사할 수 있으며, 복사하고 싶지 않은 필드가 있는 경우에도 ignoreProperties 파라미터를 활용하여 해당 필드에 대한 값 복사가 되지 않도록 할 수 있습니다.

 

BeanUtils.copyProperties를 사용할 때 주의할 점으로는 해당 기능의 내부적인 동작 과정에서 리플렉션(Reflection)을 통해 get, set 메서드를 조회하여 값을 복사하기 때문에 복사할 원본이 되는 Source의 경우 멤버 변수에 대한 get 메서드가 구현되어 있어야 하고, 값이 복사될 대상이 되는 Target의 경우 멤버 변수에 set 메서드가 구현되어 있어야 한다는 점입니다.

 

때문에 해당 기능을 사용하여도 Dto -> Entity 변환을 하기 위해서는 Entity의 각 필드에 set 메서드가 구현되어야 합니다.

추가적으로 이때 복사는 얕은 복사가 된다는 점도 알아두어야 합니다.

 

***

BeanUtils.copyProperties 내부적으로 리플렉션이 사용되는 것을 보고 처음에는 막연하게 '성능이 안 좋을 것 같은데 안 쓰는 게 좋지 않을까?'라고 생각했는데요.

런타임 시점에서 사용할 인스턴스가 선택되어 동작하는 리플렉션의 유연함 때문에 컴파일러의 최적화를 받을 수 없으며, 때문에 성능상의 단점은 존재할 수밖에 없다고 생각합니다.

 

하지만 해당 기능이 빈번하게 호출되지 않으며, 성능상의 문제가 발생하지 않는 서비스 환경이라면 위의 get, set 메서드를 직업 호출하는 코드를 짜지 않아도 된다는 장점도 있을 수는 있겠구나 생각하게 되었는데요.

만약 사용하게 되더라도 단순 편리성만 생각하여 사용하는 게 아니라 이러한 내용을 인지하고 사용하는 것이 필요할 것 같습니다.

(개인적으로는 다른 방안이 있기 때문에 굳이 쓰지 않을 것 같습니다.)

 

 


builder 사용하는 방법

@AllArgsConstructor
@Getter
@Entity
public class Source {

    @Id
    private Long id;
    private String name;
    private String value;
    private Boolean isUsed;

    public Target toTarget() {
        return Target.builder()
                .id(id)
                .name(name)
                .value(value)
                .isUsed(isUsed)
                .build();
    }
}

(Source class)

 

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class Target {

    private Long id;
    private String name;
    private String value;
    private Boolean isUsed;
}

(Target class)

 

@Test
void copyTest_builder() {
    Source source = new Source(1L, "testName", "testValue", true);
    Target target = source.toTarget();
}

(builder example)

 

앞서 이야기했던 builder를 통한 Dto 또는 Entity 변환입니다.

이 경우 builder를 사용하기 때문에 Entity에 set 메서드를 생성하지 않아도 된다는 장점이 있는데요.

 

하지만 Entity에 연결되는 Dto가 많아질수록 각각의 Dto에 대한 메서드가 구현되어야 하며, 과연 Entity에서 이러한 메서드가 구현되는 게 맞는지에 대한 의문도 생기게 됩니다.

번거로운 측면은 있지만 set 메서드를 구현하지 않아도 된다는 점과 리플렉션을 사용하지 않는다는 장점이 있습니다.

 

 


객체의 필드 값을 복사하는 라이브러리

객체의 필드 값을 복사할 때는 위 방법 외에도 'ModelMapper' 또는 'MapStruct' 같은 라이브러리도 있는데요.

 

ModelMapper의 경우 BeanUtils와 마찬가지로 내부적으로 리플렉션이 사용되기 때문에 성능적 이슈가 생길 수 있으며, MapStruct 같은 경우에는 리플렉션이 사용되지 않기 때문에 앞선 두 가지 기능보다 빠르다는 특징이 있습니다.

 

    //Test Results = 55ms
    @Test
    void copyTest_getset() {
        Source source = new Source(1L, "testName", "testValue", true);
        for (int i = 0; i < 1000000; i++) {
            Target target = new Target();
            target.setId(source.getId());
            target.setName(source.getName());
            target.setValue(source.getValue());
            target.setIsUsed(source.getIsUsed());
        }
    }

    //Test Results = 3sec 53ms
    @Test
    void copyTest_copyProperties() {
        Source source = new Source(1L, "testName", "testValue", true);
        for (int i = 0; i < 1000000; i++) {
            Target target = new Target();
            BeanUtils.copyProperties(source, target);
        }
    }

    //Test Results = 2sec 93ms
    @Test
    void copyTest_ModelMapper() {
        ModelMapper modelMapper = new ModelMapper();
        Source source = new Source(1L, "testName", "testValue", true);
        for (int i = 0; i < 1000000; i++) {
            modelMapper.map(source, Target.class);
        }
    }


    //Test Results = 55ms
    @Test
    void copyTest_MapStruct() {
        TestMapper mapper = new TestMapperImpl();
        Source source = new Source(1L, "testName", "testValue", true);
        for (int i = 0; i < 1000000; i++) {
            Target target = mapper.toDto(source);
        }
    }

(대략적인 테스트 결과이며, 라이브러리에 대한 사용 방법은 생략하였습니다.)

 

내부적으로 Reflection이 사용되는 BeanUtils와 ModelMapper에 대한 테스트 시간이 get, set을 사용하는 방식과 MapStruct를 사용하는 방식에 비해 훨씬 오래 걸리는 것을 확인할 수 있습니다.

 

궁금하신 부분이나 잘못된 부분은 댓글 남겨주시면 확인하겠습니다. 감사합니다.

 

 

 

< 관련 자료 >

2021.12.21 - [Programming/Java] - 역할 분리를 위한 Entity, DTO 개념과 차이점