본문 바로가기
Java, Kotlin, Spring

[Java/Spring] (4) MapStruct - Mapper 세부 설정

by 댕댕미냉 2023. 1. 13.

mapstruct

 

Mapper 세부 설정

이번 포스트는 MapStruct를 설명하는 시리즈의 마지막 게시글입니다. 앞선 게시글들에서는 Mapper를 설정하는 방법과 @Mapping을 활용하는 방법들에 대해서 작성되었으며, 이번 포스트는 Mapper를 작성하는 시간을 줄여줄 수 있는 방법이나 어노테이션들에 대하여 작성될 예정입니다.

 

 

6. 구체적인 매핑 메서드 지정하기

앞선 게시글의 5.5. 에서는 커스텀 메서드를 만드는 방법에 대해서 알아보았다면, 이번에는 매핑 메서드를 지정하는 방식에 대해서 알아보겠습니다. 기본적으로 Mapper는 인자와 리턴값이 일치하는 커스텀 메서드가 있다면 그 메서드를 활용하지만, 어떤 케이스에서는 특정 메서드를 지정하고 싶을 수 있습니다. 이런 상황에서, MapStruct는 @Qualifier를 사용해서 이를 지정할 수 있는데, 아래 2가지 방법을 통해 @Qualifier를 지정할 수 있습니다.

 

  • 어노테이션을 직접 만들어서 @Mapping을 사용할 때 property인 qualifiedBy를 사용합니다.
  • @Named로 이름을 지정하고, @Mapping을 사용할 때 property인 qualifiedByName을 활용한다. 

 

  • qualifiedBy를 활용하는 방법
import org.mapstruct.Qualifier;

@Qualifier
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface TitleTranslator {
}

//

import org.mapstruct.Qualifier;

@Qualifier
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface EnglishToGerman {
}

//

@TitleTranslator
public class Titles {

    @EnglishToGerman
    public String translateTitleEG(String title) {
        // some mapping logic
    }

    @GermanToEnglish
    public String translateTitleGE(String title) {
        // some mapping logic
    }
}

//

@Mapper( uses = Titles.class )
public interface MovieMapper {

     @Mapping( target = "title", qualifiedBy = { TitleTranslator.class, EnglishToGerman.class } )
     GermanRelease toGerman( OriginalRelease movies );

}

 

  • qualifiedByName을 활용하는 방법
@Named("TitleTranslator")
public class Titles {

    @Named("EnglishToGerman")
    public String translateTitleEG(String title) {
        // some mapping logic
    }

    @Named("GermanToEnglish")
    public String translateTitleGE(String title) {
        // some mapping logic
    }
}

//

@Mapper( uses = Titles.class )
public interface MovieMapper {

     @Mapping( target = "title", qualifiedByName = { "TitleTranslator", "EnglishToGerman" } )
     GermanRelease toGerman( OriginalRelease movies );

}

 

7. @BeforeMapping, @AfterMapping

Mapper내부에 있는 모든 매핑 메서드에서 공통적으로 앞 또는 뒤에 적용하고 싶은 로직이 있다면, @BeforeMapping과 @AfterMapping을 활용해서 쉽게 처리할 수 있습니다. 각각의 어노테이션이 Mapper 내부에 존재하는 경우, 각 매핑 메서드의 앞, 뒤에서 해당 메서드가 실행되게 됩니다.

 

@Mapper
public abstract class VehicleMapper {

    @BeforeMapping
    protected void flushEntity(AbstractVehicle vehicle) {
        // I would call my entity manager's flush() method here to make sure my entity
        // is populated with the right @Version before I let it map into the DTO
    }

    @AfterMapping
    protected void fillTank(AbstractVehicle vehicle, @MappingTarget AbstractVehicleDto result) {
        result.fuelUp( new Fuel( vehicle.getTankCapacity(), vehicle.getFuelType() ) );
    }

    public abstract CarDto toCarDto(Car car);
}

// Generates something like this:
public class VehicleMapperImpl extends VehicleMapper {

