Post

Java 매핑 프레임워크 성능

1.매핑 프레임워크

1) Dozer

Dozer는 재귀를 사용하여 한 객체에서 다른 객체로 데이터를 복사하는 매핑 프레임워크이다. 이 프레임워크는 빈 간에 속성을 복사할 수 있을 뿐만 아니라, 서로 다른 유형 간에 자동으로 변환할 수도 있다.

Dozer 프레임워크를 사용하려면 프로젝트에 다음과 같은 종속성을 추가해야 한다.

1
2
3
4
5
<dependency>
    <groupId>com.github.dozermapper</groupId>
    <artifactId>dozer-core</artifactId>
    <version>6.5.2</version>
</dependency>

프레임워크에 대한 문서는 여기에서 찾을 수 있으며, 최신 버전은 여기에서 찾을 수 있다.

2) Orika

Orika는 한 객체에서 다른 객체로 데이터를 재귀적으로 복사하는 빈 대 빈 매핑 프레임워크이다.

Orika의 일반적인 작동 원리는 Dozer와 유사하다. 두 가지의 주요 차이점은 Orika가 바이트코드 생성을 사용한다는 것이다. 이를 통해 최소한의 오버헤드로 더 빠른 매퍼를 생성할 수 있다.

이를 사용하려면 프로젝트에 다음과 같은 종속성을 추가해야 한다.

1
2
3
4
5
<dependency>
    <groupId>ma.glasnost.orika</groupId>
    <artifactId>orika-core</artifactId>
    <version>1.5.4</version>
</dependency>

프레임워크의 실제 문서는 여기에서 찾을 수 있으며, 최신 버전은 여기에서 찾을 수 있다.

주의: Java 16 부터 불법적인 반사적 접근은 기본적으로 거부된다. Orika 1.5.4 버전은 이러한 반사적 접근을 사용하므로 Orika는 현재 Java 16과 함께 사용할 수 없다. 이 문제는 향후 1.6.0 버전이 출시되면 해결될 것으로 예상된다.

3) MapStruct

MapStruct는 빈 매퍼 클래스를 자동으로 생성하는 코드 생성기이다.

MapStruct는 다양한 데이터 유형 간의 변환 기능도 제공한다.

프로젝트에 MapStruct를 추가하려면 다음 종속성을 포함해야 합니다.

1
2
3
4
5
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.6.0.Beta1</version>
</dependency>

프레임워크에 대한 문서는 여기에서 찾을 수 있으며, 최신 버전은 여기에서 찾을 수 있다.

4) ModelMapper

ModelMapper는 객체 매핑을 단순화하는 프레임워크로, 규칙에 따라 객체 간 매핑 방식을 결정한다. 타입 안전 및 리팩토링 안전 API를 제공한다.

프레임워크에 대한 자세한 내용은 설명서에서 확인할 수 있다.

프로젝트에 ModelMapper를 포함하려면 다음 종속성을 추가해야 합니다.

1
2
3
4
5
<dependency>
  <groupId>org.modelmapper</groupId>
  <artifactId>modelmapper</artifactId>
  <version>3.2.0</version>
</dependency>

프레임워크의 최신 버전은 여기에서 찾을 수 있다.

5) JMapper

JMapper는 Java Beans 간의 사용하기 쉽고 고성능의 매핑을 제공하는 것을 목표로 하는 매핑 프레임워크이다.

이 프레임워크는 주석과 관계 매핑을 사용하여 DRY 원칙을 적용하는 것을 목표로 한다.

이 프레임워크는 주석 기반, XML 또는 API 기반 등 다양한 구성 방식을 허용한다.

프레임워크에 대한 자세한 내용은 설명서에서 확인할 수 있다.

프로젝트에 JMapper를 포함하려면 종속성을 추가해야 한다.

1
2
3
4
5
<dependency>
    <groupId>com.googlecode.jmapper-framework</groupId>
    <artifactId>jmapper-core</artifactId>
    <version>1.6.1.CR2</version>
</dependency>

프레임워크의 최신 버전은 여기에서 찾을 수 있다.

2. 테스트 모델

매핑을 제대로 테스트하려면 소스 모델과 타겟 모델이 필요하다. 두 가지 테스트 모델을 만들었다.

첫 번째는 하나의 문자열 필드가 있는 간단한 POJO이다. 이를 통해 더 간단한 사례에서 프레임워크를 비교하고 더 복잡한 빈을 사용하면 변경 사항이 있는지 확인할 수 있다.

간단한 소스 모델은 아래와 같다.

1
2
3
4
public class SourceCode {
    String code;
    // getter and setter
}

그리고 그 목적지는 매우 유사하다.

1
2
3
4
public class DestinationCode {
    String code;
    // getter and setter
}

소스 빈의 실제 예는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
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
}

대상 클래스는 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
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
}

전체 모델 구조는 여기에서 확인할 수 있다.

3. 변환기

테스트 설정의 설계를 단순화하기 위해 Converter 인터페이스를 만들었다.

1
2
3
4
public interface Converter {
    Order convert(SourceOrder sourceOrder);
    DestinationCode convert(SourceCode sourceCode);
}

그리고 모든 사용자 정의 매퍼는 이 인터페이스를 구현한다.

1) OrikaConverter

Orika는 전체 API 구현을 허용하므로 매퍼 생성이 크게 간소화된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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);
    }
}

2) DozerConverter

