Skip to content

DTO Mapper

English ID edited this page Dec 19, 2024 · 8 revisions

DTO 매핑 전략

다음 이유로 MapStruct를 채택했습니다.

  • 다른 기술에 비해 초기에 약간의 설명을 듣거나 조금은 파악해야 쉽게 사용할 수 있지만, 초기 진입이 충분히 쉽습니다.
    (적어도 초기에 설명을 듣고도 진입을 어려워하는 사례를 거의 볼 수 없었습니다.)
  • 복잡한 객체의 매핑도 명시적으로 지정할 수 있기 때문에 혼란이 적고 매우 편리합니다. (주로 ModelMapper와 비교할 때 장점)
  • 컴파일타임에 준비되기 때문에 성능이 좋습니다. (단, 매핑 성능이 전체 성능에 큰 영향을 주는 케이스는 드뭅니다.)

다음은 대안 기술과 비교한 표입니다.

구분 dto.toEntity() 함수 ModelMapper ✅ MapStruct
설명 DTO의 메서드를 사용하여 엔티티로 변환 리플렉션을 사용하여 DTO를 엔티티로 매핑 컴파일 타임에 코드 생성을 통해 매핑 수행
성능 ✅ 높음 (직접 코드 작성) 중간 (리플렉션 때문에 다소 느림) ✅ 매우 높음 (컴파일 최적화된 코드)
진입의 용이성 ✅ 매우 높음 (직접 제어 가능) ✅ 높음 (단순 설정으로 사용 가능) 중간 (종종 추가적인 설정 필요, 애노테이션 사용)
유지보수성 중간 (수동 작업이 많지만 명확함) 낮음 (자동 매핑은 추적과 수정이 어려움) ✅ 높음 (명시적 매핑으로 변경이 명확하게 관리됨)
설정의 복잡성 ✅ 없음 (수동 코드 작성) 중간 (설정 파일이나 API 필요) ✅ 낮음 (애노테이션 기반, 설정 최소화)
대규모 프로젝트 적합성 낮음 (수동 작업이 많아짐) 높음 (자동화로 인한 효율 증가) ✅ 매우 높음 (코드 자동 생성으로 일관성 및 효율 유지)

추가적으로 다음과 같은 특징이 있습니다.

  • Java 16 record에 대한 매핑도 잘 지원합니다.
  • 매우 스마트한 자동 매핑을 지원합니다. (생성자, public 필드, setter, 빌더, new Builder, 문자열과 각종 타입 간 매핑 등)
  • 스프링 프레임워크에 종속적인 라이브러리가 아닙니다.
  • 여러 커스텀 매핑 전략 지원 (다소 학습이 필요합니다. 한국어 번역 자료는 매우 부족합니다.)

DTO Mapper 구현

Prerequisites

MapStruct 라이브러리 추가

build.gradle.kts 파일에 다음을 추가합니다. (버전은 예시입니다.)

dependencies {
    // mapstruct
    implementation("org.mapstruct:mapstruct:1.6.3")
    annotationProcessor("org.mapstruct:mapstruct-processor:1.6.3")
    annotationProcessor("org.projectlombok:lombok-mapstruct-binding:0.2.0")
}

선택 사항: default component model

맵 스트럭트 사용 시 component model을 "spring"으로 명시하는 작업을 생략하기 위해, 컴파일 명령에 다음 옵션을 추가할 수 있습니다.

-Amapstruct.defaultComponentModel=spring

예시(build.gradle.kts):

tasks.withType<JavaCompile> {
    options.compilerArgs.addAll(listOf(
            "--enable-preview",
            "-Amapstruct.defaultComponentModel=spring", // << added
    ))
}

DTO Mapper 인터페이스 생성 (Spring 예시)

import org.mapstruct.Mapper;
import org.springframework.data.domain.Page;

import java.time.Instant;

// SPRING = "spring"
import static org.mapstruct.MappingConstants.ComponentModel.SPRING;

@Mapper(componentModel = SPRING) // "spring"
public interface BoardDtoMapper {
    Board toEntity(BoardCreateRequest dto, BoardStatus status, Instant createdAt, Instant updatedAt);
}

이로써 빈 등록이 완료됩니다.

  • 빈 스캔 범위에 있거나, 별도 빈 등록 수단을 사용해야 합니다. 대부분의 간략한 아키텍처에서 빈 스캔 범위에 잘 포함되어 있습니다.
Board 예시
// simplified example
public class Board extends BaseEntity {
    private String title;
    private String content;

    private BoardStatus status;
    private Instant createdAt;
    private Instant updatedAt;
}
BoardCreateRequest 예시
// simplified example
public record BoardCreateRequest(String title, String content) {/* can be empty here */}

DTO Mapper 사용

등록된 매퍼 빈을 주입받아 사용합니다.

// simplified example

@Component // @Controller, @Service 등 다른 애노테이션을 사용할 수도 있습니다.
public class ExampleShower {
    private final BoardDtoMapper mapper;

    public ExampleShower(BoardDtoMapper boardDtoMapper) {
        this.mapper = boardDtoMapper;
    }

    public void doExample(BoardCreateRequest dto) {
        Instant now = Instant.now();
        Board entity = mapper.toEntity(
                dto,
                BoardStatus.ACTIVE,
                now,
                now
        );

        // handle the entity now
        //  ... (skipped)
    }
}

Quick

  • Home
  • Code Styles
    • Controller
    • Service
      • Interface: Use Cases
      • Command Service
      • Query Service
    • Repository (Spring Data JPA Only)
      • CQRS
      • Command Repository
      • Query Repository
    • Domain
      • Enum Status
      • Base Entity
      • UUID Base Entity
      • JPA Entity
    • Error Code & Exceptional Response
      • Interface Error Code
      • Enum Error Code (impl. Error Code)
      • Custom Exception
      • Extended Custom Exception
    • 상수 관리
      • Static Final 기호상수
      • Enum을 통한 상수 관리
  • 프로젝트 보조 도구 활용
    • 작업 환경 및 운영 환경
      • Docker Compose (작업 환경)
      • Flyway (작업 환경, 운영 환경의 일치 필요)
Clone this wiki locally