2026년 2월 15일조회 44분 읽기
카테고리: JAVA

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으로 쓰는 경우입니다.

해결: 규칙만 선언해두면, 실제 구현 코드는 빌드 때 생성됩니다.

java
import org.mapstruct.*;

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

    @Mapping(source = "name", target = "username")
    UserDto toDto(User user);
}

4.2 특정 필드 제외(예: 민감정보): ignore = true

상황: 비밀번호/토큰 같은 값은 DTO로 흘리면 안 됩니다.

해결: 의도를 명시적으로 남겨두면, 리뷰/운영 시에도 안전장치가 됩니다.

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

    @Mapping(target = "password", ignore = true)
    UserDto toDto(User user);
}

4.3 복잡한 객체 그래프 매핑: uses = ...로 매퍼 조합하기

상황: 도메인이 커지면 Order -> Customer -> Address -> ...처럼 객체 그래프가 깊어집니다. 이때 하나의 매퍼에 모든 규칙을 넣기 시작하면 유지보수가 급격히 어려워집니다.

과정: 객체 그래프를 그대로 따라가되, 매퍼를 “도메인 단위”로 쪼개서 조합합니다.

해결: uses로 매퍼를 연결하면, 중첩 객체 매핑 책임이 자연스럽게 분리됩니다.

java
@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은 건드리지 않도록 정책을 명시할 수 있습니다.

java
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 symbol
  • XXXMapperImpl을 찾지 못한다는 에러

처음엔 “내가 매핑 코드를 건드렸나?” 싶어서 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 참고하면 좋은 공식 문서