자바 백엔드 입문 58편 — MapStruct DTO 매핑 자동화

2026-05-17자바 백엔드 입문

자바 백엔드 입문 58편. Entity와 DTO 사이 변환을 컴파일 시점 코드 생성으로 자동화하는 MapStruct 표준 패턴을 풀어쓴 학습 노트.

📚 자바 백엔드 입문 · 58편 — MapStruct DTO 매핑 자동화

이 글은 자바 백엔드 입문 시리즈 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"

시리즈 다른 편 (앞뒤 글 모음)

이전 글:

다음 글:

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!