MapStruct 데이터 변환 (1) 기본 개념과 코드

Letsdev·2023년 10월 18일
1

MapStruct 사용하기

목록 보기
1/2

🔭 4 min read


MapStruct? 💡

맵스트럭트 로고

A {...} ➡️ B {...}

MapStruct객체의 타입 변환에 사용할 수 있는 자바 라이브러리입니다.
MapStruct의 정체성은 자바 객체 매핑을 위한 코드 생성기입니다. 두 클래스의 각 필드를 동일한 이름 또는 지정한 이름 간에 매핑하여 사본 객체를 만듭니다.

왜 쓰는 걸까? 🤷‍♀️

이런 라이브러리를 사용하는 이유는 자바가 전통적인 강타입 언어이기 때문입니다.
자바는 서로 다른 두 클래스가 아무리 내부 필드 구조를 동일하게 띠더라도 서로 변환하여야 합니다. 일반적인 경우 변환 없이 서로 직접 대체할 수 없기 때문입니다.

✍️ ps. 자바는 최신 신택스에서 내용을 기반으로 한 변환도 어느 정도 지원하곤 합니다.

자바의 최신 신택스에서는 일부 패턴 매칭 등을 통해 객체의 내부 구조를 기반으로 매칭하거나 변환하는 등, 마치 덕타이핑(🦆)처럼 동작하는 일부 기능을 제공하고 있습니다. 아직 엔티티 등에 사용할 수는 없는데, 다소간 유연하게 제공하려는 노력이 보입니다.

Map Struct vs Model Mapper

Model Mapper는 자바 리플렉션을 통해 런타임에 온갖 처리를 수행합니다. 반면 MapStruct는 컴파일타임에 미리 준비해 둔 동작을 런타임에 실행만 하면 되기 때문에 퍼포먼스에 있어서는 MapStruct가 앞선다고 말할 수 있습니다.

평범한 데이터 변환은 전체 로직에서는 작은 부분을 차지하는 편일 겁니다.^{*} 따라서 둘 중 어느것을 택하더라도 퍼포먼스에 큰 영향은 아닐 것입니다. 두 라이브러리는 거의 동일한 기능을 제공할 수 있습니다.
일부 아키텍처나 로직에서는 변환을 빈번하게 수행해야 할 수도 있습니다.{}^{*}{}_{일부\ 아키텍처나\ 로직에서는\ 변환을\ 빈번하게\ 수행해야\ 할\ 수도\ 있습니다.}

컴파일타임 vs 런타임 중 어느 곳에서 시간을 아낄 것이냐 하면, 너무 억지로 컴파일 타임을 증가시키는 경우가 아니라면 런타임 이점을 얻는 것이 이득이기 때문에, 런타임 이점을 위해 MapStruct를 선택하는 것은 합리적인 도출입니다.

그 외 작업 편의성 등은 개인차가 있을 수 있습니다.

우리 팀은 리플렉션에 대한 기피, 그리고 굳이 Model Mapper를 선택할 이유가 없다는 쪽으로 생각이 모여 MapStruct를 선택했습니다.

지원이 얼마나 되는 걸까?

생각보다 잘 만들어 두었더군요.

일반적인 지원 특성

일반적인 지원만 해도 충분한 커버리지가 있습니다.

  • public 필드도 가능합니다.
  • Getter/Setter를 사용한 필드도 가능합니다.
  • 롬복(Lombok)의 @Builder도 인식하여 잘 적용합니다.
  • 리스트(List<T>) 변환도 알잘딱깔센 해 줍니다.

추가적인 이점들

