DB의 테이블 단위로 매핑되는 java의 객체를 Entity로 정의하고, app 내부이든 외부이든 원하는 형태로 가공하여 사용하기 위해 DTO (Data Transfer Object)를 명확히 구분하여 사용한다.
DTO -> Entity, Entity -> DTO의 객체 매핑은 빈번하고 많은 생성 코드를 차지하게 된다.
간결하고 모던한 객체 변환 매핑을 위한 라이브러리이다.
ModelMapper보다 개인적으로 심플해보이며, lombok과 builder pattern을 같이 사용했을 때도 좋은 코드 품질을 보인다.
https://mapstruct.org
// gradle
ext {
mapstructVersion = "1.3.0.Final"
lombokVersion = "1.18.6"
}
dependencies {
implementation "org.mapstruct:mapstruct:${mapstructVersion}"
annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
annotationProcessor "org.projectlombok:lombok:${lombokVersion}"
}
mapstruct와 lombok의 선언 순서 중요하며, IDEA에서 인식을 못해 에러가 발생한다
JAVA 소스
// DTO
// @RequestBody로 사용하지 않는다면, @Builder, @Value만 지정을 추천합니다.
@Builder
@Getter
@AllArgsConstructor(access = AccessLevel.PROTECTED)
public class DataDto {
String name;
String email;
String birth;
}
// JPA Entity. JPA Repository가 아니면 new 및 setter 접근 차단.
@Entity
@Table(name = "table_data")
@Builder
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
public class DataEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private final long id = 0;
String name;
String email;
int ageGroup;
}
// Mapper
@Mapper
public interface DataMapper {
DataMapper INSTANCE = Mappers.getMapper(DataMapper.class);
// source: 인자로 받은 dto 객체. target: 리턴 대상인 entity 객체.
// 2번째 인자로 받은 ageGroup을 DataEntity.ageGroup에 매핑.
@Mapping(source = "ageGroup", target = "ageGroup")
DataEntity toDataEntity(DataDto dataDto, int ageGroup);
// dto와 entity의 속성 이름이 동일하며, 별도의 임의의 매핑이 필요없다면 @Mapping 생략 가능.
/* @Mappings({
@Mapping(source = "ageGroup", target = "ageGroup"),
@Mapping(source = "dataDto.name", target = "name"),
})
*/
}
// Service
public class Service {
public DataEntity toDataEntity(DataDto dataDto) {
// static INSTANCE.
return DataMapper.INSTANCE.toDataEntity(dataDto, 1);
}
}
public interface GenericMapper<D, E> {
D toDto(E entity);
E toEntity(D dto);
}
@Mapper
public interface DataMapper extends GenericMapper<DataDto, DataEntity> {
}
기본 builder pattern 코드와 비교해 보자.
기본 builder pattern
public class Service {
public DataEntity toDataEntity(DataDto dataDto) {
// @Valid를 거치겠지만 서비스 입장에선 새로 생성된 dto일 수도 있으며, Optional null 체크를 기본으로 추가하였다.
String name = Optional.ofNullable(dataDto).map(DataDto::getName).orElse("");
String email = Optional.ofNullable(dataDto).map(DataDto::getEmail).orElse("");
String birth = Optional.ofNullable(dataDto).map(DataDto::getBirth).orElse("");
// birth를 통한 계산 로직.
int ageGroup = 1;
return DataEntity.builder()
.name(name)
.email(email)
.ageGroup(ageGroup)
.build();
}
}
// Service
public class Service {
public DataEntity toDataEntity(DataDto dataDto) {
String birth = Optional.ofNullable(dataDto).map(DataDto::getBirth).orElse("");
// birth를 통한 계산 로직.
int ageGroup = 1;
return DataMapper.INSTANCE.toDataEntity(dataDto, 1);
}
}
간결하고 좋은 품질의 코드를 유지하는 방법으로 MapStruct가 될 것 같다.