🔭 4 min read
A {...} ➡️ B {...}
MapStruct는 객체의 타입 변환에 사용할 수 있는 자바 라이브러리입니다.
MapStruct의 정체성은 자바 객체 매핑을 위한 코드 생성기입니다. 두 클래스의 각 필드를 동일한 이름 또는 지정한 이름 간에 매핑하여 사본 객체를 만듭니다.
이런 라이브러리를 사용하는 이유는 자바가 전통적인 강타입 언어이기 때문입니다.
자바는 서로 다른 두 클래스가 아무리 내부 필드 구조를 동일하게 띠더라도 서로 변환하여야 합니다. 일반적인 경우 변환 없이 서로 직접 대체할 수 없기 때문입니다.
✍️ ps. 자바는 최신 신택스에서 내용을 기반으로 한 변환도 어느 정도 지원하곤 합니다.
자바의 최신 신택스에서는 일부 패턴 매칭 등을 통해 객체의 내부 구조를 기반으로 매칭하거나 변환하는 등, 마치 덕타이핑(🦆)처럼 동작하는 일부 기능을 제공하고 있습니다. 아직 엔티티 등에 사용할 수는 없는데, 다소간 유연하게 제공하려는 노력이 보입니다.
Model Mapper는 자바 리플렉션을 통해 런타임에 온갖 처리를 수행합니다. 반면 MapStruct는 컴파일타임에 미리 준비해 둔 동작을 런타임에 실행만 하면 되기 때문에 퍼포먼스에 있어서는 MapStruct가 앞선다고 말할 수 있습니다.
평범한 데이터 변환은 전체 로직에서는 작은 부분을 차지하는 편일 겁니다. 따라서 둘 중 어느것을 택하더라도 퍼포먼스에 큰 영향은 아닐 것입니다. 두 라이브러리는 거의 동일한 기능을 제공할 수 있습니다.
컴파일타임 vs 런타임 중 어느 곳에서 시간을 아낄 것이냐 하면, 너무 억지로 컴파일 타임을 증가시키는 경우가 아니라면 런타임 이점을 얻는 것이 이득이기 때문에, 런타임 이점을 위해 MapStruct를 선택하는 것은 합리적인 도출입니다.
그 외 작업 편의성 등은 개인차가 있을 수 있습니다.
우리 팀은 리플렉션에 대한 기피, 그리고 굳이 Model Mapper를 선택할 이유가 없다는 쪽으로 생각이 모여 MapStruct를 선택했습니다.
생각보다 잘 만들어 두었더군요.
일반적인 지원만 해도 충분한 커버리지가 있습니다.
public
필드도 가능합니다.Getter
/Setter
를 사용한 필드도 가능합니다.Lombok
)의 @Builder
도 인식하여 잘 적용합니다.List<T>
) 변환도 알잘딱깔센 해 줍니다.추가적으로 여러 기술에 종속된 네이밍에 대해서도 호환해 주는 것으로 보입니다.
.newBuilder()
등 롬복 빌더와 다른 네이밍을 사용하고 있는 빌더도 생각보다 잘 인식합니다.gRPC
사용 시 protobuf
플러그인에 의해 자동으로 생성된 코드 등product.optionGroups[].options
등에 대한 매핑com.example.demo.MyProductStatus
와 열거 타입인 org.another.ProvidedProductStatus
간 열거 상수 네임이 같다면 자동으로 매핑대신 명시적인 것을 선호하는 것으로 보여, 어느 정도 러닝커브를 유발할 순 있습니다.
또 아직 한글 자료가 별로 없는 편이기 때문에 영문 자료가 낯설다면 러닝커브를 더 크게 느낄 수 있습니다.
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;
스프링 환경에서 사용하는 코드와 구체적인 사용은 이 스레드의 다른 글에서 이어서 설명하겠습니다.