MapStruct 사용해보기
MapStruct는 DTO와 엔티티 간의 변환 작업을 자동화하는 도구이다.
- 중복 코드 제거: DTO와 엔티티 간 변환이 여러 군데에서 필요할 때, 수작업으로 변환 메서드를 작성하는 것보다 자동으로 변환을 처리해주어 코드 중복을 줄일 수 있다.
- 성능 최적화: MapStruct는 컴파일 시점에 변환 코드를 생성하기 때문에 런타임 성능에 큰 영향을 주지 않으며, Reflection을 사용하는 방식보다 훨씬 빠르다.
- 유지보수성 향상: 변환 로직이 변경될 경우, 일관되게 관리되기 때문에 코드의 유지보수가 용이하다.
MapStruct – Java bean mappings, the easy way!
MapStruct Spring Extensions 1.1.3 released March 14, 2025 It is my pleasure to announce the next official release of MapStruct Spring Extensions. What started out as a StackOverflow question turned into its own (sub-)project within the MapStruct organizati
mapstruct.org
MapStruct를 사용하기 위해서 의존성을 먼저 추가해주어야 하는데 주의해야 할 사항은 Lombok을 사용한다면 Lombok 의존성보다 밑에 작성해주어야 한다. 왜냐하면 MapStruct는 Lombok이 만든 getter, setter를 필요로 하기 때문이다.
// Lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// MapStruct
implementation 'org.mapstruct:mapstruct:1.6.3'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.6.3'
이제 엔티티를 DTO로 변환시켜 반환하는 로직을 MapStruct를 적용해보겠습니다.
Entity ▼
public class User{
private UUID id;
private String username;
private String email;
private String phone;
private String password;
private Role role;
}
DTO ▼
// MapStruct 적용 전
public record UserInfoResponseDtoV1(
UUID userId,
String username,
String email,
String phone,
Role role,
Integer mileage,
PageResponseDto<UUID> userCoupons
){
public static UserInfoResponseDtoV1 of(User user) {
return new UserInfoResponseDtoV1(
user.getId(),
user.getUsername(),
user.getEmail(),
user.getPhone(),
user.getRole(),
null,
null
);
}
}
====================================================================
// MapStruct 적용 후
public record UserInfoResponseDtoV2(
UUID userId,
String username,
String email,
String phone,
Role role,
Integer mileage,
PageResponseDto<UUID> userCoupons
){}
Entity -> DTO를 수행하는 Mapper를 생성하게 되면
@Mapper(componentModel = "spring") // Spring 컨테이너에서 빈으로 등록해줌
public interface UserMapper {
@Mapping(target = "mileage", ignore = true) // mileage 필드는 별도로 처리
@Mapping(target = "userCoupons", ignore = true) // userCoupons 필드는 별도로 처리
UserInfoResponseDtoV2 userToUserInfoResponseDto(User user);
}
구현체를 자동으로 생성해주는 것을 볼 수 있다.
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2025-04-25T16:27:36+0900",
comments = "version: 1.6.3, compiler: IncrementalProcessingEnvironment from gradle-language-java-8.13.jar, environment: Java 17.0.14 (Eclipse Adoptium)"
)
@Component
public class UserMapperImpl implements UserMapper {
@Override
public UserInfoResponseDtoV2 userToUserInfoResponseDto(User user) {
if ( user == null ) {
return null;
}
String username = null;
String email = null;
String phone = null;
Role role = null;
username = user.getUsername();
email = user.getEmail();
phone = user.getPhone();
role = user.getRole();
Integer mileage = null;
PageResponseDto<UUID> userCoupons = null;
UUID userId = null;
UserInfoResponseDtoV2 userInfoResponseDtoV2 = new UserInfoResponseDtoV2( userId, username, email, phone, role, mileage, userCoupons );
return userInfoResponseDtoV2;
}
}
이제 기존에 직접 매핑할 때와 MapStruct를 사용할 때의 성능 차이가 있는지 테스트를 진행해보겠습니다.
// MapStruct 적용 전
public UserInfoResponseDtoV1 getUser(UUID userId) {
User user = userRepository.findByIdAndDeletedAtIsNull(userId)
.orElseThrow(()-> new AuthException(ResponseCode.USER_NOT_FOUND));
return UserInfoResponseDtoV1.of(user);
}
====================================================================
// MapStruct 적용 후
public UserInfoResponseDtoV2 getUser(UUID userId) {
User user = userRepository.findByIdAndDeletedAtIsNull(userId)
.orElseThrow(()-> new AuthException(ResponseCode.USER_NOT_FOUND));
UserInfoResponseDtoV2 responseDto = userMapper.userToUserInfoResponseDto(user);
return responseDto;
}
@Test
@DisplayName("유저 단건 조회 V1 vs V2 성능 비교 테스트")
void compareV1AndV2Performance() {
when(userRepository.findByIdAndDeletedAtIsNull(userId)).thenReturn(Optional.of(user));
long startV1 = System.nanoTime();
UserInfoResponseDtoV1 resultV1 = userServiceV1.getUser(userId);
long endV1 = System.nanoTime();
UserInfoResponseDtoV2 mappedDto = new UserInfoResponseDtoV2(
user.getId(), user.getUsername(), user.getEmail(), user.getPhone(), user.getRole(), null, null
);
when(userMapper.userToUserInfoResponseDto(user)).thenReturn(mappedDto);
long startV2 = System.nanoTime();
UserInfoResponseDtoV2 resultV2 = userServiceV2.getUser(userId);
long endV2 = System.nanoTime();
long durationV1 = endV1 - startV1;
long durationV2 = endV2 - startV2;
System.out.println("V1 (직접 매핑) Execution Time: " + durationV1 + "ns");
System.out.println("V2 (MapStruct) Execution Time: " + durationV2 + "ns");
assertNotNull(resultV1);
assertNotNull(resultV2);
}
나노미터 단위로 성능을 테스트한 결과 MapStruct를 사용했을 때가 성능면에서 훌륭한 테스트를 보여주었습니다.