슬기로운개발생활

[Spring] MapStruct 와 Lombok 본문

CS/Spring

[Spring] MapStruct 와 Lombok

슬기로운개발자 2021. 2. 10. 15:11

MapStruct: 클래스간 변환을 쉽게 해주고 변환 코드를 자동으로 생성해주는 라이브러리

Lombok: 보일러 플레이트 코드 (getter / setter / constructor / builder 등) 를 줄여주는 자동 코드 생성 라이브러리

이 두개를 섞어서 사용할 때 주의해야 할 점이 있더라...버전의 문제였다. 학부 프로젝트하면서 개발할 땐 버전에 대해 신경쓰지않았는데, 버전은 정말 중요한 요소 중 하나이고, 현재 프로젝트 기술 스택에 포함되어 있다면 릴리즈 변경 사항을 챙겨보길 바란다.

버전을 올리거나 내리는건 생각하는 것 만큼 단순한 일이 아니다....
참고 깃헙 이슈: https://github.com/mapstruct/mapstruct/issues/510

예시 클래스와 매퍼는 아래와 같고, 사용된 라이브러리 버전은 Lombok(1.18.12), MapStruct(1.3.1.Final) 이다. 또한, 해당 클래스들은 모두 같은 모듈내에 있다.

a 라는 같은 모듈내에 있다.

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Person {
    private String name;

    @JsonProperty("name")
    public void setCustomName(String name) {
        // 예시 코드라 아무것도 안하지만, 보통 CustomSetter에는 다른 로직이 들어갈 것이다.
        this.name = "custom = " + name;
    }

    @JsonProperty("name")
    public String getCustomName() {
        return this.name;
    }

    public static class PersonBuilder {
        public PersonBuilder customName(String name) {
            return this.customName(name);
        }
    }
}
@Data
public class PersonRequest {
    private String personName;
}
@Mapper
public interface PersonMapper {
    @Mapping(target = "customName", source = "personName")
    @Mapping(target = "name", ignore = true)
    Person toEntity(PersonRequest req);
}

 

[Issue] Gradle dependencies 순서에 따라 생성되는 코드가 다르다!! AP(AnnotationProcessor) 동작 순서가 달라서

1. Lombok 다음 MapStruct: MapperImpl 은 setter 로 생성이 된다.

AP가 컴파일 첫번째 과정에서 Lombok 이 먼저 만들어놓은 getter, setter 를 그 다음에 실행된 MapStruct가 사용해서 코드를 생성할 수 있기 때문에 setter 코드를 바로 생성한다.
두번째 과정에서 Lombok이 builder를 생성하지만 MapStruct는 변하지 않는다.

implementation "org.projectlombok:lombok:1.18.12"
implementation "org.mapstruct:mapstruct:1.3.1.Final"

annotationProcessor "org.projectlombok:lombok:1.18.12"
annotationProcessor "org.mapstruct:mapstruct-processor:1.3.1.Final"
@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2021-02-10T14:23:34+0900",
    comments = "version: 1.3.1.Final, compiler: javac, environment: Java 14.0.2 (AdoptOpenJDK)"
)
public class PersonMapperImpl implements PersonMapper {

    @Override
    public Person toEntity(PersonRequest req) {
        if ( req == null ) {
            return null;
        }

        Person person = new Person();

        person.setCustomName( req.getPersonName() );

        return person;
    }
}

 

2. MapStruct 다음 Lombok: MapperImpl 이 builder 로 생성한다.

먼저 실행된 MapStruct가 접근할 수 있는 getter/setter가 없는 상태라 생성에 실패한다. 그 다음 Lombok이 getter/setter 를 만든다.
다음 라운드에서 Lombok이 builder를 만들고, MapStruct가 다시 생성하려할텐데 이때는 builder가 있는 상태라 builder로 생성을 시도한다.

implementation "org.projectlombok:lombok:1.18.12"
implementation "org.mapstruct:mapstruct:1.3.1.Final"

// Changed order
annotationProcessor "org.mapstruct:mapstruct-processor:1.3.1.Final"
annotationProcessor "org.projectlombok:lombok:1.18.12"
@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2021-02-10T14:23:34+0900",
    comments = "version: 1.3.1.Final, compiler: javac, environment: Java 14.0.2 (AdoptOpenJDK)"
)
public class PersonMapperImpl implements PersonMapper {

    @Override
    public Person toEntity(PersonRequest req) {
        if ( req == null ) {
            return null;
        }

        PersonBuilder person = Person.builder();

        person.customName( req.getPersonName() );

        return person.build();
    }
}

 

이 때, 문제가 될 만한건 뭘까?

사용자는 setCustomName 이라는 Setter 를 사용하기 위해 구현해놓았다. Mapper 인터페이스를 만들때도 Setter가 있으니 문제가 없다고 생각할 것이다. 하지만 Mapper가 Builder 메소드를 사용하여 코드를 생성할 때 문제가 된다. 주석친 부분이 없어 컴파일 에러가 발생하기 때문이다.

우선, 기본적으로 MapStruct 는 Target 객체에 @Builder 어노테이션이 달려있다면 Builder 메소드를 우선 사용하게 되어있다. 하지만 위의 이슈 때문에 제대로 작동되지 않았고 컴파일 에러까지 난 상황이다.

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Person {
    private String name;

    @JsonProperty("name")
    public void setCustomName(String name) {
        this.name = "custom = " + name;
    }

    @JsonProperty("name")
    public String getCustomName() {
        return this.name;
    }

//    public static class PersonBuilder {
//        public PersonBuilder customName(String name) {
//            return this.customName(name);
//        }
//    }
}

 

어떻게 해결해야 할까?

우선 Lombok 1.18.16 미만 버전을 사용할 경우 해결방법이다.

  1. (예시로 든 케이스 한정이긴 하나...) @Mapper 어노테이션에 값을 주는 방법이 있다. 
  2. Lombok이 달려있는 객체와 Mapper 인터페이스 클래스를 각각 다른 모듈에 위치시킨다.
  3. 난 같은 모듈에 있어야 하고 순서 신경쓰기 귀찮다? -> 커스텀한 Setter & Builder 메소드 둘 다 항상 작성한다.
// 1번 방법
@Mapper(builder = @Builder(disableBuilder = true))
public interface PersonMapper {

2번 방법

Lombok 1.18.16 버전 이상을 사용할 경우 해결방법이다.

lombok-mapstruct-binding 의존성을 추가해준다. 이는 Lombok과 MapStruct가 함께 잘 동작하도록 만들어준다.

implementation "org.mapstruct:mapstruct:1.3.1.Final"
implementation "org.projectlombok:lombok:1.18.16"
implementation 'org.projectlombok:lombok-mapstruct-binding:0.2.0'

// 이제 순서 상관없음
annotationProcessor "org.projectlombok:lombok-mapstruct-binding:0.2.0"
annotationProcessor "org.mapstruct:mapstruct-processor:1.3.1.Final"
annotationProcessor "org.projectlombok:lombok:1.18.16"

 

오류가 있거나 틀린 부분이 있다면 태클 감사히 받겠습니다!
Comments