1. 소개
여러 계층으로 구성된 대규모 Java 애플리케이션을 만들려면 지속성 모델, 도메인 모델 또는 소위 DTO와 같은 여러 모델을 사용해야 합니다. 서로 다른 애플리케이션 계층에 대해 여러 모델을 사용하려면 빈 간 매핑 방법을 제공해야 합니다.
이 작업을 수동으로 수행하면 많은 상용구 코드를 빠르게 생성하고 많은 시간을 소비할 수 있습니다. 다행스럽게도 Java용 객체 매핑 프레임워크가 여러 개 있습니다.
이 사용방법(예제)에서는 가장 널리 사용되는 Java 매핑 프레임워크의 성능을 비교할 것입니다.
2. 매핑 프레임워크
2.1. 도저
Dozer는 재귀를 사용하여 한 개체에서 다른 개체로 데이터를 복사하는 매핑 프레임워크입니다 . 프레임워크는 빈 간에 속성을 복사할 수 있을 뿐만 아니라 다른 유형 간에 자동으로 변환할 수도 있습니다.
Dozer 프레임워크를 사용하려면 프로젝트에 이러한 의존성을 추가해야 합니다.
<dependency>
<groupId>com.github.dozermapper</groupId>
<artifactId>dozer-core</artifactId>
<version>6.5.2</version>
</dependency>
Dozer 프레임워크 사용에 대한 자세한 내용은 이 문서 에서 확인할 수 있습니다 .
프레임워크 문서는 여기에서 찾을 수 있으며 최신 버전은 여기 에서 찾을 수 있습니다 .
2.2. 오리카
Orika는 한 개체에서 다른 개체로 데이터를 재귀적으로 복사하는 bean to bean 매핑 프레임워크입니다 .
Orika의 일반적인 작동 원리는 Dozer와 유사합니다. 둘 사이의 주요 차이점은 Orika가 바이트코드 생성 을 사용한다는 사실입니다 . 이를 통해 최소한의 오버헤드로 더 빠른 매퍼를 생성할 수 있습니다.
이를 사용하려면 프로젝트에 이러한 의존성을 추가해야 합니다.
<dependency>
<groupId>ma.glasnost.orika</groupId>
<artifactId>orika-core</artifactId>
<version>1.5.4</version>
</dependency>
Orika 사용법에 대한 자세한 내용은 이 문서 에서 확인할 수 있습니다 .
프레임워크의 실제 문서는 여기에서 찾을 수 있으며 최신 버전은 여기 에서 찾을 수 있습니다 .
경고: Java 16 부터 잘못된 반사 액세스 는 기본적으로 거부됩니다 . Orika의 1.5.4 버전은 이러한 반사 액세스를 사용하므로 Orika는 현재 Java 16과 함께 사용할 수 없습니다. 이 문제는 향후 1.6.0 버전의 릴리스로 해결될 것이라고 합니다.
2.3. MapStruct
MapStruct는 빈 매퍼 클래스를 자동으로 생성하는 코드 생성기입니다.
MapStruct에는 서로 다른 데이터 유형 간에 변환하는 기능도 있습니다. 사용 방법에 대한 자세한 내용은 이 문서 에서 확인할 수 있습니다 .
프로젝트 에 MapStruct를 추가하려면 다음 의존성을 포함해야 합니다.
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.2.Final</version>
</dependency>
프레임워크 문서는 여기에서 찾을 수 있으며 최신 버전은 여기 에서 찾을 수 있습니다 .
2.4. ModelMapper
ModelMapper는 규칙에 따라 개체가 서로 매핑되는 방식을 결정하여 개체 매핑을 단순화하는 것을 목표로 하는 프레임워크입니다. 유형 안전 및 리팩토링 안전 API를 제공합니다.
프레임워크에 대한 자세한 내용은 설명서 에서 찾을 수 있습니다 .
프로젝트에 ModelMapper를 포함하려면 다음 의존성을 추가해야 합니다.
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>3.1.0</version>
</dependency>
프레임워크의 최신 버전은 여기 에서 찾을 수 있습니다 .
2.5. JMapper
JMapper는 사용하기 쉬운 고성능 Java Bean 간의 매핑을 제공하는 것을 목표로 하는 매핑 프레임워크입니다.
프레임워크는 어노테이션 및 관계형 매핑을 사용하여 DRY 원칙을 적용하는 것을 목표로 합니다.
프레임워크는 어노테이션 기반, XML 또는 API 기반과 같은 다양한 구성 방법을 허용합니다.
프레임워크에 대한 자세한 내용은 설명서 에서 찾을 수 있습니다 .
프로젝트에 JMapper를 포함하려면 의존성을 추가해야 합니다.
<dependency>
<groupId>com.googlecode.jmapper-framework</groupId>
<artifactId>jmapper-core</artifactId>
<version>1.6.1.CR2</version>
</dependency>
프레임워크의 최신 버전은 여기 에서 찾을 수 있습니다 .
3. 테스트 모델
매핑을 제대로 테스트하려면 소스 및 대상 모델이 있어야 합니다. 두 가지 테스트 모델을 만들었습니다.
첫 번째는 하나의 문자열 필드가 있는 간단한 POJO입니다. 이를 통해 더 간단한 경우 프레임워크를 비교하고 더 복잡한 bean을 사용하는 경우 변경 사항이 있는지 확인할 수 있습니다.
간단한 소스 모델은 다음과 같습니다.
public class SourceCode {
String code;
// getter and setter
}
그리고 목적지는 매우 유사합니다.
public class DestinationCode {
String code;
// getter and setter
}
소스 bean의 실제 예는 다음과 같습니다.
public class SourceOrder {
private String orderFinishDate;
private PaymentType paymentType;
private Discount discount;
private DeliveryData deliveryData;
private User orderingUser;
private List<Product> orderedProducts;
private Shop offeringShop;
private int orderId;
private OrderStatus status;
private LocalDate orderDate;
// standard getters and setters
}
그리고 대상 클래스는 다음과 같습니다.
public class Order {
private User orderingUser;
private List<Product> orderedProducts;
private OrderStatus orderStatus;
private LocalDate orderDate;
private LocalDate orderFinishDate;
private PaymentType paymentType;
private Discount discount;
private int shopId;
private DeliveryData deliveryData;
private Shop offeringShop;
// standard getters and setters
}
전체 모델 구조는 여기 에서 찾을 수 있습니다 .
4. 변환기
테스트 설정의 디자인을 단순화하기 위해 변환기 인터페이스를 만들었습니다.
public interface Converter {
Order convert(SourceOrder sourceOrder);
DestinationCode convert(SourceCode sourceCode);
}
그리고 우리의 모든 사용자 지정 매퍼는 이 인터페이스를 구현합니다.
4.1. 오리카 변환기
Orika는 전체 API 구현을 허용하므로 매퍼 생성을 크게 단순화합니다.
public class OrikaConverter implements Converter{
private MapperFacade mapperFacade;
public OrikaConverter() {
MapperFactory mapperFactory = new DefaultMapperFactory
.Builder().build();
mapperFactory.classMap(Order.class, SourceOrder.class)
.field("orderStatus", "status").byDefault().register();
mapperFacade = mapperFactory.getMapperFacade();
}
@Override
public Order convert(SourceOrder sourceOrder) {
return mapperFacade.map(sourceOrder, Order.class);
}
@Override
public DestinationCode convert(SourceCode sourceCode) {
return mapperFacade.map(sourceCode, DestinationCode.class);
}
}
4.2. 도저 컨버터
Dozer에는 다음 섹션이 포함된 XML 매핑 파일이 필요합니다.
<mappings xmlns="http://dozermapper.github.io/schema/bean-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://dozermapper.github.io/schema/bean-mapping
https://dozermapper.github.io/schema/bean-mapping.xsd">
<mapping>
<class-a>com.baeldung.performancetests.model.source.SourceOrder</class-a>
<class-b>com.baeldung.performancetests.model.destination.Order</class-b>
<field>
<a>status</a>
<b>orderStatus</b>
</field>
</mapping>
<mapping>
<class-a>com.baeldung.performancetests.model.source.SourceCode</class-a>
<class-b>com.baeldung.performancetests.model.destination.DestinationCode</class-b>
</mapping>
</mappings>
XML 매핑을 정의한 후 코드에서 사용할 수 있습니다.
public class DozerConverter implements Converter {
private final Mapper mapper;
public DozerConverter() {
this.mapper = DozerBeanMapperBuilder.create()
.withMappingFiles("dozer-mapping.xml")
.build();
}
@Override
public Order convert(SourceOrder sourceOrder) {
return mapper.map(sourceOrder,Order.class);
}
@Override
public DestinationCode convert(SourceCode sourceCode) {
return mapper.map(sourceCode, DestinationCode.class);
}
}
4.3. MapStructConverter
MapStruct 정의는 전적으로 코드 생성을 기반으로 하기 때문에 매우 간단합니다.
@Mapper
public interface MapStructConverter extends Converter {
MapStructConverter MAPPER = Mappers.getMapper(MapStructConverter.class);
@Mapping(source = "status", target = "orderStatus")
@Override
Order convert(SourceOrder sourceOrder);
@Override
DestinationCode convert(SourceCode sourceCode);
}
4.4. JMapper 변환기
JMapperConverter 에는 더 많은 작업이 필요합니다. 인터페이스 구현 후:
public class JMapperConverter implements Converter {
JMapper realLifeMapper;
JMapper simpleMapper;
public JMapperConverter() {
JMapperAPI api = new JMapperAPI()
.add(JMapperAPI.mappedClass(Order.class));
realLifeMapper = new JMapper(Order.class, SourceOrder.class, api);
JMapperAPI simpleApi = new JMapperAPI()
.add(JMapperAPI.mappedClass(DestinationCode.class));
simpleMapper = new JMapper(
DestinationCode.class, SourceCode.class, simpleApi);
}
@Override
public Order convert(SourceOrder sourceOrder) {
return (Order) realLifeMapper.getDestination(sourceOrder);
}
@Override
public DestinationCode convert(SourceCode sourceCode) {
return (DestinationCode) simpleMapper.getDestination(sourceCode);
}
}
또한 대상 클래스의 각 필드에 @JMap 어노테이션 을 추가해야 합니다 . 또한 JMapper는 자체적으로 enum 유형 간에 변환할 수 없으며 사용자 지정 매핑 함수를 생성해야 합니다.
@JMapConversion(from = "paymentType", to = "paymentType")
public PaymentType conversion(com.baeldung.performancetests.model.source.PaymentType type) {
PaymentType paymentType = null;
switch(type) {
case CARD:
paymentType = PaymentType.CARD;
break;
case CASH:
paymentType = PaymentType.CASH;
break;
case TRANSFER:
paymentType = PaymentType.TRANSFER;
break;
}
return paymentType;
}
4.5. ModelMapper변환기
ModelMapperConverter 에서는 매핑하려는 클래스만 제공하면 됩니다.
public class ModelMapperConverter implements Converter {
private ModelMapper modelMapper;
public ModelMapperConverter() {
modelMapper = new ModelMapper();
}
@Override
public Order convert(SourceOrder sourceOrder) {
return modelMapper.map(sourceOrder, Order.class);
}
@Override
public DestinationCode convert(SourceCode sourceCode) {
return modelMapper.map(sourceCode, DestinationCode.class);
}
}
5. 간단한 모델 테스트
성능 테스트를 위해 Java Microbenchmark Harness를 사용할 수 있으며 사용 방법에 대한 자세한 내용은 이 문서 에서 확인할 수 있습니다 .
우리는 BenchmarkMode 를 Mode.All 로 지정하여 각 변환기 에 대해 별도의 벤치마크를 만들었 습니다.
5.1. 평균시간
JMH는 평균 실행 시간에 대해 다음 결과를 반환했습니다(작을수록 좋음).
프레임워크 이름 | 평균 실행 시간(작업당 ms) |
---|---|
MapStruct | 10 -5 |
JMapper | 10 -5 |
오리카 | 0.001 |
ModelMapper | 0.002 |
도저 | 0.004 |
이 벤치마크는 MapStruct와 JMapper 모두 최고의 평균 작업 시간을 가지고 있음을 분명히 보여줍니다.
5.2. 처리량
이 모드에서 벤치마크는 초당 작업 수를 반환합니다. 다음과 같은 결과를 얻었습니다( 많을수록 좋음 ).
프레임워크 이름 | 처리량(ms당 작업) |
---|---|
MapStruct | 58101 |
JMapper | 53667 |
오리카 | 1195년 |
ModelMapper | 379 |
도저 | 230 |
처리량 모드에서 MapStruct는 테스트된 프레임워크 중 가장 빨랐으며 JMapper가 2위를 차지했습니다.
5.3. 싱글샷타임
이 모드에서는 단일 작동의 시작부터 끝까지의 시간을 측정할 수 있습니다. 벤치마크 결과는 다음과 같습니다(적을수록 좋음).
프레임워크 이름 | 단일 샷 시간(작업당 ms) |
---|---|
JMapper | 0.016 |
MapStruct | 1.904 |
도저 | 3.864 |
오리카 | 6.593 |
ModelMapper | 8.788 |
여기에서 JMapper가 MapStruct보다 더 나은 결과를 반환하는 것을 볼 수 있습니다.
5.4. 샘플 시간
이 모드에서는 각 작업의 시간을 샘플링할 수 있습니다. 세 가지 다른 백분위수에 대한 결과는 다음과 같습니다.
샘플 시간(작업당 밀리초) | |||
---|---|---|---|
프레임워크 이름 | p0.90 | p0.999 | p1.0 |
JMapper | 10 -4 | 0.001 | 1.526 |
MapStruct | 10 -4 | 10 -4 | 1.948 |
오리카 | 0.001 | 0.018 | 2.327 |
ModelMapper | 0.002 | 0.044 | 3.604 |
도저 | 0.003 | 0.088 | 5.382 |
모든 벤치마크는 시나리오에 따라 MapStruct와 JMapper가 둘 다 좋은 선택이라는 것을 보여주었습니다 .
6. 실제 모델 테스트
성능 테스트를 위해 Java Microbenchmark Harness를 사용할 수 있으며 사용 방법에 대한 자세한 내용은 이 문서 에서 확인할 수 있습니다 .
우리는 BenchmarkMode 를 Mode.All 로 지정하여 각 변환기 에 대해 별도의 벤치마크를 만들었습니다 .
6.1. 평균시간
JMH는 평균 실행 시간에 대해 다음 결과를 반환했습니다(적을수록 좋음).
프레임워크 이름 | 평균 실행 시간(작업당 ms) |
---|---|
MapStruct | 10 -4 |
JMapper | 10 -4 |
오리카 | 0.007 |
ModelMapper | 0.137 |
도저 | 0.145 |
6.2. 처리량
이 모드에서 벤치마크는 초당 작업 수를 반환합니다. 각 매퍼에 대해 다음과 같은 결과를 얻었습니다(많을수록 좋음).
프레임워크 이름 | 처리량(ms당 작업) |
---|---|
JMapper | 3205 |
MapStruct | 3467 |
오리카 | 121 |
ModelMapper | 7 |
도저 | 6.342 |
6.3. 싱글샷타임
이 모드에서는 단일 작동의 시작부터 끝까지의 시간을 측정할 수 있습니다. 벤치마크 결과는 다음과 같습니다(적을수록 좋음).
프레임워크 이름 | 단일 샷 시간(작업당 ms) |
---|---|
JMapper | 0.722 |
MapStruct | 2.111 |
도저 | 16.311 |
ModelMapper | 22.342 |
오리카 | 32.473 |
6.4. 샘플 시간
이 모드에서는 각 작업의 시간을 샘플링할 수 있습니다. 샘플링 결과는 백분위수로 나뉘며 세 가지 다른 백분위수 p0.90, p0.999 및 p1.00에 대한 결과를 표시 합니다 .
샘플 시간(작업당 밀리초) | |||
---|---|---|---|
프레임워크 이름 | p0.90 | p0.999 | p1.0 |
JMapper | 10 -3 | 0.006 | 삼 |
MapStruct | 10 -3 | 0.006 | 8 |
오리카 | 0.007 | 0.143 | 14 |
ModelMapper | 0.138 | 0.991 | 15 |
도저 | 0.131 | 0.954 | 7 |
간단한 예제와 실제 예제의 정확한 결과는 분명히 다르지만 거의 동일한 추세를 따릅니다. 두 예에서 우리는 JMapper와 MapStruct가 최고의 자리를 놓고 치열한 경합을 벌이는 것을 보았습니다.
6.5. 결론
이 섹션에서 수행한 실제 모델 테스트를 기반으로 MapStruct가 2위를 차지했지만 최고의 성능은 분명히 JMapper에 속한다는 것을 알 수 있습니다. 동일한 테스트에서 Dozer가 SingleShotTime 을 제외하고 결과 테이블의 맨 아래에 일관되게 있음을 알 수 있습니다.
7. 요약
이 기사에서는 인기 있는 다섯 가지 Java 빈 매핑 프레임워크인 ModelMapper , MapStruct , Orika , Dozer 및 JMapper의 성능 테스트를 수행했습니다.
항상 그렇듯이 코드 샘플은 GitHub 에서 찾을 수 있습니다 .