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
는 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
는 객체 간의 매핑을 수행하는 데 사용되는 라이브러리로, 필드 이름이 동일하면 자동으로 매핑을 수행합니다.
공식문서 에서 확인할 수 있듯이, 기본 생성자를 필요로 하기 때문에 @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는 코드 생성 기반의 매핑 도구로, 인터페이스에 매핑 규칙을 정의하고, @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 사용 | |
---|---|---|---|---|---|
ObjectMapper | Error | Success | Success | Error | Error |
ModelMapper | Success (기본 객체 생성) | Success (기본 객체 생성) | Success | Error | Error |
MapStruct | Success (기본 객체 생성) | Success | Success | Success | Success |
기본적으로 필드 set 만 가능하다면 동작합니다.(@Setter 혹은 pulic field)
ObjectMapper
: 기본 제공이므로 단순 JSON 데이터 처리에 유용ModelMapper
: 간단한 객체 간의 매핑MapStruct
: 개별 mapper 생성해주는게 귀찮으나 복잡한 매핑도 가능ObjectMapper
와 ModelMapper
은 런타임 매핑과정에서 Reflection을 사용하지만,
MapStruct
는 컴파일타임에서 어노테이션을 읽어 최적화된 로직을 생성하기 때문에 Reflection을 사용하지 않아 성능상 이점이 있습니다.
개인적으로 개발할 때 setter 은 지양하는 편이고, builder을 주로 사용하기 때문에 MapStruct 만이 제대로 동작했겠네요..
무심코 어 전엔 됐던거같은데 왜안돼지? 했었는데, 알고보니 MapStruct를 자주 사용하게 되는 이유가 있었습니다.
ModelMapper should support Builder Pattern
이슈는 closed이고 MR 도 처리됐는데 일단 3.1.1 버전 기준 여전히 동작안하기는 함...
Jackson의 확장 구조를 파헤쳐 보자
자바 코드 매핑 vs MapStruct vs ModelMapper
Jackson ObjectMapper에서 기본 생성자 없이 Deserialization 하기