자바 백엔드 입문 58편. Entity와 DTO 사이 변환을 컴파일 시점 코드 생성으로 자동화하는 MapStruct 표준 패턴을 풀어쓴 학습 노트.
이 글은 자바 백엔드 입문 시리즈 59편 중 58편이에요. 28편 @RestController · 45편 Entity·Repository 에서 "Entity ↔ DTO 변환" 패턴을 다뤘는데 — 그 변환을 자동화하는 MapStruct 를 풀어 가요.
수동 DTO 변환의 함정
Entity → DTO 변환을 손으로 짜면.
public static OrderResponse from(Order entity) {
return new OrderResponse(
entity.getId(),
entity.getAmount(),
entity.getStatus(),
entity.getCreatedAt(),
entity.getUser().getName(),
entity.getUser().getEmail()
// ... 필드 20개면 20줄
);
}
문제:
- 반복 — Entity 100개면 변환 메서드 100개
- 누락 위험 — Entity에 필드 추가 시 DTO 변환 잊기
- 타이핑 실수 — getId() → setId(...) 매칭
해결 = MapStruct. "인터페이스만 정의하면 컴파일 시 변환 코드 자동 생성".
MapStruct 의존성
implementation 'org.mapstruct:mapstruct:1.5.5.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
// Lombok과 함께 쓸 때 필수
annotationProcessor 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0'
annotationProcessor = 컴파일 시점에 코드 자동 생성. 런타임 비용 0.
첫 Mapper 인터페이스
@Mapper(componentModel = "spring")
public interface OrderMapper {
OrderResponse toResponse(Order entity);
Order toEntity(OrderRequest request);
List<OrderResponse> toResponseList(List<Order> entities);
}
인터페이스에 메서드만 정의. @Mapper(componentModel = "spring") = Spring Bean으로 등록.
컴파일하면 — MapStruct가 자동으로 구현 클래스 생성.
// build/generated 에 자동 생성
@Component
public class OrderMapperImpl implements OrderMapper {
@Override
public OrderResponse toResponse(Order entity) {
if (entity == null) return null;
OrderResponse response = new OrderResponse();
response.setId(entity.getId());
response.setAmount(entity.getAmount());
response.setStatus(entity.getStatus());
response.setCreatedAt(entity.getCreatedAt());
return response;
}
// ... 나머지
}
우리가 손으로 안 짜도 — 컴파일 시점에 자동 생성. 런타임 리플렉션 없어서 성능도 빠름.
사용
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepo;
private final OrderMapper orderMapper; // 자동 주입
public OrderResponse findById(Long id) {
Order order = orderRepo.findById(id).orElseThrow();
return orderMapper.toResponse(order); // 변환
}
public List<OrderResponse> list() {
return orderMapper.toResponseList(orderRepo.findAll());
}
}
Spring Bean으로 등록돼 있어 — 그냥 주입받아 메서드 호출.
필드 이름 다를 때 — @Mapping
Entity 필드 이름과 DTO 필드 이름이 다르면 명시.
@Mapper(componentModel = "spring")
public interface OrderMapper {
@Mapping(source = "user.name", target = "userName") // 중첩 필드
@Mapping(source = "user.email", target = "userEmail")
@Mapping(target = "createdAt", dateFormat = "yyyy-MM-dd HH:mm:ss")
OrderResponse toResponse(Order entity);
}
source= Entity의 어느 필드 (점 표기로 중첩 접근)target= DTO의 어느 필드- 날짜 포맷 지정 가능
조건부 매핑·기본값
@Mapper(componentModel = "spring",
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
unmappedTargetPolicy = ReportingPolicy.ERROR) // 누락 필드 컴파일 에러
public interface OrderMapper {
@Mapping(target = "status", defaultValue = "PENDING")
@Mapping(target = "createdBy", ignore = true) // 무시
OrderResponse toResponse(Order entity);
}
unmappedTargetPolicy = ERROR — DTO에 매핑 안 된 필드가 있으면 컴파일 실패. "필드 추가 시 잊기" 위험 차단.
업데이트 매핑 — @MappingTarget
새 객체 만들지 않고 기존 객체 업데이트.
@Mapper(componentModel = "spring")
public interface OrderMapper {
void updateFromRequest(@MappingTarget Order entity, OrderUpdateRequest request);
}
// 사용
Order order = orderRepo.findById(id).get();
orderMapper.updateFromRequest(order, request);
// → order의 필드만 업데이트
51편 변경 감지 와 조합 — JPA의 Dirty Checking이 자동 UPDATE.
표현식 — 복잡한 변환
@Mapping(target = "fullName",
expression = "java(entity.getFirstName() + \" \" + entity.getLastName())")
UserResponse toResponse(User entity);
자바 표현식 박을 수도 있어요. 복잡한 변환은 "별도 메서드 호출" 이 더 깔끔.
다른 Mapper 활용 — uses
여러 Mapper가 협업할 때.
@Mapper(componentModel = "spring", uses = {AddressMapper.class, UserMapper.class})
public interface OrderMapper {
// address 필드는 AddressMapper.toResponse() 자동 호출
// user 필드는 UserMapper.toResponse() 자동 호출
OrderResponse toResponse(Order entity);
}
큰 객체 그래프도 자동 변환.
ModelMapper와 비교 — 왜 MapStruct?
비슷한 도구 ModelMapper 와 비교.
| ModelMapper | MapStruct | |
|---|---|---|
| 동작 | 런타임 리플렉션 | 컴파일 시 코드 생성 |
| 성능 | 느림 | 빠름 (수동 매핑과 동일) |
| 컴파일 검증 | X | O — 누락 필드 컴파일 에러 |
| 디버깅 | 어려움 | 생성 코드 그대로 디버깅 |
| 설정 | 간단 | 어노테이션 학습 필요 |
한국 회사 표준 = MapStruct. 성능·안전성 모두 우수.
Entity·DTO 변환 메서드가 5개 넘기 시작하면 MapStruct 도입. 처음엔 손으로 쓰는 게 더 빨라 보여도 — 시스템이 커지면 자동화의 가치가 압도적. 한국 회사 거의 모든 신규 프로젝트 표준.
한 줄 정리 — MapStruct = Entity↔DTO 매핑 자동 생성. 컴파일 시점에 코드 만들어 성능 빠르고 컴파일 검증 안전. @Mapper(componentModel = "spring") + 인터페이스만 정의 + Spring Bean 자동 주입.
시험 직전 한 번 더 — MapStruct 입문자가 매번 헷갈리는 것
- MapStruct = Entity↔DTO 매핑 자동 코드 생성
- 컴파일 시점에 구현 클래스 생성 — 런타임 비용 0
- 의존성 =
mapstruct+mapstruct-processor(annotationProcessor) - Lombok과 함께 =
lombok-mapstruct-binding추가 필수 @Mapper(componentModel = "spring")= Spring Bean으로 등록- 인터페이스만 정의 — 메서드 시그니처가 매핑 룰
@Mapping(source, target)= 필드 이름 다를 때- 중첩 필드 =
source = "user.name"점 표기 @MappingTarget= 기존 객체 업데이트unmappedTargetPolicy = ERROR= 누락 필드 컴파일 에러defaultValue·ignore·expression같은 세밀 제어uses = {OtherMapper.class}= 다른 Mapper 협업- 생성된 코드 =
build/generated/sources에 있음 (디버깅 가능) - ModelMapper vs MapStruct = MapStruct 압도적 (성능·검증)
- 한국 회사 표준 = MapStruct
- 변환 메서드 5개+ 면 도입 권장
- Spring Bean 자동 주입으로 사용 —
@Autowired private OrderMapper mapper - Stream 매핑도 자동 —
List<Order>→List<OrderResponse> - 컴파일 시 오류 메시지 = MapStruct의 핵심 안전 장치
- IDE 자동완성 + 컴파일 검증으로 "매핑 실수 0"
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 53편 — MockMvc로 컨트롤러 테스트
- 54편 — Testcontainers 실제 DB 통합 테스트
- 55편 — @Scheduled로 작업 스케줄링
- 56편 — @Cacheable 캐싱
- 57편 — Spring Boot Actuator
다음 글: