여러 레이어(Controller, Service, Repository)에서 데이터를 교환할 때 사용하는 DTO와 데이터베이스와 매핑되어있는 Entity를 서로 형변환하는 과정을 고민하다보니, 뭔가 효율적으로 매핑 할 수 있는 방법이 없을까 라는 생각을 하게 되었다. 이것저것 찾아보다 보니, MapStruct라는 라이브러리를 찾게 되었다.
Multi-layered applications often require to map between different object models (e.g. entities and DTOs). Writing such mapping code is a tedious and error-prone task. MapStruct aims at simplifying this work by automating it as much as possible.
In contrast to other mapping frameworks MapStruct generates bean mappings at compile-time which ensures a high performance, allows for fast developer feedback and thorough error checking.
MapStruct 라이브러리의 홈페이지에 사용이유를 그대로 가져와보았다. 나름대로 요약해보자면, 종종 일어나는 객체들에 대한 매핑 코드를 작성하는것은 지루하고 오류가 발생하기 쉬우므로, 이를 최대한 자동화하여 단순화하는것을 목표로 한다 인것 같다.
실제로 다른 방법(빌더, setter)들을 사용해도 당장 큰 문제는 없겠지만, 필드가 추가되는등의 작은 수정이 생기게 되더라도 이와 관련된 모든 부분들을 수정해야 된다는 생각에 정신이 아찔하다. 하지만 MapStruct를 사용하게 되면 이런 문제들을 해결할 수 있다고 하니 한번 알아봐야겠다는 생각이 들지 않을 수 없었다.
여러 장점들이 있다고 하는데, Annotation Processor를 사용해서 컴파일 시 매핑코드를 생성한다고 한다.
- Annotation Processor(어노테이션 프로세서)란?
자바 컴파일러의 컴파일 단계에서, 유저가 정의한 어노테이션의 소스코드를 분석하고 처리하기 위해 사용되는 javac에 속한 빌드 툴이라고 한다. 자세한건 다음에 알아보겠다.
이에 따른 장점으로는 이런것들이 있다고 한다.
Maven이나 Gradle에 추가
Lombok과 함께 사용 시 추가 순서에 따라 생성되는 코드가 다르다고 한다. 어노테이션 프로세서를 선언한 순서에 따라 작동하기 때문에, MapStruct를 먼저 선언해주면 getter와 setter가 생성되지 않는 불상사가 일어나게 된다.
DTO, Entity 생성
public class UserDto {
private String id;
private String password;
private String name;
... //Getter, Setter
}
public class UserEntity {
private String id;
private String password;
private String nickname;
private Date createdAt;
... // Getter, Setter
}
변환될 클래스는 setter가 필요하고, 변환대상 클래스는 getter가 필요하다. 변환 될 클래스에 @Builder 어노테이션이 붙어있다면 setter 대신 builder가 우선적으로 사용된다고 한다.
@Mapper어노테이션을 추가한 인터페이스 생성@Mapper(componentModel = "spring") // 1
public interface UserMapper {
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
@Mapping(source = "name", target = "nickname") // 2
UserEntity dtoToEntity(UserDto dto);
@Mapping(source = "nickname", target = "name")
@Mapping(target = "createdAt", ignore = true) // 3
UserDto entityToDto(UserEntity entity);
}
인터페이스에 @Mapper 어노테이션을 사용하게 되면, MapStruct가 자동으로 UserMapper를 상속받아서 UserMapperImpl을 구현해준다고 한다.
componentModel = "spring"
Spring에서 사용 시, Impl은 스프링의 싱글톤 빈으로 관리된다.
@Mapping(source = "name", target = "nickname")
변환될 클래스와 변환 대상 클래스의 필드 이름이 다르다면 다음과 같이 직접 지정해 줄 수 있다.
@Mapping(target = "createdAt", ignore = true)
매핑 시 타겟 객체에 매핑되지 않는 필드가 있다면 무시해줄수도 있다. 매핑 정책에 따라서 사용하면 된다.
이것들 이외에도 많은 기능들이 있으니, 필요하면 찾아서 사용하면 될듯하다.
UserEntity userEntity = UserMapper.INSTANCE.dtoToEntity(userDto);
UserDto userDto = UserMapper.INSTANCE.entityToDto(userEntity);
위의 예시처럼 엔티티를 변경하는 기능말고도 다른 여러가지 기능들이 있다고 한다. 객체를 합칠 수도 있고, 직접 다른 타입의 객체로 매핑하도록 직접 구현할수도 있고, 여러 정책들을 설정해줄수도 있다고 한다. 필요한 부분에 대해서 추가적으로 검색하며 사용할 수 있으면 좋겠다.