Backend/Java

객체간 맵핑을 편리하게 하기 위한 Mapstruct

야뤼송 2023. 12. 13. 11:10
반응형

 

 

1. Mapstuct??

Java에서 데이터 매핑 작업을 쉽고, 빠르게 할 수 있는 라이브러리이다.

 

2. 설정 방법

build.gradle에 dependency를 추가한다.

// mapstruct
implementation("org.mapstruct:mapstruct:1.5.3.Final")
annotationProcessor("org.mapstruct:mapstruct-processor:1.5.3.Final")

 

3. Mapper Interface 만들기

Model 생성 시 유의사항이 있다. 모델은 getter가 있어야 하고 setter 혹은 builder가 있어야한다.

만약 setter와 builder가 같이 있는 경우 builder가 사용된다.

 

Mapstruct에 사용된 Entity와 Dto 객체는 다음과 같다.

@Setter
@Getter
public class CarEntity {

    private int carSeq;
    private String carNum;
    private String carName;
    private String owner;
    private CarTypeDto type;

    private String insId;
    private LocalDateTime insDtm;
    private String modId;
    private LocalDateTime modDtm;

    private LocalDateTime dispStartDt;
    private LocalDateTime dispEndDt;

    private YnPolicy useYn;
    private String local;

}
@Setter
@Getter
public class CarSaveRequestDto {

    private String carNum;
    private String carName;
    private String owner;

    private String color;
    private String company;

    private String userId;

    private String dispStartDt;
    private String dispEndDt;

}
@Setter
@Getter
public class CarSearchResponseDto {

    private int carSeq;
    private String carNum;
    private String carName;
    private String owner;
    private String color;
    private String company;

    private String dispStartDt;
    private String dispEndDt;
    private String dispDt;

    private String useYn;
    private String useYnNm;
    private String local;

}

 

맵핑을 하기위한 Mapstruct interface는 손쉽게 사용 가능하다.

@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE, componentModel = MappingConstants.ComponentModel.SPRING)
public interface CarMapstruct {
	
    // CarSaveRequestDto -> CarEntity로 매핑
    CarEntity toCarDto(CarSaveRequestDto dto);
 
	//CarEntity -> CarSearchResponseDto로 매핑
    CarSearchResponseDto toCarSearchResponseDto(CarEntity dto);
    List<CarSearchResponseDto> toCarSearchResponseDto(List<CarEntity> dto);
 
}

 

 

@Mapper 어노테이션을 붙이면 자동으로 Mapstruct가 CarMapstruct의 구현체를 생성해 주고 기본적으로 필드명이 동일하면 자동으로 변환하여 매핑됩니다.

이렇게 생성된 구현체는 다음과 같습니다.

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2023-12-13T10:57:35+0900",
    comments = "version: 1.5.3.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-7.4.1.jar, environment: Java 17.0.6 (Amazon.com Inc.)"
)
@Component
public class CarMapstructImpl implements CarMapstruct {

    @Override
    public CarEntity toCarDto(CarSaveReqDto dto) {
        if ( dto == null ) {
            return null;
        }

        CarEntity carEntity = new CarEntity();

        carEntity.setCarNum( dto.getCarNum() );
        carEntity.setCarName( dto.getCarName() );
        carEntity.setOwner( dto.getOwner() );
        if ( dto.getDispStartDt() != null ) {
            carEntity.setDispStartDt( LocalDateTime.parse( dto.getDispStartDt() ) );
        }
        if ( dto.getDispEndDt() != null ) {
            carEntity.setDispEndDt( LocalDateTime.parse( dto.getDispEndDt() ) );
        }

        return carEntity;
    }

    @Override
    public CarSearchResDto toCarSearchResponseDto(CarEntity dto) {
        if ( dto == null ) {
            return null;
        }

        CarSearchResDto carSearchResDto = new CarSearchResDto();

        carSearchResDto.setCarSeq( dto.getCarSeq() );
        carSearchResDto.setCarNum( dto.getCarNum() );
        carSearchResDto.setCarName( dto.getCarName() );
        carSearchResDto.setOwner( dto.getOwner() );
        if ( dto.getDispStartDt() != null ) {
            carSearchResDto.setDispStartDt( DateTimeFormatter.ISO_LOCAL_DATE_TIME.format( dto.getDispStartDt() ) );
        }
        if ( dto.getDispEndDt() != null ) {
            carSearchResDto.setDispEndDt( DateTimeFormatter.ISO_LOCAL_DATE_TIME.format( dto.getDispEndDt() ) );
        }
        if ( dto.getUseYn() != null ) {
            carSearchResDto.setUseYn( dto.getUseYn().name() );
        }
        carSearchResDto.setLocal( dto.getLocal() );

        return carSearchResDto;
    }

