MapStruct & lombok
728x90
반응형
MapStruct & lombok
사용 전에 주의사항을 공유하기 위해 작성하였고 사용법은 부가적으로 작성하였다.
주의사항
- lombok 을 사용할 경우 라이브러리 의존성 설정시 순서가 중요하다
- lombok 과 함께 사용하기 위해 최소 버전은 아래와 같다.
- lombok 1.16.14 or later, MapStruct 1.2.0 or later
- MapStruct 는 getter & setter 를 기반으로 맵핑되기 때문에 getter & setter 는 필수이다.
- 내가 사용한 1.4.2 버전은 빌더 패턴을 사용한 클래스도 사용할 수 있도록 개선되었다.
- setter 가 없는 경우 객체 업데이트에 사용되는
@MappingTarge
사용할 수 없다. - 불변 클래스는 MapStruct 사용이 적합하지 않다.
- 본문 맨 하단에
불변 클래스에 대한 해결 방안을 마련해두었다.
- 본문 맨 하단에
- 활발하게 업데이트를 하고 있어 최신 MapStruct 를 사용하는 것을 권장한다.
- 2021-05-25 에 spring 확장 라이브러리를 출시했다.
장점
- ModelMapper 는 런타임에 리플렉션으로 데이터를 맵핑한다.
- MapStruct 는 컴파일 과정에 데이터 맵핑을 위한 Mapper Class 를 자동으로 생성해준다.
- MapStruct 가 월등히 성능적으로 우수하다.
- 컴파일시 오류를 검출하므로 안정적으로 타입을 맵핑한다.
설치
- Gradle 5.0 or later
dependencies {
implementation "org.mapstruct:mapstruct:1.4.2.Final"
implementation "org.mapstruct.extensions.spring:mapstruct-spring-annotations:0.1.0"
compileOnly "org.projectlombok:lombok:${lombokVersion}"
testCompileOnly "org.projectlombok:lombok:${lombokVersion}"
annotationProcessor "org.mapstruct:mapstruct-processor:1.4.2.Final"
annotationProcessor "org.mapstruct.extensions.spring:mapstruct-spring-annotations:0.1.0"
annotationProcessor "org.projectlombok:lombok:${lombokVersion}"
annotationProcessor "org.projectlombok:lombok-mapstruct-binding:0.2.0"
testAnnotationProcessor "org.mapstruct:mapstruct-processor:1.4.2.Final"
testAnnotationProcessor "org.mapstruct.extensions.spring:mapstruct-spring-annotations:0.1.0"
testAnnotationProcessor "org.projectlombok:lombok:${lombokVersion}"
testAnnotationProcessor "org.projectlombok:lombok-mapstruct-binding:0.2.0"
}
IntelliJ 를 사용할 경우 MapStruct Support Plugin 을 설치한다. 맵핑 설정에 관련하여 힌트를 주거나 자동으로 생성해주는 역할을 지원한다.
개발
Setter 에 의한 Mapper
Model Code
테스트에 사용될 model class 이며 간소화하기 위해 inner class 사용했다.
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Account {
@NoArgsConstructor
@AllArgsConstructor
@Getter @Setter @EqualsAndHashCode @ToString
public static class Dto {
private String username;
private String name;
private UUID uid;
}
@NoArgsConstructor
@AllArgsConstructor
@Getter @Setter @EqualsAndHashCode @ToString
public static class Domain {
private Long id;
private String username;
private String name;
private Boolean disabled;
private Boolean blocked;
private LocalDateTime registeredOn;
private LocalDateTime updatedOn;
private LocalDateTime lastAccessedOn;
private UUID uid;
}
}
Mapper Code
@Mapper
public interface AccountMapper {
AccountMapper INSTANCE = Mappers.getMapper(AccountMapper.class);
@Mapping(target = "updatedOn", ignore = true)
@Mapping(target = "registeredOn", ignore = true)
@Mapping(target = "lastAccessedOn", ignore = true)
@Mapping(target = "id", ignore = true)
@Mapping(target = "disabled", ignore = true)
@Mapping(target = "blocked", ignore = true)
Domain toDomain(Dto source);
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
@Mapping(target = "updatedOn", ignore = true)
@Mapping(target = "registeredOn", ignore = true)
@Mapping(target = "lastAccessedOn", ignore = true)
@Mapping(target = "id", ignore = true)
@Mapping(target = "disabled", ignore = true)
@Mapping(target = "blocked", ignore = true)
@Mapping(target = "uid", ignore = true)
void update(Dto source, @MappingTarget Domain target);
}
위 자바 코드가 빌드되면 아래와 같은 자바 코드가 자동으로 만들어진다.
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2021-09-06T22:45:59+0900",
comments = "version: 1.4.2.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-6.5.jar, environment: Java 11.0.10 (AdoptOpenJDK)"
)
public class AccountMapperImpl implements AccountMapper {
@Override
public Domain toDomain(Dto source) {
if ( source == null ) {
return null;
}
Domain domain = new Domain();
domain.setUsername( source.getUsername() );
domain.setName( source.getName() );
domain.setUid( source.getUid() );
return domain;
}
@Override
public void update(Dto source, Domain target) {
if ( source == null ) {
return;
}
if ( source.getUsername() != null ) {
target.setUsername( source.getUsername() );
}
if ( source.getName() != null ) {
target.setName( source.getName() );
}
}
}
Test Code
public class AccountMapperTest {
private Dto dto;
@BeforeEach
void init() {
dto = new Dto("few", "boat", UUID.randomUUID());
}
@Test
void toDomain() {
Domain domain = AccountMapper.INSTANCE.toDomain(dto);
assertEquals(dto.getName(), domain.getName());
assertEquals(dto.getUsername(), domain.getUsername());
assertEquals(dto.getUid(), domain.getUid());
}
@Test
@DisplayName("dto 의 값을 domain 에 대입하여 업데이트한다. 단 uid 는 업데이트 하지 않는 다.")
void update() {
Domain domain = new Domain();
domain.setId(1L);
domain.setName("account");
domain.setUsername("brave");
domain.setUid(UUID.randomUUID());
AccountMapper.INSTANCE.update(dto, domain);
assertEquals(dto.getName(), domain.getName());
assertEquals(dto.getUsername(), domain.getUsername());
assertEquals(1L, domain.getId());
assertNotEquals(dto.getUid(), domain.getUid());
}
@Test
@DisplayName("update() 테스트와 같지만 dto 에 null 값은 업데이트를 무시한다.")
void null_ignore_and_update() {
Dto aDto = new Dto(null, "boat", UUID.randomUUID());
Domain domain = new Domain();
domain.setId(1L);
domain.setName("account");
domain.setUsername("brave");
domain.setUid(UUID.randomUUID());
AccountMapper.INSTANCE.update(aDto, domain);
assertEquals(aDto.getName(), domain.getName());
assertEquals("brave", domain.getUsername());
assertEquals(1L, domain.getId());
assertNotEquals(aDto.getUid(), domain.getUid());
}
}
불변 클래스에 의한 Mapper
Model Code
테스트에 사용될 model class 이며 간소화하기 위해 inner class 사용했다.
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Sample {
@Value
@Builder
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public static class Dto {
String username;
String name;
UUID uid;
}
@Value
@Builder
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public static class Domain {
Long id;
String username;
String name;
Boolean disabled;
Boolean blocked;
LocalDateTime registeredOn;
LocalDateTime updatedOn;
LocalDateTime lastAccessedOn;
UUID uid;
}
}
Mapper Code
setter 없기때문에 @MappingTarget
을 사용할 수 없다. 그래서 defaultExpression 이용하여 null 인 경우 domain 을 유지하도록 하였다.
@Mapper
public interface SampleMapper {
SampleMapper INSTANCE = Mappers.getMapper(SampleMapper.class);
@Mapping(target = "updatedOn", ignore = true)
@Mapping(target = "registeredOn", ignore = true)
@Mapping(target = "lastAccessedOn", ignore = true)
@Mapping(target = "id", ignore = true)
@Mapping(target = "disabled", ignore = true)
@Mapping(target = "blocked", ignore = true)
@Mapping(source = "uid", target = "uid")
@Mapping(source = "username", target = "username")
@Mapping(source = "name", target = "name")
Domain convert(Dto dto);
@Mapping(source = "domain.updatedOn", target = "updatedOn")
@Mapping(source = "domain.registeredOn", target = "registeredOn")
@Mapping(source = "domain.lastAccessedOn", target = "lastAccessedOn")
@Mapping(source = "domain.id", target = "id")
@Mapping(source = "domain.disabled", target = "disabled")
@Mapping(source = "domain.blocked", target = "blocked")
@Mapping(source = "dto.uid", target = "uid", defaultExpression = "java(domain.getUid())")
@Mapping(source = "dto.username", target = "username", defaultExpression = "java(domain.getUsername())")
@Mapping(source = "dto.name", target = "name", defaultExpression = "java(domain.getName())")
Domain updateDomain(Dto dto, Domain domain);
}
Test Code
@Slf4j
class SampleMapperTest {
@Test
void toDomain() {
Dto dto = Dto.builder()
.username("bundle")
.name("rotten")
.uid(UUID.randomUUID())
.build();
Domain domain = SampleMapper.INSTANCE.convert(dto);
assertNotNull(domain);
assertEquals(dto.getName(), domain.getName());
assertEquals(dto.getUsername(), domain.getUsername());
assertEquals(dto.getUid(), domain.getUid());
assertNull(domain.getId());
}
@Test
void updateDomain() {
Dto dto = Dto.builder()
.username(null)
.name("rotten")
.uid(UUID.randomUUID())
.build();
Domain domain = Domain.builder()
.id(100L)
.name("explode")
.username("fortune")
.disabled(true)
.blocked(true)
.registeredOn(LocalDateTime.now())
.uid(UUID.randomUUID())
.build();
Domain expected = SampleMapper.INSTANCE.updateDomain(dto, domain);
assertNotNull(expected.getUsername());
assertEquals("fortune", expected.getUsername());
assertEquals(100L, expected.getId());
assertEquals(dto.getName(), expected.getName());
assertEquals(dto.getUid(), expected.getUid());
}
}
728x90
반응형
'Tech' 카테고리의 다른 글
Spring RestDocs 작성 가이드 #2 응용편 - 보일러플레이트 코드 제거 (0) | 2021.09.14 |
---|---|
Spring RestDocs 작성 가이드 #1 기본편 (0) | 2021.09.14 |
Spring MVC Test - Response Body 한글 깨짐 이슈 (0) | 2021.09.12 |
도커로 테스트 환경 구성하기 - Docker, Test Containers (0) | 2021.09.12 |