[Spring] MapStruct (+ ModelMapper, Reflection) 사용법
Java & Spring

[Spring] MapStruct (+ ModelMapper, Reflection) 사용법

MapStruct란 Entity를 DTO로 변환하거나 DTO를 Entity로 변환하려고 할 때 사용하는 객체 매핑 라이브러리다.

이 라이브러리가 어떻게 사용되게 되었는지 기존 개발 방식부터 보면서 알아본다.


Getter, Setter, Builder 패턴

기존에 우리는 객체 매핑을 해줄 때 getter, setter 혹은 builder 패턴을 이용해 매핑 처리를 해줬다.

// getter, setter로
Member member = new Member();
member.setName(memberDto.name());
member.setEmail(memberDto.email());

// builder 패턴으로
return Member.builder()
	.name(memberDto.name())
	.email(memberDto.email());

이 방식으로 객체 매핑을 하면 필드끼리 매핑되는 것을 컴파일 타임에 식별할 수 있고, 중간에 어떤 과정이 들어간 라이브러리와는 다르게 바로 매핑이 되기 때문에 성능에 대한 영향이 없다.

 

하지만 만약 어느 한쪽에 객체의 필드를 추가하거나 변경하는 경우 매핑하는 코드 부분도 같이 수정해줘야 한다는 단점이 있고, 필드가 어누 많다면 데이터를 누락시키는 실수를 하거나 다른 데이터와 매핑 시키는 휴먼 에러가 발생할 수도 있다.

 

이 방식의 단점을 개선하고자 나타난게 객체 변환 라이브러리다.

대표적으로 MapStruct, ModelMapper가 있다.


MapStruct

MapStruct를 사용해주려면 우선 dependency 추가를 해줘야한다.

// mapstruct
implementation 'org.mapstruct:mapstruct:1.4.2.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'

 

 

가장 기본적인 사용법은 아래와 같다. (밑에서는 @Entity 등과 같은 애노테이션이 다 달려있다고 가정하고 간소화해서 표현하겠다.)

두 객체의 필드값이 똑같은 경우이다.

// 이 클래스를
public class TestDto {
    private String a;
    private String b;
    private String c;
}

// 이 클래스로 바꾸고 싶을 때
public class TestEntity {
    private String a;
    private String b;
    private String c;
}

// 이렇게 구현!
@Mapper
public interface TestMapper {
    TestEntity toTestEntity(TestDto testDto);
}

 

여기서 만약 서로 다른 속성이 있는 경우는 아래와 같다.

// 이 클래스를
public class TestDto {
    private String a;
    private String b;
    private String c;
}

// 이 클래스로 바꾸고 싶을 때
public class TestEntity {
    private String a;
    private String b;
    private String d;
}

// 이렇게 구현!
@Mapper
public interface TestMapper {
    @Mapping(source = "c", target = "d")
    TestEntity toTestEntity(TestDto testDto);
}

 

target인 객체에 필드가 더 많을 때

// 이 클래스를
public class TestDto {
    private String a;
    private String b;
}

// 이 클래스로 바꾸고 싶을 때
public class TestEntity {
    private String a;
    private String b;
    private String c;
}

// 이렇게 구현!
@Mapper
public interface TestMapper {
    TestEntity toTestEntity(TestDto testDto, String c);
}

 

두 객체를 source로 두고 하나의 객체가 필드로 들어가는 target인 객체를 생성하고 싶을 때

// 이 클래스 2개를
public class TestDto {
    private String a;
    private String b;
}

public class Test2Dto {
    private String c;
}

// 이 클래스로 바꾸고 싶을 때
public class TestEntity {
    private String a;
    private String b;
    private Test2Dto test;
}

// 이렇게 구현!
@Mapper
public interface TestMapper {
    @Mapping(source = "test2Dto", target = "test")
    TestEntity toTestEntity(TestDto testDto, Test2Dto test2Dto);
}

 

target에 넣지 않을 필드값 무시하기

참고로 @Mapper에 들어가는 unmappedTargetPolicy는 source의 필드가 target에 매핑되지 않을 때 정책으로, ERROR로 설정하면 매핑되지 않을 때 오류가 발생한다.

자세한 정책은 여기서 확인(https://mapstruct.org/documentation/stable/reference/html/)

// 이 클래스를
public class TestDto {
    private String a;
    private String b;
}

// 이 클래스로 바꾸고 싶을 때
public class TestEntity {
    private String a;
    private String b;
    private String c;
}

// 이렇게 구현!
@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface TestMapper {
    @Mapping(target = "c", ignore = true)
    TestEntity toTestEntity(TestDto testDto);
}

// 혹은 이렇게!
@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface TestMapper {
    TestEntity toTestEntity(TestDto testDto);
}

 

 

이렇게 위에 생긴 implementation을 타고 들어가면 생성된 구현체를 볼 수 있다. 참고로 처음에 메서드를 추가하면 구현체에 메서드가 오버라이드 되지 않아 오류가 날 수 있는데, 서버를 올리면 컴파일되면서 코드가 생성되니 빨간줄이 불편하면 빌드를 한번 해주면 해결된다:)

 


MapStruct VS ModelMapper?

ModelMapper도 MapStruct와 마찬가지고 객체 매핑 처리를 해주는 라이브러리다.

그런데 MapStruct를 사용하는 것을 많이 볼 수 있는데, 이는 ModelMapper는 Reflection API를 사용하여 객체 필드 정보를 추출하고 매핑하기 때문에 성능이 떨어지기 때문이다. 실제로 테스트를 돌려보면 꽤나 차이가 많이 난다. (MapStruct가 더 빠름) 반면에 MapStruct는 컴파일 시 미리 생성된 구현체를 통해 매핑하기 때문에 속도적인 측면에서 이점이 있다.

 


+ 추가 Reflection?

Reflection은 구체적인 클래스 타입을 알지 못해서 그 클래스의 메소드와 타입 그리고 변수들을 접근할 수 있도록 해주는 자바 API다.

이렇게 말하면 잘 이해가 안되고 아래 코드를 보면 이해가 간다.

Class a = Class.forName("클래스이름");

// 메서드 가져오기
Method[] m = a.getMethods();

// 필드 가져오기
Field[] f = c.getFields();

어디서 쓰이는지 감이 안와서 예를 들자면,

스프링에서는 Reflection을 이용해서 빈을 애플리케이션에서 가져와서 사용한다.

728x90
반응형