다양한 요구사항이 생길 수 있기 때문에 단순한 매핑만으로 DTO ↔ Entity 변환이 힘들 수 있다. 그럴 때 Mapper 클래스를 Custom 하여 사용해보자.
Mapper 사용방법
대상의 속성 매핑 커스텀하기 with qualifiedByName
qualifedByName 은 대상의 속성을 매핑하기 위한 @Mapping 의 속성으로, 사용자가 정의한 매핑 방법을 @Named 어노테이션으로 이름으로 지정하여 사용할 수 있다. 이를 통해 복잡한 데이터 구조를 변환하거나 기본 매핑만으로 부족한 경우 사용자가 정의한 매핑 방법을 선택할 수 있게 한다.
간단한 예제로 사용자의 생년월일을 나이로 변환하는 작업을 해보자. ( 다른 필드는 모두 동일 )
1. UserMapper 구현
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface UserMapper {
/* (User) LocalDateTime birthDate → (UserDto) int age */
@Mapping(target = "age", source = "birthDate", qualifiedByName = "mapBirthDateToAge")
UserDto toDto(User users);
/* Custom Mapping Method */
@Named("mapBirthDateToAge")
default int mapBirthDateToAge(LocalDateTime birthDate) {
if (birthDate == null) {
return 0;
}
LocalDateTime now = LocalDateTime.now();
return Period.between(birthDate.toLocalDate(), now.toLocalDate()).getYears();
}
}
2. 테스트하기
@ExtendWith({SpringExtension.class, MockitoExtension.class})
class UserMapperTest {
@InjectMocks
private UserMapperImpl userMapperImpl;
@Test
@DisplayName("Entity 내 생년월일로 나이를 계산하여 DTO 에 매핑한다.")
public void mapBirthDateToAgeTest() {
// given
User user = User.builder()
.email("limnj@test.com")
.password("1234")
.birthDate(LocalDateTime.now().minusYears(76))
.build();
// when
UserDto dto = userMapperImpl.toDto(user);
// then
assertEquals(76, dto.getAge());
}
}
3. 실행 결과
매핑 로직 커스텀하기 with @DecoratedWith
@DecoratedWith 를 사용하여 Mapper 가 자동으로 생성하는 로직을 커스텀할 수 있다. Decoratoer 클래스를 만들어 필요에 따라 기본 매퍼의 메서드를 호출하여 확장하는 등의 커스텀이 가능하다.
동작 방식 정리
- delegate : 사용자가 생성한 Decoratoer 클래스는 자동 생성된 매퍼에 대한 참조를 가지며 필요에 따라 원래 매핑 메서드를 호출할 수 있다.
- delegate 메서드를 호출하기 전 후로 추가 로직을 사용하거나 특정 조건에 따라 호출하지 않을 수도 있다.
- 특정 메서드를 Override 하여 수정할 수 있다.
사용예제
간단한 예제로 UserMapperDecorator 를 만들고 UserMapper 의 toDto 를 위임하여 로직을 추가해보자.
요구사항 : UserDto 에 isAdult 필드를 추가하여 성인이면 Y, 아니면 N 로 저장하기
1. UserMapperDecorator 추상 클래스로 생성
@Qualifier : import org.springframework.beans.factory.annotation.Qualifier;
public abstract class UserMapperDecorator implements UserMapper {
@Autowired
@Qualifier("delegate")
private UserMapper delegate;
@Override
public UserDto toDto(User user) {
// 1. 기본 구현 로직 호출
UserDto userDto = delegate.toDto(user);
// 2. 어른 여부 판단 로직 추가 ( 성인이면 Y, 아니면 N )
userDto.setIsAdult(userDto.getAge() >= 20 ? "Y" : "N");
// 3. 결과 반환
return userDto;
}
}
2. UserMapper 에 @DecoratedWith 추가
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
@DecoratedWith(UserMapperDecorator.class)
public interface UserMapper {
..
}
3. 테스트하기
@SpringBootTest
class UserMapperDecoratorTest {
@Autowired
private UserMapper userMapperImpl;
@Test
@DisplayName("나이가 20살 이상이면 Y, 미만이면 N 으로 나타낸다.")
public void mapBirthDateToAgeTest() {
// given
User user1 = User.builder()
.email("limnj@test.com")
.password("1234")
.birthDate(LocalDateTime.now().minusYears(19))
.build();
User user2 = User.builder()
.email("limnj@test.com")
.password("1234")
.birthDate(LocalDateTime.now().minusYears(20))
.build();
// when
UserDto dto1 = userMapperImpl.toDto(user1);
UserDto dto2 = userMapperImpl.toDto(user2);
// then
assertEquals("N", dto1.getIsAdult());
assertEquals("Y", dto2.getIsAdult());
}
}
4. 실행결과
정리
특정 필드에 대한 속성 매핑을 커스텀하고 싶다면 Qualifier 를, 기본 구현 로직을 확장하고 싶다면 @DecoratedWith 를 사용하여 해결할 수 있다. 그렇지만 유지보수성을 위해 불필요한 커스터마이징인지 아닌지에 대한 고민이 필요하다.
참고
https://www.baeldung.com/mapstruct-custom-mapper
https://mapstruct.org/documentation/stable/reference/html/#customizing-mappers-using-decorators
https://mapstruct.org/documentation/stable/api/org/mapstruct/DecoratedWith.html
'Web' 카테고리의 다른 글
[Spring] Logback MDC 로 애플리케이션 로깅하기 (0) | 2024.04.20 |
---|---|
[JPA] deleteAll() 과 deleteAllInBatch() (0) | 2024.03.17 |
[JPA] @ColumnDefault 이해하기 (0) | 2024.01.28 |
[Spring] Soft Delete with JPA (0) | 2024.01.14 |
[Spring] QueryDsl 로 동적 쿼리 짜기 with JPA (0) | 2024.01.07 |