Backend/Spring+Boot

Mapstruct 주의사항

findmypiece 2021. 11. 30. 14:08
728x90

Mapstruct vo, dto, entity 간 변환을 쉽게 할 수 있도록 해준다. 사실 전에는 클래스마다 static of 메소드를 만들어서 변환시 마다 빌더를 직접 코딩했는데 payload 방대할 경우 이게 참 못할 짓이다.

 

일반적으로 로직을 고민하는 시간이 코드를 작성하는 시간보다 훨씬 오래 걸리는데 방대한 payload 에 대해 빌더를 직접 코딩하다보면 반대 상황에 놓이게 되고 하나의 오타가 반나절을 잡아먹기도 한다.

 

Mapstruct 대한 예제는 정말 많으니 그걸 참고하도록 하고 여기에서는 주의사항에 대해 기록하고자 한다.

 

사람마다 다르겠지만 나는 request 데이터를 매핑하는 vo 클래스는 immtable 로 만드는 것을 선호한다. 클래스를 immtable로 만드는 방법은 간단하다. 생성자 또는 빌더만 정의하고 Setter 메소드를 두지 않으면 된다. 이 중에서도 나는 생성자보다는 빌더를 선호한다. 객체 생성시 속성 값을 다이나믹하게 할당하고 싶어서이다. 

 

우선 Mapstruct, Lombok 를 사용하는 환경이라면 아래와 같은 의존성이 필요하다.

implementation 'org.projectlombok:lombok:1.18.20'
implementation 'org.projectlombok:lombok-mapstruct-binding:0.2.0'
implementation 'org.mapstruct:mapstruct:1.4.2.Final'

그리고 아래와 같이 annotationProcessor 설정도 필수로 필요하다.

annotationProcessor "org.mapstruct:mapstruct-processor:1.4.2.Final"
annotationProcessor "org.projectlombok:lombok:1.18.20"
annotationProcessor "org.projectlombok:lombok-mapstruct-binding:0.2.0"

 

Mapstruct 에서는 변환시 타겟에 해당하는 클래스의 생성자 혹은 빌더가 활용되는데 이때 주의사항을 나열해보면 아래와 같다.

 

1. annotationProcessor 이 위와 같이 선언되어 있을 경우 타겟 클래스에 빌더가 정의되어 있다면 빌더가 사용되고 생성자가 정의되어 있다면 생성자가 사용된다. 동시에 선언되어 있다면 빌더가 우선순위가 더 높다. 

 

2. 빌더를 활용할 생각이라면 annotationProcessor 정의시 "org.mapstruct:mapstruct-processor:1.4.2.Final" 가 lombok 관련 annotationProcessor 보다 상단에 존재해야 한다. lombok 보다 하단에 위치할 경우 생성자만 활용된다.

 

3. 만약 생성자를 활용하는 거라면 @AllArgsConstructor 를 지정해서 모든 속성을 포함하는 생성자를 만들어야 하고 @NoArgsConstructor 로 기본생성자만 만들 생각이라면 @Setter로 setter 메소드를 반드시 포함시켜 줘야 한다.

 

여기까지가 공통적으로 주의해야 하는 내용이고 서칭하다보면 여기저기 많이 보이는 말들이다. 그런데 나는 이 외에 또 다른 문제가 발생했다.

 

나는 vo 클래스를 정의할 때 커스텀클래스를 포함하곤 한다. 방대한 payload 를 primitive type 으로만 정의할 경우 보는게 어려워 연관된 속성은 내부클래스로 정의해서 해당 타입을 사용한다. 예를 들어 아래와 같이 말이다.

@Getter
@Builder
public class Test {
   private String testName;
   private String testAge;
   private AddData addData;
   
   @Getter
   @Builder
   public static class AddData{
      private String c;
      private String d;
   }
}

 

위와 같은 클래스를 타겟클래스로 사용할 경우 Mapper 의 Mapping 은 아래와 같이 수동으로 작성해야 한다.

@Mapper
public interface TestMapper {

    @Mapping(target = "addData", expression = "java(toAddData(testParam))")
    Test toTest(TestParam testParam);

    default Test.AddData toAddData(TestParam testParam) {
        return Test.AddData.builder()
                .c(testParam.getC())
                .d(testParam.getD())
                .build();
    }
}

 

문제는 이렇게 할 경우 아래와 같은 에러가 발생하며 컴파일이 실패한다. 빌더에서 addData 라는 이름의 속성을 알 수 없다고 한다. 

Unknown property "addData" in result type Test.TestBuilder ...

처음에는 빌더를 사용할 경우 속성에 대문자를 사용하지 못하는건가? 라고 생각했지만 그건 또 아니다. addData 라는 속성명을 아래와 같이 AddData 로 바꾸면 잘 된다.

Test.addData -> Test.Adddata
@Mapping.target.addData -> @Mapping.target.AddData

그리고 또 아래와 같이 소문자만으로 구성하면 또 잘 된다.

Test.addData -> Test.adddata
@Mapping.target.addData -> @Mapping.target.addData

 

이걸 이틀 동안 잡고 있었는데 결론은 Mapstruct 버그인 것 같다. @Mapping 에 target 을 지정할 때 add 로 시작하면서 바로 뒤에 대문자가 올 경우에만 컴파일이 실패한다.

 

일단은 Mapstruct 를 사용하는 타겟 클래스를 정의할 때 add 로 시작하면서 바로뒤에 대문자가 오는 속성명은 사용하지 않아야 할 것 같다. 만약 이런 속성명을 포함하는 클래스를 Mapstruct 에서 정상적으로 사용하는 케이스가 있다면 제보 부탁한다.

728x90