본문 바로가기
Java & Spring

mapStruct 의존성 순서 문제 해결방법과 간단한 사용법

by devlect 2023. 12. 11.

각 레이어 간 전달되는 DTO간 변환이나 DTO, Entity간 변환을 별도의 Mapper 클래스를 만들어 일일이 수작업으로 해주다보면 힘이 들기도 하고, 실수도 자주 생기게 된다. 테스트 코드에서 걸러주긴 하지만 이건 테스트 코드에서 거르기 전에 시스템 적으로 예방하는 편이 맞다. 물론 요즘에는 Copilot을 쓰면 이런 보일러 플레이트 코드를 자동완성해주니 일정 부분 해결되긴 하지만 좀 더 구조적인 방법을 찾고 싶다면 전문 Mapper 라이브러리를 써보는 것도 좋다.

 

이 분야에서 나름 네임드는 ModelMappermapStruct다. ModelMapper는 별도의 인터페이스 정의같은 것 없이 상대적으로 좀 더 간편하게 시작할 수 있지만 구체적인 타입과 메서드를 알기 위해 런타임 리플렉션이 일어나므로 속도 측면에서 mapStruct보다 느리다. mapStruct는 인터페이스를 정의해주고 몇 가지 설정하는 부분들이 있어 좀 손이 더 가는 면이 있지만 입맛에 맞게 설정할 수 있다고 생각하면 또 장점이 된다.

 

___

mapStruct 의존성 문제

처음 써보려고 하면 의존성 문제를 제일 먼저 만나게 된다. Lombok을 사용한다면 어노테이션 프로세서 우선순위 문제가 생기는데 build.gradle에 나열된 의존성이 순서가 있다. mapStruct가 Lombok을 이용하므로 Lombok 먼저 써주고 mapStruct를 써주면 문제가 생기지 않는다. 근데 생각해보면 의존성 순서는 사람이 바뀌고 유지보수가 길어지다보면 사람에 따라서 바뀔 위험이 언제나 있다. 심지어 점점 더 높아진다. 개조심이라고 써 둘 것이 아니라 개가 사람을 못 물도록 구조를 만들어두길 원한다면 lombok-mapstruct-binding 의존성을 추가해주는 것도 방법이다. 다음은 그 2가지 방법에 대한 설명이다.

 

방법1: 의존성 순서 지키기

annotationProcessor 순서를 lombok 먼저 그 다음에 mapstruct-processor 추가해주기

    compileOnly 'org.projectlombok:lombok'

    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.mapstruct:mapstruct:1.5.5.Final'

    // 순서 중요
    annotationProcessor 'org.projectlombok:lombok'
    annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'

 

방법2: 추가 의존성 설정하고, 순서 상관없이 사용하기

lombok-mapstruct-binding 의존성 추가하고 annotationProcessor 순서 마음대로 하기

    compileOnly 'org.projectlombok:lombok'

    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.mapstruct:mapstruct:1.5.5.Final'
    implementation 'org.projectlombok:lombok-mapstruct-binding:0.2.0'

    // lombok-mapstruct-binding 의존성을 추가해주면 
    // mapstruct-processor와 lombok 순서를 바꿔줘도 된다
    annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0'
    annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
    annotationProcessor 'org.projectlombok:lombok'

 

 

___

 

간단한 사용법

아래는 mapStruct의 간단한 사용 예시다. Car와 CarDto 클래스가 있고 이를 mapStruct를 이용해 맵핑하는 방법을 간단히 작성해 보았다.

 

Car 클래스

@Data
public class CarDto {
    private String make;
    private int seatCount;
    private String carType;
    private int discountPrice;
}

 

CarDto 클래스

@Getter
@ToString
@AllArgsConstructor
public class Car {
    private String make;
    private int numberOfSeats;
    private CarType type;
    private int price;

    public int getDiscountPrice(int percent) {
        return price * (100 - percent) / 100;
    }

    public void changePrice(int price) {
        this.price = price;
    }
}

 

CarType 클래스

public enum CarType {
    NONE,
    SEDAN,
    SUV
}

 

Mapper 인터페이스

1. 맵핑을 원하는 항목을 @Mapping에 지정해주고

2. 별도의 연산이 필요한 경우 @AfterMapping 어노테이션 사용

import org.mapstruct.AfterMapping;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;

@Mapper
public interface CarMapper {
    CarMapper INSTANCE = org.mapstruct.factory.Mappers.getMapper(CarMapper.class);

    @Mapping(source = "numberOfSeats", target = "seatCount")
    @Mapping(source = "type", target = "carType")
    @Mapping(source = "price", target = "discountPrice")
    CarDto toDto(Car car);

    @Mapping(source = "discountPrice", target = "price")
    @Mapping(source = "seatCount", target = "numberOfSeats")
    @Mapping(source = "carType", target = "type")
    Car toEntity(CarDto carDto);

    @AfterMapping
    default void applyDiscount(Car car, @MappingTarget CarDto carDto) {
        carDto.setDiscountPrice(car.getDiscountPrice(50));
    }

    @AfterMapping
    default void applyOriginalPrice(CarDto carDto, @MappingTarget Car car) {
        // 예를 들어, 할인된 가격의 2배를 원래 가격으로 설정한다.
        int price = carDto.getDiscountPrice() * 2;
        car.changePrice(price);
    }
}

 

사용방법

public class CarMappingExample {
    public static void main(String[] args) {
        Car car = new Car("BMW", 5, "SEDAN", 1000); // price 설정

        CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);
    }
}

 

끗!

댓글