ObjectMapping 라이브러리 비교

나르·2023년 6월 4일
0

JAVA

목록 보기
16/18
post-thumbnail

Entity to DTO, JSON to DTO 등 객체간 매핑 작업이 필요할 때가 있습니다.

필드가 적을 때는 생성자를 통해 일일히 매핑할 수 있지만, 개발이 늘 해피할 수는 없는 법...
30개가 넘게 늘어나는 경우도 부지기수고 직접 매핑하는 과정에서의 휴먼 에러를 무시할 수 없기 때문에 필드를 통한 매핑 라이브러리를 사용합니다.

기본적으로 자주 사용되는 ObjectMapper,ModelMapper, MapStruct 세 가지를 비교해보고, 어떤 것을 사용하는 것이 좋을 지 알아보겠습니다.

import com.fasterxml.jackson.databind.ObjectMapper;
import org.modelmapper.ModelMapper;
import org.mapstruct.factory.Mappers;


class Person {  // Source class 도 동일
    private String name;
    private int age;
}

public void map() {
      // Source 객체 생성
      Source source = new Source("Koiil", 26);

      // ObjectMapper
      ObjectMapper objectMapper = new ObjectMapper();
      Person person = objectMapper.convertValue(source, Person.class);
      
      // ModelMapper
      ModelMapper modelMapper = new ModelMapper();
      Person person = modelMapper.map(source, Person.class);
      
      // MapStruct
      PersonMapper mapper = Mappers.getMapper(PersonMapper.class);
      Person person = mapper.sourceToPerson(source);
}

ObjectMapper

ObjectMapper는 Jackson 라이브러리의 일부로 제공되며, 주로 JSON-Object 변환을 수행하는 데 사용합니다.
spring-boot-starter-web 에 포함되어있기 때문에, 일반적인 spring web application 프로젝트에서는 따로 라이브러리를 추가할 필요가 없습니다.

하지만 개복치처럼 뭐 하나라도 없으면 에러를 내뱉는 라이브러리입니다.
기본생성자와 setter(혹은 getter) 가 무조건 필요하고, 하나라도 없을 시 에러가 발생합니다.

// No @NoArgsConstructor
Cannot construct instance of `...Person` (no Creators, like default constructor, exist): 
cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: UNKNOWN; byte offset: #UNKNOWN]
 
// No @Setter
Unrecognized field "name" (class ...Person), not marked as ignorable (0 known properties: ])
 at [Source: UNKNOWN; byte offset: #UNKNOWN] (through reference chain: ...Person["name"])

ObjectMapper 는 target 객체에 setter 가 아닌 getter 만 있어도 값을 정상적으로 매핑할 수 있는데, 그 이유는 How Jackson ObjectMapper Matches JSON Fields to Java Fields 에서 확인할 수 있습니다.

ModelMapper

ModelMapper는 객체 간의 매핑을 수행하는 데 사용되는 라이브러리로, 필드 이름이 동일하면 자동으로 매핑을 수행합니다.
공식문서 에서 확인할 수 있듯이, 기본 생성자를 필요로 하기 때문에 @NoArgsConstructor 가 없으면 에러를 발생시킵니다.

/** 성공 **/
@Setter
class Person {
    private String name;
    private int age;
}

/** 실패
Failed to instantiate instance of destination ...Person.
Ensure that ...Person has a non-private no-argument constructor.
**/
@Setter
@AllArgsConstructor
class Person {
    private String name;
    private int age;
}

하지만 ObjectMapper 와 다르게, @Setter가 없어도 기본 생성자로 초기화된 객체를 반환하기 때문에 에러가 발생하지 않습니다.

MapStruct

MapStruct는 코드 생성 기반의 매핑 도구로, 인터페이스에 매핑 규칙을 정의하고, @Mapping 어노테이션을 사용하여 복잡한 필드 간의 매핑 규칙을 정의할 수 있습니다.

MapStruct는 는 컴파일 시점에서 최적화된 implememt 된 클래스를 생성하는데, 해당 클래스를 살펴보면 생성자로 사용되는 어노테이션에 따라 매핑 구현이 달라지는 것을 확인할 수 있습니다.

import javax.annotation.processing.Generated;

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2023-06-04T16:47:20+0900",
    comments = "version: 1.5.3.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-7.4.1.jar, environment: Java 16.0.1 (AdoptOpenJDK)"
)
public class PersonMapperImpl implements PersonMapper {

    @Override
    public Person sourceToPerson(Source source) {
        if ( source == null ) {
            return null;
        }
        
        // @Builder 사용시
        Person.PersonBuilder person = Person.builder();

        person.name( source.getName() );
        person.age( source.getAge() );
        
        return person.build();
        
        // @NoArgsConstructor 사용시
        Person person = new Person();

        person.setName( source.getName() );
        person.setAge( source.getAge() );

        return person;
        
        // @AllArgsConstructor 사용시
        Person person = new Person( source.getName(), source.getAge() );

        return person;
        
    }
}

때문에 위 라이브러리들은 @Setter 와 생성자가 필요했지만, MapStruct는 @Setter 없이도 @AllArgsConstructor 나 @Builder 를 사용해 매핑을 할 수 있습니다.


정리

아무것도 없음
(@Setter 없음 only 기본생성자)
아무것도 없음
(public 필드)
@Setter 만 있음
(기본생성자 생성)
@Setter + @AllArgsConstructor
(기본생성자 없음)
@Builder 사용
ObjectMapperErrorSuccessSuccessErrorError
ModelMapperSuccess
(기본 객체 생성)
Success
(기본 객체 생성)
SuccessErrorError
MapStructSuccess
(기본 객체 생성)
SuccessSuccessSuccessSuccess

기본적으로 필드 set 만 가능하다면 동작합니다.(@Setter 혹은 pulic field)

  • ObjectMapper : 기본 제공이므로 단순 JSON 데이터 처리에 유용
  • ModelMapper : 간단한 객체 간의 매핑
  • MapStruct : 개별 mapper 생성해주는게 귀찮으나 복잡한 매핑도 가능

ObjectMapperModelMapper 은 런타임 매핑과정에서 Reflection을 사용하지만,
MapStruct 는 컴파일타임에서 어노테이션을 읽어 최적화된 로직을 생성하기 때문에 Reflection을 사용하지 않아 성능상 이점이 있습니다.

개인적으로 개발할 때 setter 은 지양하는 편이고, builder을 주로 사용하기 때문에 MapStruct 만이 제대로 동작했겠네요..
무심코 어 전엔 됐던거같은데 왜안돼지? 했었는데, 알고보니 MapStruct를 자주 사용하게 되는 이유가 있었습니다.


check 해볼 것

ModelMapper should support Builder Pattern

이슈는 closed이고 MR 도 처리됐는데 일단 3.1.1 버전 기준 여전히 동작안하기는 함...

Jackson의 확장 구조를 파헤쳐 보자
자바 코드 매핑 vs MapStruct vs ModelMapper
Jackson ObjectMapper에서 기본 생성자 없이 Deserialization 하기

profile
💻 + ☕ = </>

0개의 댓글