보라코딩

MapStruct 본문

코딩/Spring

MapStruct

new 보라 2024. 4. 4. 12:23

MapStruct란?

  • Java bean 유형 간의 매핑 구현을 단순화하는 코드 생성기
    • 매핑 코드를 자동으로 생성해주어 반복적인 매핑 작업 효율적으로 처리
    • DTO와 Entity간의 매핑을 편리하게 도와주는 도구

MapStruct 사용 방법

* 의존성 추가

  • 주의 : Lombok 뒤에 MapStruct dependency 선언 필요
    • MapStruct는 Lombok의 getter, setter, builder를 이용해서 생성되기 때문
dependencies {
    ...
    implementation 'org.mapstruct:mapstruct:1.5.3.Final'
    annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.3.Final'
    ...
}

1. 기본 사용 방법

  • Car ➡️ CarDto Mapping 하기

Car

  • @Data 사용하는 이유
    • 매핑 시, 필드에 접근하기 위해 Getter, Setter, Constructor 필요
@Data
@AllArgsConstructor
public class Car {

    private String name;
    private int numberOfSeats; // 변수명 다름
    private String type;
}

CarDto

@Data
public class CarDto {

    private String name;
    private int seatCount; // 변수명 다름
    private String type;
}

🎉 CarMapper

  • CarCarDto변수명이 다른 경우 매핑
  • Mapper interface에 @Mapper 어노테이션 붙이면 자동으로 Mapper 구현체 생성
  • instance 선언 시 매퍼에 접근 가능
  • @Mapping 을 통해 다른 변수명 매핑
@Mapper // MapStruct가 자동으로 CarMapper 구현체 생성
public interface CarMapper {

    // 매퍼 클래스에서 CarMapper 찾을 수 있게 하는 방법
    // 매퍼 interface에서 위와 같이 Instance를 선언해주면 매퍼에 대한 접근이 가능
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);

    // Car -> CarDto 매핑
    @Mapping(source = "numberOfSeats", target = "seatCount") // 다른 변수명 매핑
    CarDto toCarDto(Car car);
}

Mapper interface 빌드하면 자동으로 구현체 만들어줌

build/classes/java/main/에 구현체 생성
public class CarMapperImpl implements CarMapper {
    public CarMapperImpl() {
    }

    public CarDto toCarDto(Car car) {
        if (car == null) {
            return null;
        } else {
            CarDto carDto = new CarDto();
            carDto.setSeatCount(car.getNumberOfSeats());
            carDto.setName(car.getName());
            carDto.setType(car.getType());
            return carDto;
        }
    }
}

 

테스트 코드로 확인

public class CarMapperTest {

    @Test
    void mapstruct로_car을_carDto로_mapping_할_수_있다() {

        //given
        Car car = new Car("Morris", 5, "SEDAN");

        //when
        CarDto carDto = CarMapper.INSTANCE.toCarDto(car);

        //then
        assertThat( carDto ).isNotNull();
        assertThat( carDto.getName() ).isEqualTo( "Morris" );
        assertThat( carDto.getSeatCount() ).isEqualTo( 5 );
        assertThat( carDto.getType() ).isEqualTo( "SEDAN" );
    }

}

2. 여러 객체를 하나의 객체에 매핑

  • One, Two ➡️ Three Mapping 하기

One

@Data
@AllArgsConstructor
public class One {

    private boolean a;
    private int b;
    private String c;
    private String d;

}

Two

@Data
@AllArgsConstructor
public class Two {

    private List<String> e;
    private String f;

}

Three

  • One에 있는 변수 c가 없음
  • Two에 있는 변수명이 다름
@Data
public class Three {

    private boolean a;
    private int b;
    // c 없음
    private String d;

    private List<String> eee; // 변수 다름
    private String fff; // 변수 다름

}

🎉 FourMapper

  • @Mapping을 사용하여 변수명 매핑
  • One, Two에 있는 변수가 Three에 없는 것은 문제 없음
// 여러_객체를_하나의_객체에_매핑할_수_있다
@Mapper
public interface FourMapper {

    FourMapper Instance = Mappers.getMapper(FourMapper.class);

    // One, Two -> Three 매핑
    @Mapping(source = "two.e", target = "eee")
    @Mapping(source = "two.f", target = "fff")
    Three toThree(One one, Two two);
}

테스트 코드로 확인

public class FourMapperTest {
    