추가적으로 여러 기술에 종속된 네이밍에 대해서도 호환해 주는 것으로 보입니다.

  • 일부 기술에서 .newBuilder() 등 롬복 빌더와 다른 네이밍을 사용하고 있는 빌더도 생각보다 잘 인식합니다.
    ex: gRPC 사용 시 protobuf 플러그인에 의해 자동으로 생성된 코드 등
  • 내부 필드의 내부 리스트 필드의 각 원소의 필드 등, 복잡한 데이터 구조에 대해서도 매핑할 수 있습니다.
    ex: product.optionGroups[].options 등에 대한 매핑
  • 내부 필드 간에도 서로 타입이 다르더라도 필드 네임을 통해 매핑을 합니다.
    ex: 열거 타입인 com.example.demo.MyProductStatus와 열거 타입인 org.another.ProvidedProductStatus 간 열거 상수 네임이 같다면 자동으로 매핑
  • 내부 필드의 데이터 구조가 많이 달라서 일부 구간에 서로 명시적인 변환 코드 작성을 필요로 하는 경우, 특정 타입 간 매핑을 미리 작성해 두면, 그 메서드가 다른 변환에도 자동으로 적용됩니다.

러닝커브

대신 명시적인 것을 선호하는 것으로 보여, 어느 정도 러닝커브를 유발할 순 있습니다.

또 아직 한글 자료가 별로 없는 편이기 때문에 영문 자료가 낯설다면 러닝커브를 더 크게 느낄 수 있습니다.


기본 사용

Add Dependencies

Gradle 기준으로는 다음처럼 작성합니다.

dependencies {
    // Map Struct
    implementation 'org.mapstruct:mapstruct:1.5.3.Final'
    annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.3.Final'
    annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0'
}

(implementation) org.mapstruct:mapstruct:1.5.3.Final
MapStruct 사용을 위한 의존성 라이브러리입니다.

(annotationProcessor) org.mapstruct:mapstruct-processor:1.5.3.Final
의존성 목록에 반드시 Annotation Processor를 추가하여야 합니다.
MapStruct는 각 데이터 매퍼(데이터 변환 인터페이스)의 클래스를 생성해 두어야 하기 때문입니다.

(annotationProcessor) org.projectlombok:lombok-mapstruct-binding:0.2.0
롬복에서 MapStruct와 충돌을 없애기 위한 org.projectlombok:lombok-mapstruct-binding 애노테이션 프로세서를 제공합니다. 이것을 사용하지 않으면 롬복 Annotation Processor와 호출 순서 등에서 충돌이 있습니다.

Maven
Gradle에서는 의존성 라이브러리에 그룹명:아티팩트명:버전 순으로 작성하기 때문에, Maven 사용자는 참고해서 변환하여 작성하시면 됩니다.


스프링이 아닌 환경에서 사용

다음은 기본적인 MapStruct 사용에 유용한 두 애노테이션을 포함하는 예시입니다.

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

@Mapper
public interface SignUpDtoMapper {

    SignUpDtoMapper INSTANCE = Mappers.getInstance(SignUpDtoMapper.class);

    @Mapping(target = "username", source = "dto.userId")
    @Mapping(target = "password", source = "dto.userPw")
    Account toAccount(SignUpRequestDto dto, AccountStatus status, Instant createdAt);
    
    // account.username = dto.userId;
    // account.password = dto.userPw;
    // ... (다른 필드들은 이름이 같은 경우 자동으로 매핑.
    // account.status = status;
    // account.createdAt = createdAt;
}
  • @org.mapstruct.Mapper 애노테이션으로 이 인터페이스가 매퍼로 구현될 것임을 선언합니다.
  • @org.mapstruct.Mapping 애노테이션으로 이 메서드의 각 필드 중 이름이 다른 것들 또는 여러 파라미터 객체들의 중복된 필드 이름 중 선택된 것 등을 매핑할 수 있습니다.

사용

인터페이스에 INSTANCE 또는 getInstance() 등 작성한 대로 사용합니다.

SignUpDtoMapper mapper = SignUpDtoMapper.INSTANCE;

구체적인 사용

스프링 환경에서 사용하는 코드와 구체적인 사용은 이 스레드의 다른 글에서 이어서 설명하겠습니다.

profile
아 성장판 쑤셔 (블로그 이전) https://letsdev.hashnode.dev

0개의 댓글