Spring/초기 setting

mapStruct 소개 및 활용

쿠카이든 2023. 4. 10. 12:56
728x90

https://mapstruct.org/

아래 내용은 https://github.com/beaver84/setting-test 에서 실제 소스를 확인할 수 있습니다.

  • JPA를 쓰다보면 Entity로 값을 가져오는 일이 많은데, 이를 그대로 리턴하는 일은 드물고 DTO를 상황에 맞게 가공을 거쳐서 반환을 하게 된다.
  • 이 때, 단순 필드(컬럼)를 반복 나열하는 일이 많은데, 반복작업을 편리하게 도와주는 라이브러리 중 하나가 mapStruct 이다.

남용하기 보다는 필드가 많은 경우만 사용하는 것을 추천한다.

//문제 상황 - entity에서 DTO로 변환하는데 너무 많은 코드가 필요

private GoodsDTO getGoodsDTO(Goods goods) {
//        return GoodsDTO.builder().id(goods.getId())
//                .dotYn(goods.getDotYn())
//                .moq(goods.getMoq())
//                .otherGoodsCode(goods.getOtherGoodsCode())
//                .imageId(goods.getImageId())
//                .largeCategoryId(goods.getLargeCategoryId())
//                .middleCategoryId(goods.getMiddleCategoryId())
//                .modifier(goods.getModifier())
//                .modifierDate(goods.getModifierDate())
//                .domesticYn(goods.getDomesticYn())
//                .multipleYn(goods.getMultipleYn())
//                .origin(goods.getOrigin())
//                .register(goods.getRegister())
//                .orderLeadTime(goods.getOrderLeadTime())
//                .multiple(goods.getMultiple())
//                .packingQuantity(goods.getPackingQuantity())
//                .registerDate(goods.getRegisterDate())
//                .releaseType(goods.getReleaseType())
//                .useYn(goods.getUseYn())
//                .safetyStockYn(goods.getSafetyStockYn())
//                .unitId(goods.getUnitId())
//                .sellerId(goods.getSellerId())
//                .standard(goods.getStandard())
//                .weekdayClosingTime(goods.getWeekdayClosingTime())
//                .weekendClosingTime(goods.getWeekendClosingTime())
//                .build();
        //위에 코드를 아래의 한줄로 수정 가능(Entity to DTO)
        return goodsMapper.toDto(goods);
    }

 

  • MapStruct 란?
    • MapStructJava bean 유형 간(예 - Entity 와 DTO 사이의 변환)의 매핑 구현을 단순화하는 코드 생성기이다.
    • MapStruct의 특징
      • 컴파일 시점에 코드를 생성하여 런타임에서 안정성을 보장한다(자바 리플렉션을 사용하지 않는다).
      • 다른 매핑 라이브러리보다 속도가 빠르다.
      • 반복되는 객체 매핑에서 발생할 수 있는 오류를 줄일 수 있으며, 구현 코드를 자동으로 만들어주기 때문에 사용이 쉽다.
      • Annotation processor를 이용하여 객체 간 매핑을 자동으로 제공한다.

 

1. 기본 사용방법

MapStruct를 사용하기 위해서는 먼저 dependency (의존성) 추가가 필요하다.

dependencies {
    ...
    implementation 'org.mapstruct:mapstruct:1.5.3.Final'
    annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.3.Final'
    ...
}

 

2. GenericMapper Interface를 만들어준다(최초 1회).

기본적인 메서드들이 포함된 GenericMapper 인터페이스

public interface GenericMapper<D, E> {
    
    D toDto(E entity);
    E toEntity(D dto);
    List<D> toDtos(List<E> entities);
    List<E> toEntities(List<D> dtos);
}
728x90

3. 매핑을 위한 GenericMapper를 구체화한 Mapper 인터페이스를 만든다(예 - GoodsMapper).

GenericMapper 하위에 GoodsMapper 인터페이스 생성

 

4. GenericMapper를 상속한 GoodsMapper 생성

@Mapper(componentModel = "spring")
public interface GoodsMapper extends GenericMapper<GoodsDTO, Goods> {


    @Mapping(source = "name", target = "goodsName")
    GoodsDTO toDto(Goods entity);

    @Mapping(source = "goodsName", target = "name")
    Goods toEntity(GoodsDTO dto);
    
    List<GoodsDTO> toDtos(List<Goods> entities);

    List<Goods> toEntities(List<GoodsDTO> dtos);
}

 

5. @Mapping 어노테이션 활용

@Mapping 어노테이션으로 DTO의 컬럼명이 Entity와 다를 경우 간단히 매핑

  • entity와 dto의 컬럼이름이 다를 경우 @Mapping 어노테이션을 통해 간단히 변환이 가능하다.
  • entity에서는 name 컬럼이고, DTO에서는 goodsName일 경우, 위와 같이 셋팅을 하면 아래와 같이 DTO의 출력 필드가 변환이 된다(DB에서는 name 컬럼에 데이터 저장됨).

save 결과로 goodsName 필드에 값 출력
실제 DB에는 name 필드에 값이 저장됨

 

6. 메서드 앞에 default 접근 제어자를 활용하여 상세 커스터마이징도 가능

  • validation을 하거나 ENUM 값을 커스텀하고 싶을 때는 메서드 앞에 default 접근제어자를 붙이면 가능하다(메뉴얼 참고).
