MapStruct
org.mapstruct package 안에 있는 매핑 라이브러리이다.
이 녀석도 아주 강력한 기능을 제공하는데, 어노테이션 기반으로 작성하기에 Spring을 했다면 더 쉬워지는 라이브러리이다.
특히, 리플렉션을 사용하지 않기 때문에 다른 동적 맵핑보다 시간 절약성, 컴파일 시간 안전성, 오류 내용 확인성 등에서 높은 우위를 가진다.
나는 MapStruct를 설정하는 과정이 귀찮기도 하고 lombok 어노테이션과 순서를 맞춰야 한다는 제약 때문에 그냥 사용하기 싫어진다...
설정
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.4.2.Final</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
<org.projectlombok.version>1.18.22</org.projectlombok.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>
위처럼 lombok 프로세싱보다 mapstruct 프로세싱이 먼저 이루어지도록 해야한다. (컴파일 시점에서 매핑하는 동안 순서상에 문제가 있다는 뜻)
장점은 정말 정말 간단하면서도 기능이 강력하다. (내가 다 만들거나, sturct가 다 만들어주거나 둘 중 하나..)
위와 같이 title과 subject는 이름이 다르고, category는 type이 다른 상황으로 레코드를 만들었다.
record class는 java14부터 릴리즈된 키워드로 Setter를 제외한 @Data의 전체 기능을 가진다.
기본 매핑
source class는 field가 private이 아니거나, private인 경우 getter가 필요하다.
target class는 field가 private이 아니거나, setter 혹은 AllArgsConstructor가 필요하다.
객체지향이라는 것을 아주 조금이라도 알고 있다면, field를 public으로 열어놓지 말자
@Mapper
public interface ArticleMapper {
ArticleMapper INSTANCE = Mappers.getMapper(ArticleMapper.class);
ArticleDto toDto(Article article);
Article toEntity(ArticleDto articleDto);
}
위의 코드를 작성하고 Build하면 다음과 같은 코드가 target 폴더에 생성된다.
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2022-06-26T00:32:24+0900",
comments = "version: 1.4.2.Final, compiler: javac, environment: Java 17.0.3 (Azul Systems, Inc.)"
)
public class ArticleMapperImpl implements ArticleMapper {
@Override
public ArticleDto toDto(Article article) {
if ( article == null ) {
return null;
}
int id = 0;
String category = null;
String author = null;
String contents = null;
id = article.id();
if ( article.category() != null ) {
category = article.category().name();
}
author = article.author();
contents = article.contents();
String subject = null;
ArticleDto articleDto = new ArticleDto( id, subject, category, author, contents );
return articleDto;
}
@Override
public Article toEntity(ArticleDto articleDto) {
if ( articleDto == null ) {
return null;
}
int id = 0;
Category category = null;
String author = null;
String contents = null;
id = articleDto.id();
if ( articleDto.category() != null ) {
category = Enum.valueOf( Category.class, articleDto.category() );
}
author = articleDto.author();
contents = articleDto.contents();
String title = null;
String createdDate = null;
Article article = new Article( id, title, category, author, contents, createdDate );
return article;
}
}
Article article = new Article(1, "제목", Category.BLOG, "jangwook", "테스트입니다.", "2022-06-25");
ArticleDto articleDto = ArticleMapper.INSTANCE.toDto(article);
System.out.println(articleDto);
위와 같이 이름이 다른 경우는 mapping이 되지 않는다.
그리고 ENUM은 String으로 치환을 해주는 것 같다. 물론 Integer, Double 로는 안될 것으로 예상된다.
field name이 다른 경우 매핑
다음과 같이 어노테이션으로 매핑될 필드를 설정할 수 있다.
@Mapper
public interface ArticleMapper {
ArticleMapper INSTANCE = Mappers.getMapper(ArticleMapper.class);
@Mapping(source = "title", target = "subject")
ArticleDto toDto(Article article);
@Mapping(source = "subject", target = "title")
Article toEntity(ArticleDto articleDto);
}
다음과 같이 사용할수도 있다.
@Mapper
public interface PersonMapper {
PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);
@Mapping(source = "name.lastName", target = "lastName")
@Mapping(source = "name.firstName", target = "firstName")
@Mapping(source = "age.international", target = "internationalAge")
@Mapping(source = "age.domestic", target = "domesticAge")
PersonDto toDto(Person person);
}
type이 다른 경우 매핑
type이 다른 경우는 따로 어노테이션이 없다. 그래서 default method로 직접 만들면 된다.
@Mapper
public interface ArticleMapper {
ArticleMapper INSTANCE = Mappers.getMapper(ArticleMapper.class);
@Mapping(source = "title", target = "subject")
ArticleDto toDto(Article article);
default Article toEntity(ArticleDto articleDto) {
if ( articleDto == null ) {
return null;
}
Category category = null;
int id = articleDto.id();
String title = articleDto.subject();
if ( articleDto.category() != null ) {
category = Enum.valueOf( Category.class, articleDto.category() );
}
String author = articleDto.author();
String contents = articleDto.contents();
int commentCount = articleDto.comments().size();
String createdDate = null;
return new Article( id, title, category, author, contents, commentCount, createdDate );
}
}
위와 같이 comments의 사이즈를 commentCount에 넣어주고 생성자로 만들어 return하면 된다.
특정 field 무시
@Mapper
public interface ArticleMapper {
ArticleMapper INSTANCE = Mappers.getMapper(ArticleMapper.class);
@Mapping(source = "title", target = "subject")
@Mapping(target = "contents", ignore = true)
ArticleDto toDto(Article article);
}
Article article = new Article(1, "제목", Category.BLOG, "jangwook", "테스트입니다.", 0, "2022-06-25");
ArticleDto articleDto = ArticleMapper.INSTANCE.toDto(article);
System.out.println(articleDto);
null 무시
새로운 객체를 만들어낼 때는 자동으로 null을 무시한다.
이외에 다양한 사용법
interface로 프로토타입으로 만들기만 하면 매핑이되는 마법을 보았다. 심지어 오버라이딩으로 나만의 매핑 메서드를 만들수도 있다.
이뿐만 아니라, 다음의 기능들도 할 수 있는데,
하나의 객체로 합치기
@Mapping(source = "name.lastName", target = "lastName")
@Mapping(source = "name.firstName", target = "firstName")
@Mapping(source = "age.international", target = "internationalAge")
@Mapping(source = "age.domestic", target = "domesticAge")
Info assemble(Name name, Age age);
Name name = new Name("jangwook", "ryu");
Age age = new Age(26, 28);
Info assembleInfo = PersonMapper.INSTANCE.assemble(name, age);
이외에도 매핑 정책, 기존의 객체에 매핑 , 전처리, 후처리 등등 여러가지 많은데, 피곤하기도 하고 모두 설명하면 헷갈릴수도 있기 때문에 여기까지 포스팅한다.
단점으로는 클래스 파일이 많아지는 것, 의존성의 순서에 대한 제약이 있다는 것 외에는 기능적으로 아주 뛰어나다고 생각한다.
Reference
https://coding-start.tistory.com/349
Java - Model(Object) mapping을 위한 Mapstruct (맵스트럭트)!
오늘 다루어볼 내용은 Model mapping을 아주 쉽게 해주는 Mapstruct라는 라이브러리를 다루어볼 것이다. 그 전에 Model mapping에 많이 사용되는 ModelMapper와의 간단한 차이를 이야기해보면, Model Mapper는 Ru..
coding-start.tistory.com
위의 블로그가 Mapstruct의 기능들을 아주 간략히 잘 설명해주신 것 같다.
https://mapstruct.org/documentation/1.4/api/
MapStruct 1.4.2.Final
mapstruct.org
'🧑🏻💻Dev' 카테고리의 다른 글
javax.validation 독립적으로 사용하지 않기 (2) | 2022.11.18 |
---|---|
객체지향 생활 체조 9가지 원칙 (0) | 2022.11.15 |
객체 복사 라이브러리 비교 2편 : ModelMapper (0) | 2022.06.25 |
객체 복사 라이브러리 비교 1편 : BeanUtils (0) | 2022.06.25 |
[Spring] HV000170: No JSR-223 scripting engine could be bootstrapped for language (0) | 2022.06.25 |