개발/Spring Boot

[ModelMapper] 필드 타입이 다른 두 객체간 매핑 커스텀하기

YJ_Lee 2023. 12. 2. 19:28

Introduction

프로젝트를 진행하던 중 타입이 다른 두 객체간에 ModelMapper로 매핑을 할 일이 생겼다.

public class Person {
    ...
    private String images;
}
public class PersonDto {
    ...
    private List<String> images;
}

Entity의 Images는 String 타입으로 여러 이미지 파일 이름을 ;를 구분자로 사용하여 저장하고, Dto는 String 을 Split 하여 리스트로 변환하여 응답해야 하는 상황이다.

즉,

"a;b;c;d" -> ["a", "b", "c", "d"]

위와 같이 매핑하는것이 목표이다.

Solution

ExpressionMap과 Converter를 사용하면 된다. ExpressionMap은 람다 식을 이용하여 필드 이름이 다른 매칭 정의나, 일치하지 않는 타입에 대한 매핑을 정의할 수 있으며, Converter는 단순 매핑이 아닌 커스텀된 데이터의 매핑이 가능해진다.

@Bean
public ModelMapeer modelMapper() {
    ModelMapper modelMapper = new ModelMapper();  

    modelMapper.typeMap(Person.class, PersonDto.class).addMappings(mapper -> {
        mapper.using(imagesConverter())  
            .map(Person::getImages, PersonDto::setImages);  
    });
    return modelMapper;
}

private Converter<String, List<String>> imagesConverter() {  
    return context -> {  
        if (context.getSource() == null || context.getSource().isEmpty()) {  
            return null;  
        }  

        return Arrays.stream(context.getSource().split(";")).toList();  
    }
)
  • typeMap 을 통해 Source Class와, Destination Class 를 지정한다.
  • addMappings() 메서드로 ExpressionMap을 추가한다. - mapper 람다식
  • ExpressionMap에 Converter를 추가한다. - mapper.using()
  • map()으로 필드 매핑을 지정한다.

Converter는 AbstractConverter나 Converter를 구현하면 되는데, 람다식을 이용하면 간편하다.
람다식의 context는 MappingContext로 지정된 타입의 Source, SourceType, Destination, DestinationType 등을 불러올 수 있다.

context의 source 인 String 이 null이나 Empty 상태가 아니라면 ; 구분자로 Split 후 리스트로 변환하여 리턴해 주었다.

주의할 점은 Converter를 추가하면 해당 세팅이 우선시 되기 때문에 ModelMapper의 setSkipNullEnabled 옵션이 true 이더라도 매핑을 시도하게 된다. 그러면 Converter 코드가 실행되다가 NPE 혹은 다른 예외를 발생시키게 된다.

ExpressionMap 의 Skip 또한 마찬가지이다. 따라서 Converter 구현 코드에 반드시 null 체크 혹은 다른 예외 체크를 추가해 주어야 한다.

Test

@Autowired  
private ModelMapper modelMapper;  

@Test  
void customMapperTest() {  
    Person person = new Person();  
    person.setId(1);  
    person.setName("Lee");  
    person.setAge(30);  
    person.setImages("a;b;c;d");  

    PersonDto personDto = modelMapper.map(person, PersonDto.class);  

    System.out.println(person);  
    System.out.println(personDto);  

    assertThat(personDto.getId()).isEqualTo(person.getId());  
    assertThat(personDto.getName()).isEqualTo(person.getName());  
    assertThat(personDto.getImages().size()).isEqualTo(4);  
}  

@Test  
void nullImagesTest() {  
    Person person = new Person();  

    PersonDto personDto = modelMapper.map(person, PersonDto.class);  

    assertThat(personDto.getImages()).isNull();  
}
Person(id=1, age=30, name=Lee, images=a;b;c;d)
PersonDto(id=1, age=30, name=Lee, images=[a, b, c, d])

Link