@Mapper(componentModel = "spring")
public interface GoodsMapper extends GenericMapper<GoodsDTO, Goods> {
    
    //YesNo ENUM 타입일 경우 DTO -> Entity로 변환할 때, 값이 어떻게 변화할지 셋팅 예시
    //접근 제어자를 default로 해야 커스터마이징을 인식
    default boolean map(YesNo value) {
        switch (value) {
            case Y:
                return true;
            case N:
            default:
                return false;
        }
    }
    
    //YesNo ENUM 타입일 경우 Entity -> DTO로 변환할 때, 값이 어떻게 변화할지 셋팅 예시(위와 반대)
    //접근 제어자를 default로 해야 커스터마이징을 인식
    default YesNo map(boolean value) {
        return value ? YesNo.Y : YesNo.N;
    }
    
    //default 접근 권한을 설정하여 toDto메서드의 커스터마이징이 가능
    default GoodsDTO toDto(Goods entity){
        GoodsDTO.GoodsDTOBuilder goodsDTO = GoodsDTO.builder();
        goodsDTO.id(entity.getId());
        goodsDTO.sellerId(entity.getSellerId());
        goodsDTO.largeCategoryId(entity.getLargeCategoryId());
        goodsDTO.middleCategoryId(entity.getMiddleCategoryId());
        goodsDTO.unitId(entity.getUnitId());
        goodsDTO.name(entity.getName());
        goodsDTO.otherGoodsCode(entity.getOtherGoodsCode());
        
        // 한 필드의 validation 검증
        if(Objects.isNull(entity.getOrderLeadTime())) {
            goodsDTO.orderLeadTime(0);
        }
        
        // 입력 값을 원하는 다른 값으로 커스터마이징 가능
        goodsDTO.useYn(YesNo.TEST);
        goodsDTO.register(entity.getRegister());
        goodsDTO.modifier(entity.getModifier());
        goodsDTO.registerDate(entity.getRegisterDate());
        goodsDTO.modifierDate(entity.getModifierDate());
        goodsDTO.standard(entity.getStandard());
        goodsDTO.origin(entity.getOrigin());
        goodsDTO.releaseType(entity.getReleaseType());
        goodsDTO.weekdayClosingTime(entity.getWeekdayClosingTime());
        goodsDTO.weekendClosingTime(entity.getWeekendClosingTime());
        goodsDTO.packingQuantity(entity.getPackingQuantity());
        goodsDTO.moq(entity.getMoq());
        goodsDTO.dotYn(entity.getDotYn());
        goodsDTO.safetyStockYn(entity.getSafetyStockYn());
        goodsDTO.imageId(entity.getImageId());
        goodsDTO.multipleYn(entity.getMultipleYn());
        goodsDTO.multiple(entity.getMultiple());
        goodsDTO.domesticYn(entity.getDomesticYn());
        return goodsDTO.build();
    }
//    GoodsDTO toDto(Goods entity);
    
    //List 형식의 Entities를 DTOs로 변환
    List<GoodsDTO> toDtos(List<Goods> entities);

    //DTO를 Entity로 변환
    Goods toEntity(GoodsDTO dto);
    
    //List 형식의 DTOs를 Entities로 변환
    List<Goods> toEntities(List<GoodsDTO> dtos);
}

 

7. 위와 같이 설정하면 설정이 완료되고 빌드 시, 작성한 interface를 바탕으로 클래스 파일이 만들어진다(예 - GoodsMapperImpl.class)

  • 실제로는 이를 기반으로 DTO ↔︎ Entity 간의 변환이 이루어진다.
  • 아래 예시에는 dto 에서는 Y나 N으로 값을 받았지만, 실제로 DB에는 1 또는 0의 값이 들어가게 된다(default 접근제어자로 설정을 하였으므로 - default boolean map(YesNo value))

out 폴더 밑에 generated 된 GoodsMapperImpl.class

 

8. goodsMapper.toEntity 테스트 결과

postman으로 API를 통한 조회가 잘됨을 확인

@Test
    @DisplayName("상품 테이블 조회 테스트")
    void findById() {
        
        GoodsDTO goodsDTO = new GoodsDTO();
        goodsDTO.setName("좋은상품");
        goodsDTO.setWeekdayClosingTime(2000);
        goodsDTO.setSellerId(1);
        goodsDTO.setOrderLeadTime(null);
        goodsDTO.setUseYn(YesNo.Y);
        
        //toEntity로 DTO를 Entity로 한번에 변환
        Goods goodsEntity = goodsMapper.toEntity(goodsDTO);
        goodsRepository.save(goodsEntity);

        
        Goods goodsResult = goodsRepository.findByName("좋은상품");

        
        assertThat(goodsResult.getId()).isGreaterThanOrEqualTo(1);
        assertThat(goodsResult.getName()).isEqualTo("좋은상품");
        assertThat(goodsResult.getWeekdayClosingTime()).isEqualTo(2000);
    }

 

  • 추가 사항 - 혹시 …mapper.class 파일을 찾지 못해서 에러가 발생했을 때, 메뉴 중 빌드에서 프로젝트 다시 빌드를 수행하면 generated Class가 생성되며 정상적으로 프로젝트가 빌드가 된다.

 

 
728x90