    @Test
    void 여러_객체를_하나의_객체에_매핑할_수_있다() {
        // given
        One one = new One(true, 2, "c", "d");
        Two two = new Two(new ArrayList<>(), "f");

        // when
        Three three = FourMapper.Instance.toThree(one, two);

        // then
        assertThat( three ).isNotNull();
        assertThat( three.isA() ).isEqualTo(true);
        assertThat( three.getB() ).isEqualTo(2);
        assertThat( three.getD() ).isEqualTo("d");
        assertThat( three.getEee() ).isEqualTo(new ArrayList<>());
        assertThat( three.getFff() ).isEqualTo("f");
    }
}

3. 여러 인자 값을 매핑

  • Five ➡️ Six Mapping 하기

Five

@Data
@AllArgsConstructor
public class Five {

    private boolean a;
    private int b;
    private String c;
    private String d;

    private Date creationDate;
}

Six

  • Five에 있는 c 가 없음
  • Six에 e, f 가 추가됨
@Data
@AllArgsConstructor
public class Six {

    private boolean a;
    private int b;
    // c 없음
    private String d;

    private String e; // 추가
    private Long f; // 추가

    private Date creationDate;
}

🎉 SevenMapper

  • Six에만 있는 e, f 에 대한 파라미터 추가 가능
  • 필수는 아니며 생략 시 null 적용됨
// 여러가지_다른_인자_값도_매핑할_수_있다
@Mapper
public interface SevenMapper {

    SevenMapper INSTANCE = Mappers.getMapper(SevenMapper.class);

    // Five -> Six 매핑
    Six toSix(Long f, Five five); // Six에만 있는 e 매핑 안함
}

테스트 코드로 확인

  • 매핑하지 않은 e는 null
public class SevenMapperTest {

    @Test
    void 여러가지_다른_인자_값도_매핑할_수_있다(){

        // given
        Five five = new Five(true, 2, "c", "d", new Date());

        // when
        Six six = SevenMapper.INSTANCE.toSix(6L, five);

        // then
        assertThat(six).isNotNull();
        assertThat(six.isA()).isTrue();
        assertThat(six.getB()).isEqualTo(2);
        assertThat(six.getD()).isEqualTo("d");
        assertThat(six.getE()).isNull(); // mapping 안한 e는 null
        assertThat(six.getF()).isEqualTo(6L);
    }
}

4. 추가 매핑 방법 (Custom)

  • default 값 지정
    • 객체에 빈 값이 들어오는 경우 또는 특정 값을 지정해야 하는 경우
    • defaultValue(단순한 기본값), defaultExpression(동적값) 사용하여 값 지정 가능
  • 특정 필드 매핑 무시
    • ignore 사용해서 특정 필드 제외 가능
// 매핑시_default값_지정_가능하며_매핑_무시할_수_있다
@Mapper
public interface EightMapper {

    EightMapper INSTANCE = Mappers.getMapper(EightMapper.class);

    // Five -> Six 매핑
    @Mapping(source = "five.b", target = "b",  ignore = true) // b는 둘다 있어
    @Mapping(source = "five.d", target = "d", defaultValue = "default_d") // d는 둘다 있어
    @Mapping(source = "five.creationDate", target = "creationDate", defaultExpression = "java(new Date())")
    @Mapping(source = "e", target = "e", defaultValue = "default_e") // e는 six만 있어
    Six toSix(String e, Long f, Five five);
}

테스트 코드로 확인

  • 변수 b : ignore로 무시했기 때문에 int면 0, String이면 null
  • 변수 d, e : 값이 없을 경우에 defaultValue 적용됨
  • 변수 createDate : 동적값 적용됨
@Test
void ignore_사용하여_매핑_무시할_수_있다(){

    // given
    Five five = new Five(true, 222, "c", "d값_지정", new Date());

    // when
    Six six = EightMapper.INSTANCE.toSix("e값_지정", 6L, five);

    // then
    assertThat(six).isNotNull();
    assertThat(six.isA()).isTrue();
    assertThat(six.getB()).isEqualTo(0); // b는 ignore로 제외해서 int면 0, String이면 null
    assertThat(six.getD()).isEqualTo("d값_지정");
    assertThat(six.getE()).isEqualTo("e값_지정");
    assertThat(six.getF()).isEqualTo(6L);
    //assertThat(six.getCreationDate()).isEqualTo(new Date());
}
@Test
void 매핑시_null인_경우_default_지정된_값을_사용할_수_있다(){

    // given
    Five five = new Five(true, 2, "c", null, new Date());

    // when
    Six six = EightMapper.INSTANCE.toSix(null, 6L, five);

    // then
    assertThat(six).isNotNull();
    assertThat(six.isA()).isTrue();
    assertThat(six.getB()).isEqualTo(0);
    assertThat(six.getD()).isEqualTo("default_d"); // null이면 defaultValue
    assertThat(six.getE()).isEqualTo("default_e"); // null이면 defaultValue
    assertThat(six.getF()).isEqualTo(6L);
    //assertThat(six.getCreationDate()).isEqualTo(new Date());

}

