— DTO 매핑을 컴파일 타임에 해결하는 방법

백엔드 개발을 하다 보면 반드시 마주치는 코드가 있다.

 
 
UserDto dto = new UserDto();
dto.setId(user.getId());
dto.setEmail(user.getEmail());
dto.setRole(user.getRole().name());
 

이 작업은 단순하지만 반복적이다.
그리고 실수하기 쉽다.

이 문제를 해결하기 위한 대표적인 도구가 MapStruct다.

MapStruct는 컴파일 타임에 매핑 코드를 자동 생성하는 라이브러리다.

런타임 리플렉션을 사용하는 ModelMapper와 달리,
실제 자바 코드를 생성한다는 점이 핵심이다.


왜 MapStruct를 쓰는가?

1️⃣ 성능

리플렉션 기반이 아니다.
컴파일 시점에 구현체를 생성한다.

즉, 일반 수동 매핑과 동일한 성능.


2️⃣ 타입 안정성

필드가 변경되면 컴파일 에러 발생.
런타임 에러가 아니다.


3️⃣ 가독성

매핑 규칙이 인터페이스에 선언적으로 정의된다.


의존성 추가

Gradle 기준:

 
 
implementation 'org.mapstruct:mapstruct:1.5.5.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
 

Spring Boot 환경에서는:

 
 
annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0'
 

(롬복 사용 시 필요)


기본 사용 예제

Domain

 
 
public class User {
private Long id;
private String email;
private UserRole role;
}
 

DTO

 
 
public class UserResponse {
private Long id;
private String email;
private String role;
}
 

Mapper 정의

@Mapper(componentModel = "spring")
public interface UserMapper {

    @Mapping(target = "role", expression = "java(user.getRole().name())")
    UserResponse toResponse(User user);
}

 

끝이다.

구현체는 MapStruct가 자동 생성한다.


Entity ↔ Domain 매핑 사례 (헥사고날 구조)

헥사고날 아키텍처에서 자주 등장하는 패턴:

  • Domain은 JPA를 모른다.
  • JpaEntity는 인프라 영역에 존재한다.

JpaEntity

 
 
@Entity
public class UserJpaEntity {
private Long id;
private String email;
private String role;
}
 

Mapper

 
 
@Mapper(componentModel = "spring")
public interface UserPersistenceMapper {

@Mapping(target = "role", expression = "java(UserRole.fromString(entity.getRole()))")
User toDomain(UserJpaEntity entity);

@Mapping(target = "role", expression = "java(domain.getRole().name())")
UserJpaEntity toEntity(User domain);
}
 

이 구조를 쓰면:

  • Domain은 JPA에 의존하지 않는다.
  • Entity는 문자열 기반으로 저장한다.
  • 매핑은 Adapter에서 처리된다.

헥사고날 구조와 궁합이 좋다.


자주 사용하는 기능들

1️⃣ 필드명 다를 때

 
 
@Mapping(source = "password", target = "passwordHash")
 

2️⃣ 여러 필드 조합

 
 
@Mapping(target = "fullName", expression = "java(user.getFirstName() + \" \" + user.getLastName())")
 

3️⃣ 기본값 설정

 
 
@Mapping(target = "status", defaultValue = "ACTIVE")
 

4️⃣ 리스트 매핑

 
 
List<UserResponse> toResponseList(List<User> users);
 

자동 처리된다.

LIST

+ Recent posts