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 에서 찾을 수 있습니다 .

Generic footer banner