5. 별도 메서드를 통해 매핑

  • Nine ➡️ Ten Mapping 하기

Nine

  • animal type 다름 (String)
@Data
@AllArgsConstructor
public class Nine {

    private String name;
    private int age;

    private String animal; // type 다름
}

Ten

  • animal type 다름 (Animal)
@Data
@AllArgsConstructor
public class Ten {

    private String name;
    private int age;

    private Animal animal; // type 다름
}

Animal

public enum Animal {
    DOG,
    CAT,
    PIG
}

🎉 방법1_EnumMapper

  • qualifiedByName : 매핑할 때 이용할 메서드 지정
  • @Named : 매핑에 이용될 메서드 명시
// 타입이_다른_경우_별도의_메서드를_이용해_매핑할_수_있다
@Mapper
public interface EnumMapper {

    EnumMapper INSTANCE = Mappers.getMapper(EnumMapper.class);

    // Nine -> Ten 매핑
    // qualifiedByName : 매핑 시 이용할 메서드 지정
    @Mapping(source = "nine.animal", target = "animal", qualifiedByName = "stringToEnum")
    Ten toTen(Nine nine);

    // 매핑에 이용될 메서드 명시
    @Named("stringToEnum")
    static Animal stringToEnum(String animal) {
        return switch (animal.toUpperCase()) {
            case "CAT" -> Animal.CAT;
            case "DOG" -> Animal.DOG;
            default -> Animal.PIG;
        };
    }
}

🎉 방법2_EnumMapper2

  • default 사용하여 동일하게 사용 가능
  • 변수명은 달라도 되며 타입은 같아야 매핑됨
// 타입이_다른_경우_별도의_메서드를_이용해_매핑할_수_있다
@Mapper
public interface EnumMapper2 {

    EnumMapper2 INSTANCE = Mappers.getMapper(EnumMapper2.class);

    // Nine -> Ten 매핑
    Ten toTen(Nine nine);

    default Animal stringToEnum(String animal) {
        return switch (animal.toUpperCase()) {
            case "CAT" -> Animal.CAT;
            case "DOG" -> Animal.DOG;
            default -> Animal.PIG;
        };
    }
}

테스트 코드로 확인

  • 방법1_qualifiedByName 사용한 메서드
@Test
    void 타입이_다른_경우_별도의_qualifiedByName로_메서드를_이용해_매핑할_수_있다(){

        // given
        Nine nine = new Nine("name", 123, "dog");

        // when
        Ten ten = EnumMapper.INSTANCE.toTen(nine);

        // then
        assertThat(ten).isNotNull();
        assertThat(ten.getName()).isEqualTo("name");
        assertThat(ten.getAge()).isEqualTo(123);
        assertThat(ten.getAnimal()).isEqualTo(Animal.DOG); // string -> Animal Enum 타입
    }
  • 방법2_default 사용한 메서드
@Test
void 타입이_다른_경우_별도의_default_메서드를_이용해_매핑할_수_있다(){

    // given
    Nine nine = new Nine("name", 123, "dog");

    // when
    Ten ten = EnumMapper2.INSTANCE.toTen(nine);

    // then
    assertThat(ten).isNotNull();
    assertThat(ten.getName()).isEqualTo("name");
    assertThat(ten.getAge()).isEqualTo(123);
    assertThat(ten.getAnimal()).isEqualTo(Animal.DOG); // string -> Animal Enum 타입
}

6. 사용자 정의 매퍼 매서드

  • 매핑이 까다로운 경우, 직접 매핑 메서드를 구현 가능
  • default 를 붙여 메서드를 만들어주면 구현 메서드 대신 default로 정의한 메서드 사용 가능
default MessageListServiceDto toMessageListServiceDto(String messageId, Integer count, RequestDto requestDto) {
    String messageType = Optional.ofNullable(requestDto.getType()).orElse("sms").toUpperCase();
    Type msgType = Type.SMS;

    if (messageType.equals("LMS")) {
        msgType = Type.LMS;
    } else if (messageType.equals("MMS")){
        msgType = Type.MMS;
    }

    return MessageListServiceDto.builder()
            .messageId(Optional.ofNullable(messageId).orElse(UUID.randomUUID().toString()))
            .count(Optional.ofNullable(count).orElse(0))
            .title(requestDto.getTitle())
            .content(requestDto.getContent())
            .sender(requestDto.getSender())
            .receiver(Optional.ofNullable(requestDto.getReceiver()).orElse(Collections.EMPTY_LIST))
            .requestTime(LocalDateTime.now())
            .type(msgType)
            .build();
}

