[JAVA] MapStruct

BBinss·2020년 8월 21일
1
post-thumbnail

개요

DB의 테이블 단위로 매핑되는 java의 객체를 Entity로 정의하고, app 내부이든 외부이든 원하는 형태로 가공하여 사용하기 위해 DTO (Data Transfer Object)를 명확히 구분하여 사용한다.

  1. controller에서 requestBody로 전달 받은 DTO 객체를 가공 후 DB로 입력할 때
  2. DB에서 조회한 데이터를 담은 Entity를 전달하고자 하는 형태로 DTO로 변환 할 때

DTO -> Entity, Entity -> DTO의 객체 매핑은 빈번하고 많은 생성 코드를 차지하게 된다.

MapStruct

간결하고 모던한 객체 변환 매핑을 위한 라이브러리이다.
ModelMapper보다 개인적으로 심플해보이며, lombok과 builder pattern을 같이 사용했을 때도 좋은 코드 품질을 보인다.
https://mapstruct.org

사용법

  • AdoptOpenJDK 11.0.7+10
  • SPRING-BOOT 2
  • lombok 1.18.6
  • mapstruct 1.3.0.final
//  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);
    }
}

설정

  • 별다른 임의 매핑이 필요없다면, 공통 Generic mapper interface를 선언해두고, 상속 받는다면 추가적인 코드 생산이 필요 없다. (마치 JpaRepository<Entity, Type> 처럼)
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);
    }
}

결론

  1. dto 및 entity의 속성 값이 많아 질수록
  2. 동일한 이름의 속성이 상당수 존재하는 경우
  3. 빈번한 객체 매핑이 일어나는 경우

간결하고 좋은 품질의 코드를 유지하는 방법으로 MapStruct가 될 것 같다.

profile
궁빈

0개의 댓글