ModelMapper

dongbin_Shin·2021년 8월 31일
8
post-thumbnail

ModelMapper란?

"서로 다른 클래스의 값을 한번에 복사하게 도와주는 라이브러리"
어떤 Object(Source Object)에 있는 필드 값들을 자동으로 원하는 Object(Destination Object)에 Mapping시켜주는 라이브러리이다.
ModelMapper 공식 문서

사용하는 이유

DTO와 같은 클래스로 데이터를 받은 후 원하는 클래스(엔티티)에 넣어줄 때, 보통 Getter와 Setter를 이용해 필드를 하나씩 복사, 붙여넣기 하는 작업을 거친다.
이때 문제가 발생한다.

  1. 매우 귀찮다.
  2. 실수할 가능성이 크다.

input과 output과 repository에 저장되는 data의 모델링이 다른 경우가 많다. 즉 다른 모델의 object를 변환해줘야 하는 작업이 빈번하게 발생한다.
필드가 늘어날수록 개발자의 부담은 커지고 매핑 시 실수할 확률 또한 늘어난다.
이런 단점들을 해결하기 위한 라이브러리이다.

언제 사용할까?

프로젝트에서 view layer에서 사용하는 데이터의 형태는 DTO라는 이름을 붙여 사용한다면, 이는 repository에서 사용하는 entity class형태와 필드 대부분 동일하지만 다른 형태로 구성되어 있다. 결과적으로 get과 같은 요청 시 entity object는 DTO 형태로 매핑하서 controller를 통해 응답하게 되고, 반대로 post 요청 시 controller에서 받은 DTO object를 entity class형태로 변환해야 한다.
==> DTO object와 entity object간의 변환이 잦다.

사용 방법

의존성 추가

MVNRepository

maven

<dependency>
  <groupId>org.modelmapper</groupId>
  <artifactId>modelmapper</artifactId>
  <version>2.3.0</version>
</dependency>

gradle

compile group: 'org.modelmapper', name: 'modelmapper', version: '2.3.0'

Spring bean 등록

@Configuration이 달린 Class에서 @Bean어노테이션을 이용해 Bean으로 등록해준다.
이때 매핑 전략과 Tokenizer를 설정하여 자신이 원하는대로 Custom ModelMapper를 만들 수 있다.

@Configuration
public class UserMapper {
    private final ModelMapper modelMapper = new ModelMapper();
    @Bean
    public ModelMapper userMapper() {
        // 매핑 전략 설정
        modelMapper.getConfiguration()
                .setMatchingStrategy(MatchingStrategies.STRICT);
        modelMapper.createTypeMap(User.class, UserDTO.class)
                .addMapping(User::getName, UserDTO::setUserName);
        return modelMapper;
    }
}

MatchingStrategies

@Configuration
public class CustomModelMapper {

    private final ModelMapper modelMapper = new ModelMapper();

    @Bean
    public ModelMapper strictModelMapper() {
        modelMapper.getConfiguration()
                .setMatchingStrategy(MatchingStrategies.STRICT);
        return modelMapper;
    }

    @Bean
    public ModelMapper standardModelMapper() {
        modelMapper.getConfiguration()
                .setMatchingStrategy(MatchingStrategies.STANDARD);
        return modelMapper;
    }