    @Override
    public List<CarSearchResDto> toCarSearchResponseDto(List<CarEntity> dto) {
        if ( dto == null ) {
            return null;
        }

        List<CarSearchResDto> list = new ArrayList<CarSearchResDto>( dto.size() );
        for ( CarEntity carEntity : dto ) {
            list.add( toCarSearchResponseDto( carEntity ) );
        }

        return list;
    }
}

 

@Mapper안에 사용할 수 있는 속성들은 아래와 같다.

  • unmappedTargetPolicy는 매핑 실패시 정책
  • componentModel : 종속성 주입 시 사용.

 

4. Mapstruct의 다양한 활용법

Mapstructsms 필드명이 다르거나 맵핑을 제외하거나 하는 다양한 편의기능을 제공하고 있다.

여기서 전체기능에 대해 이야기하기 보단 간단하게 몇가지만 다뤄보고자 한다.

  • source : 소스 필드
  • targer : 타켓 필드
  • ignore : 매핑시키지 않기
  • expression : 직접 구현, 간단한 java 코드 사용 가능
  • constant : 상수 값
  • defaultValue : 디폴트값
  • qualifiedByName : 별도 메소드 분리하여 복잡하게 매핑
  • beforeMapping / afterMapping : 매핑 전후로 수행할 로직
  • condition : 맵핑 시 모든 필드들에 대해  공통 함수를 적용할 때  사용 (null 이나 빈 값 체크 시 ) → 전체 필드에 적용되므로, 공통일 경우에만 쓸 것.
@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE, componentModel = MappingConstants.ComponentModel.SPRING)
public interface CarMapstruct {
 
    @Mapping(target = "carSeq", ignore = true)
    @Mapping(target = "type.color", source = "color")
    @Mapping(target = "type.company", source = "company")
    @Mapping(target = "insId", source = "userId", defaultValue = "SYSTEM")
    @Mapping(target = "modId", source = "userId")
    @Mapping(target = "local", constant = "KOR")
    @Mapping(target = "dispStartDt", source = "dispStartDt", qualifiedByName = "setStartTime")
    @Mapping(target = "dispEndDt", source = "dispEndDt", qualifiedByName = "setEndTime")
    CarEntity toCarDto(CarSaveRequestDto dto);
 
    @Mapping(target = "owner", expression = "java(dto.getOwner() + \"님\")")
    @Mapping(target = "color", source = "type.color")
    @Mapping(target = "company", source = "type.company")
    @Mapping(target = "dispStartDt", source = "dispStartDt", dateFormat = "yyyy-MM-dd")
    @Mapping(target = "dispEndDt", source = "dispEndDt", dateFormat = "yyyy-MM-dd HH:mm:ss")
    @Mapping(target = "dispDt", source = ".", qualifiedByName = "setDisplayDate")
    @Mapping(target = "useYnNm", source = "useYn.desc")
    CarSearchResponseDto toCarSearchResponseDto(CarEntity dto);
    List<CarSearchResponseDto> toCarSearchResponseDto(List<CarEntity> dto);
 
 
    @Named("setStartTime")
    default LocalDateTime setStartTime(String date) {
        if (date == null) {
            return null;
        }
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        LocalDate ld = LocalDate.parse(date, formatter);
        return ld.atStartOfDay(); //00:00:00
    }
 
    @Named("setEndTime")
    default LocalDateTime setEndTime(String date) {
        if (date == null) {
            return null;
        }
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        LocalDate ld = LocalDate.parse(date, formatter);
        return ld.atTime(23, 59, 59); //23:59:59
    }
 
    @Named("setDisplayDate")
    default String setDisplayDate(CarEntity dto) {
        if (dto.getDispStartDt() == null || dto.getDispEndDt() == null) {
            return null;
        }
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        return dto.getDispStartDt().format(dtf) + " ~ " + dto.getDispEndDt().format(dtf);
    }
 
    @Mapping(target = "useYn", constant = "Y")
    @Mapping(target = "user.venCd", source = "venCd")
    ItemCateEntity toItemCateEntity(ItemCateSearchDto dto);
 
    ItemCateGetDto toItemCateGetDto(ItemCateEntity dto);
    List<ItemCateGetDto> toItemCateGetDto(List<ItemCateEntity> dto);
 
    @AfterMapping
    default void setSearchValue(@MappingTarget ItemCateEntity target, ItemCateSearchDto source) {
        if (StringUtils.isAnyBlank(source.getSearchKeyword(), source.getSearchType())) {
            return;
        }
        ItemCateSearchType searchType = ItemCateSearchType.valueOf(source.getSearchType().toUpperCase());
        switch (searchType) {
            case CATECD:
                target.setItemCateCd(source.getSearchKeyword());
                break;
            case CATENM:
                target.setItemCateNm(source.getSearchKeyword());
                break;
        }
    }
 
    @Condition
    public static boolean isNotEmpty(String value) {
        return value != null && !value.isEmpty();
    }  
}

 

 

 

반응형