ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • ModelMapper 정확히 알고 사용하기
    카테고리 없음 2022. 10. 4. 06:43

    회사에서 디버깅중 ModelMapper에 대한 오류가 떠서 이것이 정확히 가이드라인대로 사용하고 있는지

    파악하고 작동원리를 파헤쳐 보자.

     

    ModelMapper

    • 용도 : 자바에서 한 객체에서 다른 객체로 데이터 값을 넣어줄 때 OO.set 메서드를 사용해서
                각 값을 입력해주어야 하거나, 값 매핑을 해주는 새로운 메서드를 만들어야 하는데.
                이 것은 개발자에게 매우 단순 반복 작업이 될 수 있다.
                이 때 ModelMapper 라이브러리를 활용하면 이 단순반복 작업을 해당 라이브러리가
                자동으로 할 수 있게 해준다.

    • 원리 :  리플렉션을 이용해서 Type비교를 해주고 설정한 매핑되는 방식, 매핑 전략 에 따라서 각종 프로퍼티(속성)
                  접근, 변경 , 무시 등이 일어 날 수 있게  도와준다.



    • 사용하기 : 
      1. 의존성 추가 pom.xml 에 다음과 같이 추가하기
    <dependency>
      <groupId>org.modelmapper</groupId>
      <artifactId>modelmapper</artifactId>
      <version>2.3.9</version>
    </dependency>
    더보기

    만약에 모델 매핑이 잘 안된다면 버전을 한번 바꾸어 보자.(컴파일 오류)

    필자는 3.1.0으로 하였다가 오류가 나서 2.3.9로 바꾸었더니 실행이 잘되었다.

     

           2. ModelMapper 설정하기 (스태틱 변수로 선언)

    설정 설명 기본값
    Ambiguity ignored 둘 이상의 소스 속성과 일치하는 대상 속성을 무시할지 여부를 결정합니다.
    (모호한 속성 무시)
    false
    Access level 접근 레벨 (Public, Protected, Private) public
    Collections merge 원본과 대상의 크기가 다른 동안 대상 항목을 바꾸거나 병합할지 여부를 결정합니다. true
    Field matching 필드값으로 매칭이 가능한지 여부를 결정합니다. disabled
    Naming convention 이름을 기준으로 일치시킬 수 있는 메서드 및 필드 결정합니다. JavaBeans
    Full type matching 조건부 변환기(ContionalConverters) 가 적용되기 위해
    전체 일치를 정의해야 하는지 여부를 결정합니다.
    false
    Implicit matching 암시적 매핑(모델을 지능적으로 매핑)을 사용할지 말지 여부를 결정합니다. true
    Name transformer 토큰화에 앞서 적격 속성 및 클래스 이름을 변환합니다. JavaBeans
    Name tokenizer 일치하기 전에 소스 및 대상 속성 이름을 토큰화합니다. CamelCase
    Matching strategy 소스 토큰과 대상 토큰의 일치 방법을 결정합니다. Standard
    Prefer nested properties 내포된 속성을 암시적으로 매핑해야 하는지 여부를 결정합니다.
    원형 참조가 포함된 모델을 매핑하는 동안 이 옵션을 사용하지 않는 것이 좋습니다.
    true
    Skip null 속성 값이 null일 때 속성을 건너뛸지 여부를 결정합니다. false

         

        private static ModelMapper standardModelMapper = new ModelMapper();
        private static ModelMapper looseModelMapper = new ModelMapper();
        private static ModelMapper strictModelMapper = new ModelMapper();
    
        static {
            standardModelMapper.getConfiguration()
                    .setFieldMatchingEnabled(true)
                    .setFieldAccessLevel(Configuration.AccessLevel.PRIVATE)
                    .setMatchingStrategy(MatchingStrategies.STANDARD);
    
            looseModelMapper.getConfiguration()
                    .setFieldMatchingEnabled(true)
                    .setFieldAccessLevel(Configuration.AccessLevel.PRIVATE)
                    .setMatchingStrategy(MatchingStrategies.LOOSE);
    
            strictModelMapper.getConfiguration()
                    .setFieldMatchingEnabled(true)
                    .setFieldAccessLevel(Configuration.AccessLevel.PRIVATE)
                    .setMatchingStrategy(MatchingStrategies.STRICT);
        }

    매칭전략에 따라 매퍼를 3개를 생성하였다. 또한, 각각 필드 매칭방식을 사용할 수 있게 설정하였고,

    접근레벨을 private로 하였다. 

     

     

    3. 예시 Model 생성

     

    DepartDTO

    public class DepartDTO {
        private String name;
        private int age;
        private LocalDateTime regDt;
        private boolean isYn;
    }

     

    Depart2DTO
    public class Depart2DTO {
        private String address;
        private String hpNo;
        private int price;
        private boolean isTrue;
    }

     

    DestinationDTO

    public class DestinationDTO {
        private String name;
        private int age;
        private String hpNo;
        private boolean isTrue;
    }

     

     

    4. 메인 함수 실행

     

    매칭 전략별 결과 확인

        public static void main(String[] args) {
            DepartDTO departDTO = createDepartDTO();
    
            DestinationDTO destinationDTO = looseModelMapper.map(departDTO, DestinationDTO.class);
            System.out.println("looseModelMapepr 사용  destinationDTO : " + destinationDTO);
    
            destinationDTO = standardModelMapper.map(departDTO, DestinationDTO.class);
            System.out.println("standardModelMapper 사용  destinationDTO : " + destinationDTO);
    
            destinationDTO = strictModelMapper.map(departDTO, DestinationDTO.class);
            System.out.println("strictModelMapper 사용  destinationDTO : " + destinationDTO);
        }

    Loose매칭 전략일경우 isTrue가 true로 바뀐 것을 확인 할 수 있다. 

     

    이것이 왜 그런것인지 확인하기 위해 ModelMapper의 매칭전략을 파악해보자.

     

    매칭전략 토큰 순서 속성 이름 일치 원본 속성
    Loose 상관없음 계층 구조일 때
    마지막 속성이름이 일치
    마지막 소스 속성명이 토큰중 하나라도 일치하여야 한다.
    Standard 상관없음 모든 속성 값이 일치하여야함 모든 소스 속성명이 토큰중
    하나라도 일치하여야 한다.
    Strict 중요함 모든 속성 값이 일치하여야함 모든 소스 속성명이
    모든 토큰과 일치하여야 한다.

     

    이렇게 보아선 정확히 알 수가 없으니, 핵심 부분인 만큼 예제로 접근하여 보자.

     

    Source 모델과 Destination 모델을 다시 설계 해보자

     

    Source 모델

    public class D1DepartDTO {
        private String d1Str;
        private Integer d1Int;
        private LocalDateTime d1DateTime;
        private Boolean d1Boolean;
    }
    
    public class DepartDTO {
        private String name;
        private int age;
        private LocalDateTime regDt;
        private boolean isYn;
    
    }
    
    public class SourceDepart {
    
        private String name;
        private int age;
        private String hpNo;
        private boolean isTrue;
        private DepartDTO departDTO;
        private D1DepartDTO d1DepartDTO;
    }

     

    Destination 모델

    public class LittleD1DepartDTO {
        private LocalDateTime d1DateTime;
        private Boolean d1Boolean;
    }
    
    public class LittleDepartDTO {
        private LocalDateTime dtReg;
        private boolean ynIs;
        private String storeName;
    }
    
    public class DestinationDTO {
    
        private String name;
        private int age;
        private String hpNo;
        private boolean isTrue;
        private LocalDateTime regDt;
        private boolean isYn;
        private LittleDepartDTO departDTO;
        private LittleD1DepartDTO d1DepartDTO;
    }

    자세히 살펴보면 DestinationDTO에 isYn을 regDt 속성을 최상위 계층에 두었고, LittleDepartDTO와 LittleD1DepartDTO는

    각각 속성이 조금씩 있고, LittleDepartDTO는 변수명을 거꾸로, LittleD1DepartDTO는 정석대로 설정을 하였다.

     

    구현 (객체 생성에 대한 내용은 생략 하였습니다.)

        public static void main(String[] args) {
            SourceDepart sourceDepart = createSourceDepart();
            DestinationDTO destinationDTO = new DestinationDTO();
    
            destinationDTO = looseModelMapper.map(sourceDepart, DestinationDTO.class);
            System.out.println("looseModelMapper = " + destinationDTO);
    
            destinationDTO = standardModelMapper.map(sourceDepart, DestinationDTO.class);
            System.out.println("standardModelMapper = " + destinationDTO);
    
            destinationDTO = strictModelMapper.map(sourceDepart, DestinationDTO.class);
            System.out.println("strictModelMapper = " + destinationDTO);
    
        }

     

    결과

     

    1. Loose 전략

    looseModelMapper = DestinationDTO(name=sourceDepart20, 
                                        age=20, 
                                        hpNo=010-2222-5555, 
                                        isTrue=true, 
                                        regDt=2022-10-04T04:24:30.127, 
                                        isYn=true, 
                                        departDTO=LittleDepartDTO(dtReg=2022-10-04T04:24:30.127, 
                                                                ynIs=true
                                                                storeName=depart11), 
                                        d1DepartDTO=LittleD1DepartDTO(d1DateTime=2022-10-04T04:24:30.129,
                                                                      d1Boolean=true))

    Loose전략을 사용할 시에는 해당 계층에 값이 존재하지 않아도, 변수명이 거꾸로되어도, 타입은 같으나 변수명이
    아예 달라도 모든 값을 매핑해준다.

    따라서 이 전략을 사용할 때에는 상대적으로 중요도가 낮고, 계층구조가 소스모델과 상이한 구조를 가질 때 쓰는 것이
    좋을것 같다.

     

    2. Standard전략

     

     

    standardModelMapper = DestinationDTO(name=sourceDepart20, 
                                        	age=20, 
                                            hpNo=010-2222-5555, 
                                            isTrue=true, 
                                            regDt=null, 
                                            isYn=false, 
                                            departDTO=LittleDepartDTO(dtReg=2022-10-04T04:24:30.127, 
                                                                    ynIs=true
                                                                    storeName=null), 
                                            d1DepartDTO=LittleD1DepartDTO(d1DateTime=2022-10-04T04:24:30.129, 
                                                                    d1Boolean=true))

    Standard전략에서는 해당 계층에 존재하는 필드는 매핑되지 않았으나, ynIs나 dtReg같은 이름이 거꾸로 되어있는 

    필드들이 매핑됬다는 것을 확인 할 수 있다. 그리고 source에 존재하지 않는 storeName 필드는 매핑되지 않았다.

    필드명이 거꾸로 작성되어있을 가능성은 적겠지만, 대부분의 경우에 사용할 수 있겠다.

     

    3. Strict 전략

    strictModelMapper = DestinationDTO(name=sourceDepart20, 
                                        age=20, 
                                        hpNo=010-2222-5555, 
                                        isTrue=true, 
                                        regDt=null, 
                                        isYn=false, 
                                        departDTO=null, 
                                        d1DepartDTO=LittleD1DepartDTO(d1DateTime=2022-10-04T04:24:30.129, 
                                                                d1Boolean=true))

    Strict전략에서는 모든것이 엄격하게 관리되어서 필드명, 계층이 일치하지않으면 모두 매핑되지 않았다.

    강하게 규제하여 관리할 때는 이 전략을 사용하는 것이 좋을 것 같다.

     

    만약 원하는 하나의 필드만 매핑시켜주도록 허용하고 싶다면?

    ModelMapper를 TypeMapping하여 이를 커스텀 해보자마차

    DestinationDTO destination = strictModelMapper.createTypeMap(SourceDepart.class, DestinationDTO.class)
            .addMapping(SourceDepart::isTrue, DestinationDTO::setYn)
            .map(sourceDepart);

    간단하게 SourceDepart에 있는 isTrue 필드의 값을 DestinationDTO의 isYn에 매칭시켜준다는 의미이다.

    이렇게 매핑을 지정하면 Strict전략에서도 일치하지 않은 값이어도 매핑을 시켜줄 수 있다.

    주의점은 strictModelMapper의 인스턴스를 공유해서 쓴다면 해당 매핑이 계속 존재하기 때문에 다른곳에

    사용할때는 주의가 필요하다.

     

    마찬가지로 원하는 컬럼을 Skip할 수도 있다.

     

    DestinationDTO destination = strictModelMapper.createTypeMap(SourceDepart.class, DestinationDTO.class)
            .addMapping(SourceDepart::isTrue, DestinationDTO::setYn)
            .addMappings(source -> source.skip(DestinationDTO::setHpNo))
            .map(sourceDepart);

     

    이런 경우 HpNo에 대한 셋팅을 피할 수 있다.

    위의 결과

     

    다음으로, 유효성 검사도 가능하다.

    strictModelMapper.validate();

     

     

    만약 매핑이 안되는 컬럼이 존재하는 경우

    다음과 같은 에러를 띄워줘서 어떠한 값들이 매핑이 안되는지 알려주게 된다. (예외도 발생)

     

    마지막으로 우리가 자바에서의 필드명, 컬럼명칭 규칙이 카멜케이스, 언더스코어 등의 방식이 있는데,

    서로 다른 변수명 방식이어도 매핑을 할 수 있게 설정을 해줄 수 있다.

     

    strictModelMapper.getConfiguration()
                    .setSourceNameTokenizer(NameTokenizers.CAMEL_CASE)
                    .setDestinationNameTokenizer(NameTokenizers.UNDERSCORE);

    위와 같이 소스명은 카멜케이스, 목표모델은 언더스코어형식일때 셋팅을해주어서 외부 API 통신간에도 효율적으로

    매핑을 해줄 수 있다.

     

    다음은 성능상 훨씬 좋은 MapStruct에 대해서 알아보자

Designed by Tistory.