    public CarDto toCarDto(Car car) {
        flushEntity( car );

        if ( car == null ) {
            return null;
        }

        CarDto carDto = new CarDto();
        // attributes mapping ...

        fillTank( car, carDto );

        return carDto;
    }
}

 

8. Source ↔ Target 반대로 적용하기

Mapper를 활용하다보면, 어떤 메서드의 Source, Target이 다른 메서드에서는 반대의 역할을 하는 경우가 있습니다. 즉 1번 메서드는 A → B 로의 역할을 한다면 다른 메서드는 B → A의 역할을 하는 상황인데, 동일한 형태로 매핑이 이루어진다면 한쪽 매핑은 @InheritInverseConfiguration을 통해 간단하게 처리할 수 있습니다. 

 

 @Mapper
 public interface CarMapper {

 @Mapping( target = "seatCount", source = "numberOfSeats")
 @Mapping( target = "enginePower", source = "engineClass", ignore=true) // NOTE: source specified as well
 CarDto carToDto(Car car);

 @InheritInverseConfiguration
 @Mapping(target = "numberOfSeats", ignore = true)
 // no need to specify a mapping with ignore for "engineClass": specifying source above will assume
 Car carDtoToCar(CarDto carDto);
 }

 

또, @InheritInverseConfiguration 어노테이션을 걸어놓은 쪽에 추가적인 Mapping 어노테이션을 지정하면, 기존에 상속받던 매핑 정보에 추가적으로 매핑 방식을 지정할 수 있습니다.

 

 

9. 공통적인 속성 지정하기

MapStruct를 활용하여 매핑을 진행하다 보면, 여러 부분에서 공통적으로 활용되는 Mapping 또는 설정들이 존재할 수 있습니다. 이러한 상황에서, 번거로움을 줄이기 위해 커스텀 어노테이션을 활용하거나 config용 인터페이스를 따로 만들어 공통적인 속성을 처리할 수 있습니다.

 

9.1. Custom Annotation

여러 매핑에서 공통적으로 변경해야 하는 매핑 방식이 있다면, 다음과 같은 커스텀 어노테이션을 만들어서 활용할 수 있습니다.

 

@Retention(RetentionPolicy.CLASS)
@Mapping(target = "id", ignore = true)
@Mapping(target = "creationDate", expression = "java(new java.util.Date())")
@Mapping(target = "name", source = "groupName")
public @interface ToEntity { }

 

9.2. 공통적인 Config 설정

Mapper에 활용하는 property중 여러 Mapper에서 공통적으로 활용되는 경우가 있다면, @MapperConfig를 통해 이를 하나로 묶어 적용할 수 있습니다. 인터페이스를 만들어 @MapperConfig 안에 공통적인 property들을 적용하고, 이를 사용하는 Mapper에서 config에 해당 클래스를 지정하여 이를 적용할 수 있습니다.

 

@MapperConfig(unmappedSourcePolicy = ReportingPolicy.IGNORE, unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface UnmappedIgnoreConfig {
}

//

@Mapper(componentModel = "spring", uses = {ContactMapper.class, BookingProductConverter.class},
        config = UnmappedIgnoreConfig.class, imports = LocalDateTime.class)
public interface BookingMapper {
    // ...
}}

 

 

10. 마무리하며

지금까지 총 4개의 게시글을 통해 MapStruct를 활용하는 방법들에 대해 정리했습니다. 개인적으로 MapStruct를 활용하면서 많이 활용할 수 있는 부분들을 중심으로 내용을 정리했었는데, 언급된 기능들 이외에도 MapStruct에는 많은 기능들이 있으니 더 자세한 내용들은 공식 문서를 참고해주시면 좋을 것 같습니다. 긴 글 읽어주셔서 감사합니다 :)

 

 

 

2023.01.07 - [Spring] - [Java/Spring] (1) MapStruct를 활용해서 손쉽게 매핑하기

2023.01.08 - [Spring] - [Java/Spring] (2) MapStruct - Mapping 필드 정의하기

2023.01.13 - [Spring] - [Java/Spring] (3) MapStruct - Mapping 필드 정의하기 2

2023.01.13 - [Spring] - [Java/Spring] (4) MapStruct - Mapper 세부 설정