Dozer에는 다음 섹션이 포함된 XML 매핑 파일이 필요하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<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 매핑을 정의한 후 코드에서 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);
    }
}

3) MapStructConverter

MapStruct 정의는 전적으로 코드 생성에 기반하기 때문에 매우 간단하다.

1
2
3
4
5
6
7
8
9
10
11
@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) JMapperConverter

JMapperConverter에는 더 많은 작업이 필요하다. 인터페이스를 구현한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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는 열거형 타입을 자체적으로 변환할 수 없으므로 사용자 지정 매핑 함수를 만들어야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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;
}

5) ModelMapperConverter

ModelMapperConverter를 사용하려면 매핑하려는 클래스만 제공하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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);
    }
}

4. 간단한 모델 테스트

성능 테스트를 위해 Java Microbenchmark Harness를 사용할 수 있다.

각 변환기 에 대해 BenchmarkMode를 Mode.All로 지정하여 별도의 벤치마크를 만들었다.

1) 평균 시간

JMH는 평균 실행 시간에 대해 다음과 같은 결과를 반환했다(짧을수록 좋음).

Framework NameAverage running time (in ms per operation)
MapStruct$$10^-5
JMapper$$10^-5
Orika0.001
ModelMapper0.002
Dozer0.004

이 벤치마크는 MapStruct와 JMapper가 모두 가장 좋은 평균 작업 시간을 가지고 있음을 분명히 보여준다.

2) 처리량

이 모드에서 벤치마크는 초당 작업 수를 반환한다. 다음과 같은 결과를 얻었다(결과가 많을수록 좋다).

Framework NameThroughput (in operations per ms)
MapStruct58101
JMapper53667
Orika1195
ModelMapper379
Dozer230

처리량 모드에서는 테스트된 프레임워크 중 MapStruct가 가장 빨랐고, JMapper가 그 뒤를 이었다.

3) SingleShotTime

이 모드에서는 단일 작업의 시작부터 끝까지 소요되는 시간을 측정할 수 있다. 벤치마크 결과는 다음과 같다(작을수록 좋다).

Framework NameSingle Shot Time (in ms per operation)
JMapper0.016
MapStruct1.904
Dozer3.864
Orika6.593
ModelMapper8.788

여기서는 JMapper가 MapStruct보다 더 나은 결과를 반환하는 것을 볼 수 있다.

4) SampleTime

이 모드에서는 각 작업의 시간을 샘플링할 수 있다. 세 가지 백분위수에 대한 결과는 다음과 같다.

  • Sample Time (in milliseconds per operation)
Framework Namep0.90p0.999p1.0
JMapper$$10^-40.0011.526
MapStruct$$10^-5$$10^-41.948
Orika0.0010.0182.327
ModelMapper0.0020.0443.604
Dozer0.0030.0885.382

모든 벤치마크는 시나리오에 따라 MapStruct와 JMapper가 모두 좋은 선택임을 보여주었다.

5. 실제 모델 테스트

성능 테스트를 위해 Java Microbenchmark Harness를 사용할 수 있다.

각 변환기 에 대해 BenchmarkMode를 Mode.All로 지정하여 별도의 벤치마크를 생성했다.

1) 평균 시간

JMH는 평균 실행 시간에 대해 다음과 같은 결과를 반환했습니다(짧을수록 좋음).

Framework NameAverage running time (in ms per operation)
Framework NameAverage running time (in ms per operation)
MapStruct$$10^-4
JMapper$$10^-4
Orika0.007
ModelMapper0.137
Dozer0.145

2) 처리량

이 모드에서 벤치마크는 초당 작업 수를 반환한다. 각 매퍼에 대해 다음과 같은 결과를 얻었다(결과가 많을수록 좋다).

Framework NameThroughput (in operations per ms)
JMapper3205
MapStruct3467
Orika121
ModelMapper7
Dozer6.342

3) SingleShotTime

이 모드에서는 단일 작업의 시작부터 끝까지 소요되는 시간을 측정할 수 있습니다. 벤치마크 결과는 다음과 같습니다(짧을수록 좋습니다).

Framework NameSingle Shot Time (in ms per operation)
JMapper0.722
MapStruct2.111
Dozer16.311
ModelMapper22.342
Orika32.473

4) SampleTime

이 모드에서는 각 작업의 시간을 샘플링할 수 있다. 샘플링 결과는 백분위수로 나뉘며, 세 가지 백분위수(p0.90, p0.999 , p1.00)에 대한 결과를 제시한다.

  • Sample Time (in milliseconds per operation)
Framework Namep0.90p0.999p1.0
JMapper$$10^-40.0011.526
MapStruct$$10^-4$$10^-41.948
Orika0.0010.0182.327
ModelMapper0.0020.0443.604
Dozer0.0030.0885.382

간단한 예제와 실제 예제의 정확한 결과는 분명히 달랐지만, 거의 비슷한 경향을 보인다. 두 예제 모두에서 JMapper와 MapStruct가 1위 자리를 두고 치열한 경쟁을 벌였다.

5) 결론

이 섹션에서 수행한 실제 모델 테스트를 바탕으로, JMapper가 가장 우수한 성능을 보였으며, MapStruct가 그 뒤를 바짝 쫓았다. 같은 테스트에서 Dozer는 SingleShotTime을 제외하고 결과 테이블 최하위에 꾸준히 위치하는 것을 확인할 수 있다.

[출처 및 참고]

This post is licensed under CC BY 4.0 by the author.