7.MapStruct Processor 옵션 및 매핑 정책 등

  • ComponentModel : 매퍼를 빈으로 만들어야 하는 경우
@Mapper(componentModel = "spring")
public interface MessageMapper {
    ...
}
  • unmmappedTargetPolicy : Target 필드는 존재하는데 source 필드가 없는 경우에 대한 정책
    • ERROR : 매핑 대상이 없는 경우, 매핑 코드 생성 시 error 가 발생
    • WARN : 매핑 대상이 없는 경우, 빌드 시 warn 이 발생
    • IGNORE : 매핑 대상이 없는 경우 무시하고 매핑
@Mapper(unmmapedTargetPolicy = ReportingPolicy.{ERROR,WARN,IGNORE})
public interface MessageMapper {
    MessageMapper INSTANCE = Mappers.getMapper(MessageMapper.class);

    MessageBodyDto toMessageBodyDto(RequestDto requestDto);
}
  • nullValueMappingStrategy : source가 null인 경우 제어할 수 있는 정책
    • RETURN_NULL : source가 null 일 경우, target을 null 로 설정
    • RETURN_DEFAULT : source가 null 일 경우, default 값으로 설정
@Mapper(
        nullValueMapMappingStrategy = NullValueMappingStrategy.{RETURN_NULL,RETURN_DEFAULT},
        nullValueIterableMappingStrategy = NullValueMappingStrategy.{RETURN_NULL,RETURN_DEFAULT}
)
public interface MessageMapper {
    MessageMapper INSTANCE = Mappers.getMapper(MessageMapper.class);

    MessageBodyDto toMessageBodyDto(RequestDto requestDto);
}

8. INSTANCE 없이 Mapper 사용하기

➡️ 두 가지 방법 중 편리한 것 선택하여 사용하자

  • 첫번째, entity 또는 dto 클래스에 추가로 메서드 구현하기
@Getter
@Builder
public class WatcherFileInfo {

    private String type;
    private String filePath;

    public FileKeyInfoEntity toEntity() {
        return FileMapper.INSTANCE.toEntity(this);
    }
}
@Mapper
public interface FileMapper {

    FileMapper INSTANCE = Mappers.getMapper(FileMapper.class);

    @Mapping(target = "key", ignore = true)
    FileKeyInfoEntity toEntity(WatcherFileInfo watcherFileInfo);
}

🎉 사용 방법

FileKeyInfoEntity entity = watcherFileInfo.toEntity();

  • 두번째, Mapper를 빈으로 만들기
    • @Mapper(componentModel = "spring") 이 부분이 필요!
@Mapper(componentModel = "spring")
public interface FileMapper {

    @Mapping(target = "key", ignore = true)
    FileKeyInfoEntity toEntity(WatcherFileInfo watcherFileInfo);
}

🎉 사용 방법

fileMapper.toEntity(watcherFileInfo); 이렇게 사용 가능!

@Autowired
private final FileMapper fileMapper;

@Override
public void sendFileInfoToParser(WatcherFileInfo watcherFileInfo) {
    ...
    FileKeyInfoEntity entity = fileMapper.toEntity(watcherFileInfo);
    ...
}

9. 객체 안에 객체도 매핑 가능

  • 어떤 객체끼리 매칭되는지 기입 필요
  • 객체가 다른 경우 default로 메서드 작성 필요
@Mapping(source = "cat.table", target = "notTable")
Dog toDog(Cat cat);

default NotTable transfer(Table table123){
    return new NotTable(table123.getId(), table123.getTitle());
}

마무리

  • MapStruct 사용 시, 구현 메서드를 자동으로 만들기에 편리함
  • 단, 매핑이 원하는대로 동작하는지 확인하기 위해 테스트 코드 작성 필요
  • 반복 작업 줄어들며 에러 발생 확률 감소
  • 간단한 매핑 작업은 MapStruct 사용하고, 복잡한 매핑 작업은 직접 코드를 작성

참고

https://mapstruct.org/documentation/stable/reference/html/#checking-source-property-for-null-arguments

https://blog.naver.com/n_cloudplatform/222957490406

'코딩 > Spring' 카테고리의 다른 글

JPA N+1 문제  (0) 2024.06.24
AOP, 프록시, Transactional  (0) 2024.06.20
Spring Security Config 버전별 정보  (0) 2024.03.03
스프링부트 (JPA) 정리하기 좋은 강의  (0) 2024.03.03
자바 테스트코드 강의 추천!  (0) 2024.02.14