ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Mapstruct를 정확히 알고 사용하기
    카테고리 없음 2022. 10. 16. 15:16

    MapStruct란 ??

    구성 접근 방식에 대한 규약을 기반으로 자바 빈 유형 간의 매핑 구현을 크게 단순화하는 코드 생성기이다.
    생성된 매핑 코드는 일반 메서드 호출을 사용하므로 빠르고, Type-Safe하며, 이해하기 쉽.

     

    사용하는 이유

    다중 계층 애플리케이션은 종종 서로 다른 객체 모델(: 엔티티 및 DTO) 간에 매핑하는데, 이러한 매핑 코드를 작성하는 것은 지루하고 오류가 발생하기 쉬운 작업이다. MapStruct는 이 작업을 최대한 자동화하여 단순화할 수 있도록 도와준다.

     

    사용방법

    MapStructJava 컴파일러에 연결된 어노테이션 프로세서로, 원하는 IDE 내에서뿐만 아니라 명령줄 빌드(Maven, Gradle )에서도 사용할 수 있다.
    MapStruct는 적절한 기본값을 사용하지만 특수 동작을 구성하거나 구현할 때는 방해가 되지않는다.

     

     

    설치하기 

    공식문서는 1.5.3.Final 이 마지막 버전인데, 현재 라이브러리 설치가 되지 않아 1.5.2.Final로 하였다.

     

    Maven

        <properties>
            <maven.compiler.source>8</maven.compiler.source>
            <maven.compiler.target>8</maven.compiler.target>
            <org.mapstruct.version>1.5.2.Final</org.mapstruct.version>
        </properties>
        
    	<dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
            <version>${org.mapstruct.version}</version>
        </dependency>
        
        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.8.1</version>
                    <configuration>
                        <source>1.8</source>
                        <target>1.8</target>
                        <annotationProcessorPaths>
                            <path>
                                <groupId>org.mapstruct</groupId>
                                <artifactId>mapstruct-processor</artifactId>
                                <version>${org.mapstruct.version}</version>
                            </path>
                        </annotationProcessorPaths>
                    </configuration>
                </plugin>
            </plugins>
    	</build>

     

    Gradle

    plugins {
        ...
        id "com.diffplug.eclipse.apt" version "3.26.0" // Only for Eclipse
    }
    
    dependencies {
        ...
        implementation "org.mapstruct:mapstruct:${mapstructVersion}"
        annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
    
        // If you are using mapstruct in test code
        testAnnotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
    }

     

     

    롬복과 같이 쓰는 경우 

    lombok-mapstruct-binding을 같이 사용해야한다. 아니면 Dependency 순서를 

    Lombok -> Mapstruct를 사용해야하는데, 주의하기 어려우므로 위의 의존성을 같이 등록한다.

     

    Maven

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source> <!-- depending on your project -->
                    <target>1.8</target> <!-- depending on your project -->
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                            <version>1.18.24</version>
                        </path>
    
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok-mapstruct-binding</artifactId>
                            <version>0.2.0</version>
                        </path>
    
                        <path>
                            <groupId>org.mapstruct</groupId>
                            <artifactId>mapstruct-processor</artifactId>
                            <version>${org.mapstruct.version}</version>
                        </path>
                        <!-- other annotation processors -->
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
        </plugins>
    </build>

     

    Gradle

    dependencies {
    
        implementation "org.mapstruct:mapstruct:${mapstructVersion}"
        compileOnly "org.projectlombok:lombok:1.18.16"
        annotationProcessor "org.projectlombok:lombok-mapstruct-binding:0.2.0"
        annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
        annotationProcessor "org.projectlombok:lombok:1.18.16"
    }

     

     

    간단한 예제 사용

    Source 구조

     

    Destination 구조

     

     

    전에 학습한 ModelMapper 구조와 동일하다.

     

     

    1. 매퍼생성

    package mapStruct.mapper;
    
    import mapper.Destination;
    import mapper.SourceDepart;
    import org.mapstruct.Mapper;
    import org.mapstruct.Mapping;
    import org.mapstruct.factory.Mappers;
    
    @Mapper
    public interface DestinationMapper {
    
        DestinationMapper DESTINATION_MAPPER = Mappers.getMapper(DestinationMapper.class);
        
        Destination sourceDepartToDestination(SourceDepart sourceDepart);
    }

    기본적인 설정만 하였다.

    @Mapper 으로 해당 인터페이스가 MapStruct를 사용함을 명시하였고,

     

    Mappers.getMapper는 해당 매퍼를 하나의 인스턴스로 호출할 수 있게 해주는 팩토리이다.

     

    @Mapping 만 붙여도 해당 매핑에 대한 작업이 일어나는데, 별개로 source의 필드값과 Target의 필드값이 다른경우 위와

    같이 지정해줄 수 있다. (타입은 동일해야 한다.)

     

    밑에 메서드명은 임의대로 하면 되고 (직관적으로 하는게 가장 좋다.) Return Type이 Target이고 매개변수로 들어가는

    클래스가 Source가 된다.

     

    2. 실행코드 작동

    public static void main(String[] args) {
        SourceDepart sourceDepart = createSourceDepart();
        Destination destination = DestinationMapper.DESTINATION_MAPPER.sourceDepartToDestination(sourceDepart);
        System.out.println("MapStruct 테스트 : " + destination);
        modelMapperTest(sourceDepart);
    }

    위에는 Mapstruct 결과물을 출력하였고, 밑에는 ModelMapper와 비교할 수 있게 같은 테스트를 작동시켰다.

     

    3. 실행코드 결과

     Mapstrcut vs Source

    Mapstruct vs LooseModelMapper

    Mapstruct vs standardModelMapper

     

     

    Mapstruct vs strictModelMapper

     

    살펴보면 ModelMapper의 Strict 전략과 유사한데.

    단지 depart의 클래스가 하나도 매핑이 되지 않아도 기본 생성자값이 들어가게 설정되어있는 것 같다.

     

    4. 구현체 살펴보기

    Mapstruct는 어노테이션프로세서에의해 컴파일 시 구현체를 생성해주기 때문에 인텔리 J나 이클립스에 target폴더를 

    보시면 구현체를 확인할 수 있습니다.

     

     

    살펴보면 다음과 같은 소스가 나온다.

    ackage mapStruct.mapper;
    
    import javax.annotation.Generated;
    import mapper.Depart;
    import mapper.Destination;
    import mapper.LittleDepart;
    import mapper.SourceDepart;
    import mapper.depth1.D1Depart;
    import mapper.depth1.LittleD1Depart;
    import mapper.depth1.depth2.D2Depart;
    import mapper.depth1.depth2.LittleD2Depart;
    
    @Generated(
        value = "org.mapstruct.ap.MappingProcessor",
        date = "2022-10-21T02:25:04+0900",
        comments = "version: 1.5.2.Final, compiler: javac, environment: Java 1.8.0_271 (Oracle Corporation)"
    )
    public class DestinationMapperImpl implements DestinationMapper {
    
        @Override
        public Destination sourceDepartToDestination(SourceDepart sourceDepart) {
            if ( sourceDepart == null ) {
                return null;
            }
    
            Destination destination = new Destination();
    
            destination.setName( sourceDepart.getName() );
            destination.setAge( sourceDepart.getAge() );
            destination.setHpNo( sourceDepart.getHpNo() );
            destination.setTrue( sourceDepart.isTrue() );
            destination.setDepart( departToLittleDepart( sourceDepart.getDepart() ) );
            destination.setD1Depart( d1DepartToLittleD1Depart( sourceDepart.getD1Depart() ) );
    
            return destination;
        }
    
        protected LittleDepart departToLittleDepart(Depart depart) {
            if ( depart == null ) {
                return null;
            }
    
            LittleDepart littleDepart = new LittleDepart();
    
            return littleDepart;
        }
    
        protected LittleD2Depart d2DepartToLittleD2Depart(D2Depart d2Depart) {
            if ( d2Depart == null ) {
                return null;
            }
    
            LittleD2Depart littleD2Depart = new LittleD2Depart();
    
            littleD2Depart.setD2Str( d2Depart.getD2Str() );
            littleD2Depart.setD2Int( d2Depart.getD2Int() );
            littleD2Depart.setD2Time( d2Depart.getD2Time() );
            littleD2Depart.setD2Boolean( d2Depart.getD2Boolean() );
    
            return littleD2Depart;
        }
    
        protected LittleD1Depart d1DepartToLittleD1Depart(D1Depart d1Depart) {
            if ( d1Depart == null ) {
                return null;
            }
    
            LittleD1Depart littleD1Depart = new LittleD1Depart();
    
            littleD1Depart.setD1DateTime( d1Depart.getD1DateTime() );
            littleD1Depart.setD1Boolean( d1Depart.getD1Boolean() );
            littleD1Depart.setD2Depart( d2DepartToLittleD2Depart( d1Depart.getD2Depart() ) );
    
            return littleD1Depart;
        }
    }
    

     

    살펴보면 인터페이스에서 작성한 메소드 외에도 하위 클래스 매핑에필요한 메서드들이 알아서 작성되어있다.

    Target의 객체에 대해서는 항상 새로운 객체를 넣어주는 것을 확인할 수 있고, setter메서드를 사용해서 넣어주는데,

    setter메서드가 없을경우 필드로 접근하여 넣어주게된다.

     

    만약 롬복의 Builder를 사용하여 빌드 패턴을 이용하려해도 사용이 가능하다!!

    Builder를 등록해놓고 구현체를 확인해보면 소스가 다음과 같이 되어있다.

     

    protected LittleD2Depart d2DepartToLittleD2Depart(D2Depart d2Depart) {
        if ( d2Depart == null ) {
            return null;
        }
    
        LittleD2Depart.LittleD2DepartBuilder littleD2Depart = LittleD2Depart.builder();
    
        littleD2Depart.d2Str( d2Depart.getD2Str() );
        littleD2Depart.d2Int( d2Depart.getD2Int() );
        littleD2Depart.d2Time( d2Depart.getD2Time() );
        littleD2Depart.d2Boolean( d2Depart.getD2Boolean() );
    
        return littleD2Depart.build();
    }

     

    5. ModelMapper와의 비교

    1) 속도

    간단히 for문으로 싱글스레드로 접근하여 속도비교를 해보았다. (가장 유사한 strict전략으로)

     

    private static void speedTest(SourceDepart sourceDepart) {
        Destination destination;
        System.out.println("MapStruct 시작 시간 :" + LocalDateTime.now());
        for(int i=0; i<100000; i++) {
            destination = DestinationMapper.DESTINATION_MAPPER.sourceDepartToDestination(sourceDepart);
        }
        System.out.println("MapStruct 종료 시간 :" + LocalDateTime.now());
        System.out.println("==============================================");
        System.out.println("ModelMapper 시작 시간 :" + LocalDateTime.now());
    
        for(int i=0; i<100000; i++) {
            destination = strictModelMapper.map(sourceDepart, Destination.class);
        }
        System.out.println("ModelMapper 종료 시간 :" + LocalDateTime.now());
    
    }

    결과는 

    MapStruct 시작 시간 :2022-10-21T04:21:51.076
    MapStruct 종료 시간 :2022-10-21T04:21:51.100
    ==============================================
    ModelMapper 시작 시간 :2022-10-21T04:21:51.101
    ModelMapper 종료 시간 :2022-10-21T04:21:53.554

    당연하겠지만, MapStruct가 훨씬 빨랐다.

     

    2) 타입불일치에 대한 에러 방지

    필드명은 동일하지만 타입을 다르게 설정하고 진행하면 어떻게 될까??

     

    @Data
    public class D2Depart {
    
        private String d2Str;
        private Integer d2Int;
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
        private LocalDateTime d2Time;
        private Boolean d2Boolean;
    }
    @Getter
    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    public class LittleD2Depart {
    
        private String d2Str;
        private String d2Int;
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
        private LocalDateTime d2Time;
        private Boolean d2Boolean;
    }

     

    d2Int를 int형이아닌 String으로 바꾸고 Main을 실행해보겠다.

    -> 이상없이 들어갔다 "0" 으로 들어가져있었다.

     

    이번엔 d2Str값을 Integer로 바꾸어보았다. 그랬더니

    예외가 발생하였다. 

    이 방식의 문제는 MappingException이 런타임 에러라는 것이다.

    즉, 사용하기 전까지는 해당 매핑이 잘 못되어있는지 알 수가 없다.

     

    위와 같은 상황에서 Mapstruct를 적용해보았다.

    마찬가지로 런타임에서 에러가 났다... 내가 무언가 놓치고 있다는 생각이들어서

    LocalDateTime의 필드를 Integer로 변환해보았다.

     

    다시 ModelMapper :

     

    Mapstruct :

     빌드과정에서 에러가 나는것을 확인 할 수 있었다.

     

    위에서 String -> Integer 가 왜 에러가 나지 않았나 생각해보니 "0" , "1" 과 같은 문자열들이 Integer.parseInt() 같은

    메서드를 통해 숫자로 변환이 가능하기 때문에 해당 처리를 해주게되어있어서였다.

     

    @Mapper(typeConversionPolicy = ReportingPolicy.ERROR)

    을 사용하면 Long -> Integer 와같은 큰 타입에서 작은 타입으로 변환을 유도할 시 컴파일 단계에서 오류를 방지할 수 있다.

     

    6. 조금 더 유용하게 사용하기

    1) 스프링 프레임워크 종속성 주입 사용하기

    스프링 프레임워크를 사용한다면  빈으로 등록하는 것이 좋다.

    @Mapper
    public interface DestinationMapper {
    
        DestinationMapper DESTINATION_MAPPER = Mappers.getMapper(DestinationMapper.class);
        Destination sourceDepartToDestination(SourceDepart sourceDepart);
    }

    대신

     

    @Mapper(componentModel = "spring")
    public interface DestinationMapper {
    
        Destination sourceDepartToDestination(SourceDepart sourceDepart);
    }

    을 사용하거나 

     

    pom.xml에 

                    <configuration>
                        <source>1.8</source> <!-- depending on your project -->
                        <target>1.8</target> <!-- depending on your project -->
                        <annotationProcessorPaths>
                            <path>
                                <groupId>org.projectlombok</groupId>
                                <artifactId>lombok</artifactId>
                                <version>1.18.24</version>
                            </path>
    
                            <path>
                                <groupId>org.projectlombok</groupId>
                                <artifactId>lombok-mapstruct-binding</artifactId>
                                <version>0.2.0</version>
                            </path>
    
                            <path>
                                <groupId>org.mapstruct</groupId>
                                <artifactId>mapstruct-processor</artifactId>
                                <version>${org.mapstruct.version}</version>
                            </path>
                            <!-- other annotation processors -->
                        </annotationProcessorPaths>
                        <compilerArgs>
                            <arg>
                                -Amapstruct.defaultComponentModel=spring
                            </arg>
                        </compilerArgs>
                    </configuration>

    -Amapstruct.defaultComponentModel=spring 를 등록하는 방법이 있다 (전역 설정)

     

    2) 속성 무시하기

    사용법은 간단하다.

    @Mapping(target = "name", ignore = true)
    @Mapping(target = "age", ignore = true)
    @Mapping(target = "hpNo", ignore = true)
    Destination sourceDepartToDestination(SourceDepart sourceDepart);

    와 같이 매핑을 무시할 필드를 target을 써주고 ignore속성을 true로 바꿔주면 된다.

     

     

    3) 필드명이 다른 경우 매핑 연결해주기 ,  기본값 넣어주기, 날짜 형식등

        @Mapping(source = "d1Depart.d1Time", target = "d1Depart.d1DateTime", dateFormat = "yyyy-MM-dd HH:mm:ss", defaultValue = "2022-10-22 13:50:00")
        Destination sourceDepartToDestination(SourceDepart sourceDepart);

    source와 target 필드에 각각 연결 시켜줄 필드명을 넣으면된다.

    dateFormat 은 날짜형식, defaultValue에 기본값을 넣어주면 된다.

     

     

    4) 2개의 객체를 한 개의 객체로 매핑해주기

     

    Mapper 인터페이스에 다음과 같이

    OneObject twoObjectToOneObject(TwoObject1 twoObject1, TwoObject2 twoObject2);

    를 해주면 

     

    구현체에

    @Override
    public OneObject twoObjectToOneObject(TwoObject1 twoObject1, TwoObject2 twoObject2) {
        if ( twoObject1 == null && twoObject2 == null ) {
            return null;
        }
    
        OneObject oneObject = new OneObject();
    
        if ( twoObject1 != null ) {
            oneObject.setName( twoObject1.getName() );
        }
        if ( twoObject2 != null ) {
            oneObject.setAge( twoObject2.getAge() );
        }
    
        return oneObject;
    }

    로 등록된다.

     

    5) 사용자 임의 메소드를 사용하고 싶은경우

        default Destination userMethod(SourceDepart sourceDepart){
            //구현 내용 등록하기 Java8 이상부터 가능
            //MapStruct로 매핑하기 힘든경우 이렇게 임의로 작성할 수 있다.
            return new Destination();
        }

    default 메서드를 선언하고 안에 구현내용을 작성하면 된다.

     

     

    6) 설정 가능한 정책들

    @Mapper 어노테이션이나 @MapperConfig(설정 공유) 

    혹은 Maven이나 Gradle에

                        <compilerArgs>
                            <arg>
                                -Amapstruct.defaultComponentModel=spring
                            </arg>
                        </compilerArgs>

    와 같이 사용하면 된다.

     

    옵션 설명 기본값
    mapstruct. suppressGeneratorTimestamp
    true로 설정하면 생성된 매퍼 클래스의 @Generated 주석에 시간 스탬프가 생성되지 않습니다.
    false
    mapstruct.verbose
    true로 설정된 경우 MapStruct에서 주요 결정을 기록하는 MapStruct. 참고로 메이븐에서 글을 쓰는 순간에 메이븐-컴파일러-플러그인 구성의 문제로 인해 showWarnings를 추가해야 합니다.
    false
    mapstruct. suppressGeneratorVersionInfoComment
    true로 설정하면 생성된 매퍼 클래스의 @Generated 주석에 주석 속성이 생성되지 않습니다. 주석에는 MapStruct의 버전과 주석 처리에 사용되는 컴파일러에 대한 정보가 포함되어 있습니다.
    false
    mapstruct.defaultComponentModel
    생성해야 하는 매퍼를 기준으로 하는 구성 요소 모델의 이름(매퍼 검색 참조).
    지원되는 값은 다음과 같습니다.

    • default: 매퍼는 구성 요소 모델을 사용하지 않으며, 인스턴스는 일반적으로 검색됩니다. via Mappers#getMapper(Class)
    • cdi: 생성된 매퍼는 응용 프로그램 범위 CDI 빈이며 @Inject를 통해 검색할 수 있습니다.
    • spring: 생성된 매퍼는 싱글톤 스코프의 스프링 빈이며 @Autowire를 통해 검색할 수 있습니다.
    • jsr330: 생성된 매퍼는 {@code @Named}로 주석이 달려 있으며 @Inject(javax.inject 또는 jakarta.inject에서, 우선 순위가 있는 javax.inject에서 사용 가능한 매퍼)를 통해 검색할 수 있습니다. 예를 들어 Spring을 사용합니다.
    • jakarta: 생성된 매퍼는 {@code @Named}로 주석이 달려 있으며 @Inject(jakarta.inject에서)를 통해 검색할 수 있습니다(예: Spring 사용).

    @Mapper#componentModel()을 통해 특정 매퍼에 대해 성분 모델이 지정된 경우 주석의 값이 우선됩니다.
    default
    mapstruct.defaultInjectionStrategy
    매개 변수를 통해 매퍼에서 사용하는 주입 유형입니다. 이는 CDI, Spring 및 JSR 330과 같은 주석이 달린 기반 구성 요소 모델에서만 사용됩니다.
    지원되는 값은 다음과 같습니다.

    • field: 필드 주입
    • constructor: 생성자 주입
    CDI 구성 요소Model을 사용하면 기본 생성자도 생성됩니다.
    @Mapper#injectionStrategy()를 통해 특정 매퍼에 대해 주입 전략이 제공된 경우 주석의 값이 옵션보다 우선합니다.
    field
    mapstruct.unmappedTargetPolicy
    매핑 메서드의 대상 개체 속성이 원본 값으로 채워지지 않은 경우 적용되는 기본 보고 정책입니다.
    지원되는 값은 다음과 같습니다.
    • ERROR: 매핑되지 않은 대상 속성이 있으면 매핑 코드 생성이 실패합니다.
    • WARN: 매핑되지 않은 대상 속성은 빌드 시 경고를 발생시킵니다.
    • IGNORE: 매핑되지 않은 대상 속성은 무시됩니다.
    WARN
    mapstruct.unmappedSourcePolicy
    매핑 메서드의 원본 개체 속성이 대상 값으로 채워지지 않은 경우 적용되는 기본 보고 정책입니다.
    지원되는 값은 다음과 같습니다.
    • ERROR: 매핑되지 않은 소스 속성이 있으면 매핑 코드 생성이 실패합니다.
    • WARN: 매핑되지 않은 대상 속성은 빌드 시 경고를 발생시킵니다.
    • IGNORE: 매핑되지 않은 대상 속성은 무시됩니다.

     
    WARN
    mapstruct. disableBuilders
    true로 설정된 경우 매핑을 수행할 때 MapStruct는 빌더 패턴을 사용하지 않습니다. 이는 모든 맵퍼에 대해 @Mapper(빌더 = @Builder(disableBuilder = true)를 수행하는 것과 같습니다.
    false

     

    MapStruct – Java bean mappings, the easy way!

     

     

     

    MapStruct – Java bean mappings, the easy way!

    Java bean mappings, the easy way! Get started Download

    mapstruct.org

    MapStruct 1.5.3.Final Reference Guide 

     

    MapStruct 1.5.3.Final Reference Guide

    If set to true, MapStruct in which MapStruct logs its major decisions. Note, at the moment of writing in Maven, also showWarnings needs to be added due to a problem in the maven-compiler-plugin configuration.

    mapstruct.org

     

Designed by Tistory.