MapStruct 간단정리
Spring Boot에서 MapStruct로 DTO↔Entity 매핑 자동화하기
컴파일 타임 코드 생성 방식, ModelMapper와의 차이, 그리고 실무에서 자주 터지는 케이스들을 경험 흐름으로 정리합니다.
1. 서론: DTO 변환, 왜 이렇게 귀찮을까요?
Entity를 DTO로 바꾸는 코드는 보통 아래처럼 반복됩니다.
dto.setX(entity.getX())를 끝없이 작성합니다.- 필드가 추가되면 누락이 생기고, 이게 버그로 이어집니다.
- PATCH에서는
null처리 정책이 없으면 데이터가 의도치 않게 삭제될 수 있습니다.
그래서 많은 분들이 ModelMapper 같은 런타임 리플렉션 기반 매퍼를 떠올리지만, 변환이 많아질수록 성능/디버깅 측면에서 고민이 생기곤 합니다.
2. MapStruct란 무엇인가요? (ELI5)
MapStruct는 한마디로 “통역사”에 가깝습니다.
- 한국어(Entity)를 영어(DTO)로 번역할 때
- 매번 사전(리플렉션)을 뒤져서 단어를 찾는 대신
- 미리 번역된 문장(매핑 구현 코드)을 자동으로 만들어 두는 도구라고 이해하시면 됩니다.
즉, MapStruct는 컴파일 타임(빌드 시점) 에 매핑 코드를 생성하는 Annotation Processor입니다.
3. 동작 원리: 컴파일 타임 코드 생성
MapStruct를 이해하는 핵심은 “런타임에 리플렉션으로 매핑하지 않고, 빌드 시점에 코드가 생성된다”는 점입니다.
MapStruct vs ModelMapper 비교
정리하면 다음과 같습니다.
- MapStruct: 컴파일 타임 생성 → 런타임에는 “그냥 메서드 호출”에 가깝습니다.
- 장점: 타입 세이프(컴파일 에러로 누락 감지), 디버깅 용이, 리플렉션 부담 감소
4. 심화 문제 해결 (Process & Experience)
실무에서 MapStruct의 체감 가치는 “기본 사용법”보다는, 아래 상황을 얼마나 깔끔하게 정리하느냐에서 크게 갈립니다.
- 필드명이 다를 때(규칙 선언)
- 객체 그래프가 복잡해질 때(매퍼 조합/순환/성능)
- 부분 업데이트(PATCH)에서 null 정책
- 어느 날 갑자기 터지는 트러블슈팅(대표:
Cannot find symbol)
4.1 필드명이 다를 때: @Mapping(source, target)
상황: 모델은 같은 의미인데 필드명이 다릅니다. 예를 들어 name을 DTO에서는 username으로 쓰는 경우입니다.
해결: 규칙만 선언해두면, 실제 구현 코드는 빌드 때 생성됩니다.
import org.mapstruct.*;
@Mapper(componentModel = "spring")
public interface UserMapper {
@Mapping(source = "name", target = "username")
UserDto toDto(User user);
}
4.2 특정 필드 제외(예: 민감정보): ignore = true
상황: 비밀번호/토큰 같은 값은 DTO로 흘리면 안 됩니다.
해결: 의도를 명시적으로 남겨두면, 리뷰/운영 시에도 안전장치가 됩니다.
@Mapper(componentModel = "spring")
public interface UserMapper {
@Mapping(target = "password", ignore = true)
UserDto toDto(User user);
}
4.3 복잡한 객체 그래프 매핑: uses = ...로 매퍼 조합하기
상황: 도메인이 커지면 Order -> Customer -> Address -> ...처럼 객체 그래프가 깊어집니다. 이때 하나의 매퍼에 모든 규칙을 넣기 시작하면 유지보수가 급격히 어려워집니다.
과정: 객체 그래프를 그대로 따라가되, 매퍼를 “도메인 단위”로 쪼개서 조합합니다.
해결: uses로 매퍼를 연결하면, 중첩 객체 매핑 책임이 자연스럽게 분리됩니다.
@Mapper(componentModel = "spring")
public interface CustomerMapper {
CustomerDto toDto(Customer customer);
}
@Mapper(componentModel = "spring", uses = CustomerMapper.class)
public interface OrderMapper {
OrderDto toDto(Order order);
}
주의할 점(Experience):
- 객체 그래프가 순환(양방향 연관 등) 구조라면 DTO 설계를 먼저 정리하지 않으면 매핑이 꼬일 수 있습니다.
- “한 번에 모든 것을 DTO로 내리기”보다, API 목적에 맞게 DTO를 얇게 설계하면 매핑도 훨씬 단순해집니다.
4.4 PATCH(부분 업데이트)에서 null 전략: @MappingTarget + null IGNORE
PATCH를 붙이다가 한 번쯤은 이런 사고를 겪습니다.
- 프론트에서 일부 필드만 보내고
- 나머지는
null로 들어왔는데 - 서버가 그
null을 그대로 엔티티에 덮어써서 값이 사라지는 케이스입니다.
PATCH DTO의 null은 “값을 지우겠다”가 아니라, “이번 요청에서 안 보냈다”일 때가 많습니다. 그래서 기본 매핑처럼 그대로 반영하면 운영에서 위험해집니다.
MapStruct에서는 기존 엔티티를 대상으로 업데이트하고(@MappingTarget), null은 건드리지 않도록 정책을 명시할 수 있습니다.
import org.mapstruct.*;
@Mapper(
componentModel = "spring",
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE
)
public interface UserMapper {
void updateFromDto(UserDto dto, @MappingTarget User entity);
}
운영 관점에서 느낀 포인트는 두 가지입니다.
- 이 한 줄(IGNORE)이 있으면 “PATCH 한 번에 데이터가 날아가는” 종류의 사고를 꽤 많이 막을 수 있습니다.
- 팀 컨벤션이 없으면 서비스마다 업데이트 기준이 달라져서, 같은 API인데도 동작이 달라지는 문제가 생깁니다. 초반에 한 번 정하고 표준화해두는 게 마음이 편합니다.
4.5 직접 겪은 트러블슈팅: Cannot find symbol이 갑자기 뜰 때
MapStruct를 붙여놓고 한동안 잘 돌아가다가, 어느 날 갑자기 빌드가 깨지는 경우가 있습니다.
Cannot find symbolXXXMapperImpl을 찾지 못한다는 에러
처음엔 “내가 매핑 코드를 건드렸나?” 싶어서 Mapper부터 뒤집어보게 되는데, 의외로 원인은 코드가 아니라 빌드/IDE 쪽 설정인 경우가 많았습니다.
제가 겪은 케이스는 보통 이런 흐름이었습니다.
- 의존성 정리, 빌드 설정 변경, IDE 업데이트 같은 작업 이후
- annotation processor가 어느 순간부터 제대로 돌지 않음
- 결과적으로
MapperImpl이 생성되지 않고 컴파일이 실패
그래서 아래 순서대로만 확인해도 대부분 빠르게 복구됐습니다.
- IDE에서 Annotation Processing이 꺼져 있지 않은지
- 빌드 설정에서
mapstruct-processor가 컴파일 타임에 실행되도록 잡혀 있는지 - 캐시/산출물이 꼬였으면 clean/rebuild로 한번 정리해보기
Lombok을 같이 쓰는 환경이라면, 증상이 더 애매하게 나올 때가 있습니다(특정 필드만 인식 못 하는 등). 이럴 때도 결국 첫 번째로 돌아가서, IDE 플러그인과 annotation processing 상태부터 확인하는 게 제일 빨랐습니다.
5. 결론 및 요약
MapStruct는 “매핑 코드를 없애는 도구”라기보다,
- 매핑 규칙을 선언적으로 관리하고
- 실제 코드는 컴파일 타임에 생성하여
- 빠르고 안전한 변환을 보장하는 도구입니다.
5.1 도입 전후의 생산성 변화(체감)
- 필드가 늘어날수록 반복 코드가 폭증하는데, MapStruct 도입 후에는 규칙 선언 중심으로 전환되어 작성/리뷰 비용이 줄어듭니다.
- 필드 추가/변경 누락이 런타임 버그가 되기 전에 컴파일 시점에 드러나는 경험이 쌓이면 운영 안정감이 확실히 달라집니다.
- PATCH처럼 민감한 업데이트 시나리오에서 null 정책을 표준화하면, “데이터가 날아가는 사고”를 예방하는 데 도움이 됩니다.
5.2 참고하면 좋은 공식 문서
- MapStruct 공식 사이트: https://mapstruct.org/
- Reference Guide: https://mapstruct.org/documentation/stable/reference/