    @Bean
    public ModelMapper looseModelMapper() {
        modelMapper.getConfiguration()
                .setMatchingStrategy(MatchingStrategies.LOOSE);
        return modelMapper;
    }
}
  • MatchingStrategies.STANDARD(default)
    • source와 destination 속성과 지능적으로 일치 시킴
    • 모든 destination 객체의 property 토큰들은 매칭되어야 한다.
    • 모든 source 객체의 property들은 하나 이상의 토큰이 매칭되어야 한다.
    • 대부분의 상황에 사용 가능
    • ex) appleBanana ==> apple & banana == banana & apple
  • MatchingStrategies.STRICT
    • 가장 엄격한 전략
    • source와 destination의 타입과 필드명이 같을 때만 변환
    • 의도하지 않은 매핑이 일어나는 것을 방지할 때 사용
  • MatchingStrategies.LOOSE
    • 가장 느슨한 전략
    • 계층 구조의 마지막 destination 필드만 일치하도록 하여 source 필드를 destination 필드에 매핑할 수 있음
    • 토큰을 어떤 순서로도 일치 시킬 수 있음
    • 마지막 destination 필드명은 모든 토큰이 일치해야한다.
    • 마지막 source 필드명에는 일치하는 토큰이 하나 이상 있어야 한다.
    • 의도하지 않은 매핑이 될 확률이 높아 잘 사용하지 않는다.

Tokenizer

source 필드명과 destination의 필드명이 다를 때,
예를 들어 source는 camelCase, destination은 under_score 형태일 때 아래와 같이 설정하여 매핑할 수 있다.

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

Mapping 설정

꼭 일치하지 않는 필드명이나 구조가 있더라도 필드들을 매핑시킬 수 있다.
예를 들어 DTO의 userName 필드를 entity의 name필드에 매핑하고 싶은 경우가 이에 해당한다.
TypeMap을 이용해 설정할 수 있다.

TypeMap typeMap = modelMapper.createTypeMap(User.class, UserDTO.class);

source class 타입과 destination class 타입을 param으로 넘겨 createTypeMap 메소드를 호출하면 typeMap을 만들어준다. 이후 typeMap.addMappings()typeMap.addMapping() 메소드로 상세 설정을 한다.

// User -> UserDTO 매핑설정
modelMapper.createTypeMap(User.class, UserDTO.class)
    	.addMapping(User::getName, UserDTO::setUserName) //addMapping
    	.addMapping(User::getEmail, UserDTO::setID)
    	.addMappings(mapping -> {  // 한번에 여러개 매핑.
       		mapping.map(source -> source.getGroupName(), UserDTO::setGroupName);
       		mapping.map(User::getUserNo, UserDTO::setNo);
    })

Converter

또 매핑 시 데이터 가공이 필요하다면 converter를 사용할 수 있다.
enum 타입을 string 타입으로 변환하는 경우, 또는 대문자, 소문자로 변환하는 경우 등이 있다.

아래는 대문자로 변환한 후 매핑하는 코드이다.

typeMap.addMappings(mapping -> mapping.using(ctx -> ((String) 
      				ctx.getSource()).toUpperCase())
  				.map(Person::getName, PersonDTO::setName));

특정 필드 skip

destination 필드 중 매핑하고 싶지 않은 필드가 있을 때 아래와 같이 skip 가능하다.

typeMap.addMappings(mapping -> {
  			mapping.skip(Destination::setField);
});

Null 필드 skip

DTO에서 entity로 변환하고자 할 때, null로 넘어오는 필드를 제외하고 업데이트 하고 싶을 때 사용할 수 있다.

modelMapper.getConfiguration().setSkipNullEnabled(true);

하지만 이 방법은 modelMapper를 사용하는 모든 필드들에 대해 작동하기 때문에 특정 필드만 대상으로 skip하고 싶다면 아래의 Conditional Mapping을 사용해야 한다.

Conditional Mapping

DTO를 통해 entity의 데이터를 변경해야 하는데, 특정 필드가 null일 경우 매핑하고 싶지 않을 때 이 방법을 사용한다.

typeMap.addMappings(mapper -> mapper.when(ctx -> !ObjectUtils.isEmpty(ctx.getSource()))
					.map(UserDTO::getPassword, PersonDTO::setPassword));

Reference

"본 포스트는 작성자가 공부한 내용을 바탕으로 작성한 글입니다.
잘못된 내용이 있을 시 언제든 댓글로 피드백 부탁드리겠습니다.
항상 정확한 내용을 포스팅하도록 노력하겠습니다."

profile
멋있는 백엔드 개발자

0개의 댓글