"서로 다른 클래스의 값을 한번에 복사하게 도와주는 라이브러리"
어떤 Object(Source Object)에 있는 필드 값들을 자동으로 원하는 Object(Destination Object)에 Mapping시켜주는 라이브러리이다.
ModelMapper 공식 문서
DTO와 같은 클래스로 데이터를 받은 후 원하는 클래스(엔티티)에 넣어줄 때, 보통 Getter와 Setter를 이용해 필드를 하나씩 복사, 붙여넣기 하는 작업을 거친다.
이때 문제가 발생한다.
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간의 변환이 잦다.
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'
@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;
}
}
@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;
}
}
source 필드명과 destination의 필드명이 다를 때,
예를 들어 source는 camelCase, destination은 under_score 형태일 때 아래와 같이 설정하여 매핑할 수 있다.
modelMapper.getConfiguration()
.setSourceNameTokenizer(NameTokenizers.CAMEL_CASE)
.setDestinationNameTokenizer(NameTokenizers.UNDERSCORE);
꼭 일치하지 않는 필드명이나 구조가 있더라도 필드들을 매핑시킬 수 있다.
예를 들어 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를 사용할 수 있다.
enum 타입을 string 타입으로 변환하는 경우, 또는 대문자, 소문자로 변환하는 경우 등이 있다.
아래는 대문자로 변환한 후 매핑하는 코드이다.
typeMap.addMappings(mapping -> mapping.using(ctx -> ((String)
ctx.getSource()).toUpperCase())
.map(Person::getName, PersonDTO::setName));
destination 필드 중 매핑하고 싶지 않은 필드가 있을 때 아래와 같이 skip 가능하다.
typeMap.addMappings(mapping -> {
mapping.skip(Destination::setField);
});
DTO에서 entity로 변환하고자 할 때, null로 넘어오는 필드를 제외하고 업데이트 하고 싶을 때 사용할 수 있다.
modelMapper.getConfiguration().setSkipNullEnabled(true);
하지만 이 방법은 modelMapper를 사용하는 모든 필드들에 대해 작동하기 때문에 특정 필드만 대상으로 skip하고 싶다면 아래의 Conditional Mapping을 사용해야 한다.
DTO를 통해 entity의 데이터를 변경해야 하는데, 특정 필드가 null일 경우 매핑하고 싶지 않을 때 이 방법을 사용한다.
typeMap.addMappings(mapper -> mapper.when(ctx -> !ObjectUtils.isEmpty(ctx.getSource()))
.map(UserDTO::getPassword, PersonDTO::setPassword));
Reference
"본 포스트는 작성자가 공부한 내용을 바탕으로 작성한 글입니다.
잘못된 내용이 있을 시 언제든 댓글로 피드백 부탁드리겠습니다.
항상 정확한 내용을 포스팅하도록 노력하겠습니다."