원래 JPA를 이용하여 개발할 때, Entity를 DTO 타입으로 변환하는 과정에서 ModelMapper나 생성자 방식을 사용했다.
새롭게 알게 된 MapStruct라는 것이 ModelMapper보다 간편하고 실행 시간에서 이점이 있다고 하여 공부하게 됐다.
또한, 다른 Mapping Library들 중 속도가 가장 빠르고, compile 중 에러를 확인할 수 있다고 한다.
// 쪽지 읽기
@Override
public GetMessages readMessage(Long id){
Optional<Message> result = repository.findById(id);
Message message = result.orElseThrow(() -> new HttpClientErrorException(HttpStatus.NOT_FOUND));
// 삭제된 메시지일 경우 예외 처리
if (message.isDelFlag()) {
throw new HttpClientErrorException(HttpStatus.NOT_FOUND, "삭제된 메시지입니다.");
}
// entity -> dto 형변환 (필드가 여러 개여서 modelMapper 사용)
ModelMapper modelMapper = new ModelMapper();
// EntitySample를 String으로 변환하는 Converter 정의
Converter<EntitySample, String> toName = new AbstractConverter<EntitySample,String>() {
protected String convert(EntitySample source){
return source.getName();
}
};
// Converter를 ModelMapper에 등록
modelMapper.addConverter(toName);
// Message Entity를 GetMessages DTO로 변환
GetMessages dto = modelMapper.map(message, GetMessages.class);
return dto;
}
-> ModelMapper를 이용한 기존의 코드
MapStruct는 Java Bean Mapping Library로, 주로 DTO(Data Transfer Object) <-> 엔티티 간의 변환 작업을 손쉽게 도와준다. 빈 매핑 코드를 직접 작성하는 것은 반복적이고 시간이 많이 소모되는 작업인데, MapStruct는 이러한 작업을 효율적으로 처리해 준다.
MapStruct는 애노테이션 프로세서를 사용하여 컴파일 시점에 매핑 코드를 생성한다. 이는 런타임시 리플렉션을 사용하지 않아 성능 저하를 최소화하는 장점이 있다.
개발자는 단순히 인터페이스에 애노테이션을 붙여서 매핑 메소드를 정의하면, MapStruct가 구현체를 생성해준다. 이 구현체를 사용하여 객체 간의 변환을 수행할 수 있다. 따라서, 개발자는 매핑 로직에 대해 신경 쓸 필요 없이 비즈니스 로직에만 집중할 수 있다.
MapStruct 사용하기
기본 사용법
1. build.gradle dependency 설정을 해준다.
// MapStruct
annotationProcessor("org.mapstruct:mapstruct-processor:1.5.3.Final")
testAnnotationProcessor("org.mapstruct:mapstruct-processor:1.5.3.Final")
implementation("org.mapstruct:mapstruct:1.5.3.Final")
implementation("org.projectlombok:lombok-mapstruct-binding:0.2.0")
2. Mapper Interface 작성
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface MessageMapper {
// Entity -> DTO
public GetMessages toDTO(Message message);
}
componentModel = "spring" 옵션은 MapStruct가 생성하는 매퍼 구현체에 Spring의 @Component 어노테이션을 추가하도록 지시한다. 이렇게 함으로써, 매퍼는 Spring 빈으로 등록되고, Spring의 의존성 주입(Dependency Injection) 메커니즘을 이용해 사용할 수 있다.
unmappedTargetPolicy = ReportingPolicy.IGNORE 옵션은 매퍼가 대상 클래스에 매핑되지 않은 속성이 있을 때 어떻게 행동할지를 결정한다. ReportingPolicy.IGNORE를 설정하면, MapStruct는 이러한 속성을 무시하고, 경고 또는 에러를 발생시키지 않는다. 이 옵션은 소스 클래스와 대상 클래스가 완벽하게 일치하지 않는 경우 유용할 수 있다.
3. Service 수정
// 쪽지 읽기
@Override
public GetMessages readMessage(Long id) {
Message message = repository.findById(id).orElseThrow(() -> new HttpClientErrorException(HttpStatus.NOT_FOUND));
// 삭제된 메시지일 경우 예외 처리
if(message.isDelFlag()) {
throw new HttpClientErrorException(HttpStatus.NOT_FOUND, "삭제된 메시지입니다.");
}
// entity -> DTO 변환 (MapStruct)
GetMessages dto = MessageMapper.INSTANCE.toDTO(message);
log.info(dto);
return null;
}
위와 같이 코드를 짜고 테스트를 돌렸는데 오류가 발생했다.
EntitySample(Users) 타입의 sender와 receiver를 String 타입으로 변환하는 방법을 MapStruct가 알지 못하고 있어서 발생하는 문제라고 한다. EntitySample에는 name이라는 필드로 되어있는데, Message에는 sender, receiver로 필드명이 달라서 발생하는 문제인 것 같다.
MessageMapper의 메소드에 Mapping 어노테이션을 달아 명시함으로써 해결됐다.
@Mapping(source = "sender.name", target = "sender")
@Mapping(source = "receiver.name", target = "receiver")
public GetMessages toDTO(Message message);
@Mapping 어노테이션은 필드 명이 다르지만 매핑을 해주고 싶다던가, 특정 필드는 항상 상수로 넣어주고 싶을 때 사용된다고 한다.
2023-11-08 17:42:45.905 INFO 9196 --- [ main] c.e.demo.service.MessagesServiceImpl : GetMessages(id=3, text=modify Service Test, sendedDt=2023-11-02T17:26:11.069221, sender=Jo, receiver=Goo, isRead=true)
-> 결과가 잘 출력된 것을 확인할 수 있다.
실행 시간이 얼마나 차이가 있을까 궁금해서 테스트를 해봤다.
위가 ModelMapper를 이용했을 때, 아래가 MapStruct를 이용했을 때인데 MapStruct를 이용했을 때 약 2배가량 빠른 걸 확인할 수 있었다.
정리
1. build.gradle dependency 설정
2. Mapper Interface 작성 -> 필요에 따라 @Mapping 어노테이션 사용
3. Mapper를 이용해 변환
기존에 ModelMapper나 생성자 방식만 사용했었는데, MapStruct 방식을 이용해보니 한눈에 봐도 기존의 코드보다 훨씬 간결해졌고, 인터페이스를 정의해 주는 것만으로 간편하게 타입을 변환해 줄 수 있어서 굉장히 간편했다. 특별한 이슈가 생기지 않는다면 앞으로 ModelMapper보다 MapStruct를 훨씬 자주 사용하게 될 것 같다.
'공부' 카테고리의 다른 글
자바 웹 개발 워크북 (1-1~2) - 자바 웹 개발 환경 만들기 (0) | 2024.02.22 |
---|---|
[JAVA] JWT (JSON Web Token) - Cookie, Session, Token (1) (0) | 2023.11.09 |
[Logging] @RestControllerAdvice를 이용한 에러 핸들링 (0) | 2023.11.05 |
[QueryDSL] Projections.bean과 fields의 차이 (0) | 2023.11.02 |
[API] Kakao Login 구현하기 (1) (0) | 2023.10.20 |