infinity : 무한한 성장가능성

ModelMapper 에 대해 알아보자 본문

Develop/💜Java

ModelMapper 에 대해 알아보자

인피니 2025. 1. 5. 18:28

modelMapper을 알아보게 된 배경에 대해 설명하자면 

어느 날 주말에 갑자기 잘 돌아가던 배치에서 실패메시지가 왔다.

해당 부분에 변경사항이 없을 텐데 왜 실패가 되었지? 싶어 로그를 확인해 보니 

500 에러가 있었다... 🤔

500 에러를 넘겨준 api 서버에 가서 확인해보니 해당 요청에 대한 처리는 정상적으로 된 것을 확인할 수 있었다.

그래서 해당 배치에서 처리되어야 하는 주문의 상태값을 보니 500 에러로 처리되지 않았다고 뜬 주문번호에 대한 주문상태도 잘 처리된 것을 확인할 수 있었다.

 

위 현상을 기반으로 왜 500에러가 발생했는지 생각해 보았고, 처음 예측한 원인은 아래와 같다.

DB 세션 수를 줄이기위해 was 서버 몇 개를 반납하여 배포 스크립트 수정을 하는 pr을 봤었고, was 서버를 줄여 해당 시간의 트래픽을 줄인 was 수로는 시간지연이 발생하나? 였다.

was 반납을 진행하신 분께 여쭤보니, was 반납은 이미 몇달전 진행되었다는 것이다.

 

그럼 무엇인가? 다시 에러로그를 살펴보니 modelMapper 쪽에서 오래 걸린듯한 정황이 의심되는 것들이 보였다.

(관련 내용들을 찍어둘걸 ㅠ 블로그 작성시점에 로그를 확인해보려고 하니, 보관기간이 지나 확인을 할 수가 없다 ㅠ0ㅠ)

modelMapper에서 시간지연이 발생할 수 있나? 찾아보기 전 modelMapper을 어떤 식으로 사용하는지 찾아보았다.

사진 1

typeMap 메서드를 통해 타입을 미리 정의하고 사용하는 것을 볼 수 있었다.

 

시간 지연이 발생한 곳에서 사용하는 객체 매핑은 typeMap을 통해 미리 정의하지 않고 사용했는데 typeMap 을 통해 객체 타입을 미리 정의하고 사용하도록 수정하여 시간 지연 문제를 해결할 수 있었다.

typeMap() 메서드에 대해 알아보기 전 TypeMap 클래스에 대해 먼저 알아보고 typeMap 메서드 또한 알아보자 

 

👉 TypeMap 이란?


1. 소스 타입과 타켓 타입 간의 매핑을 정의하고 설정하는 역할을 합니다.

2. typeMap 을 사용할 경우 소스 객체필드와 타겟 객체의 필드 간 매핑을 세밀하게 설정할 수 있습니다. 

3. 기본적으로 modelMapper 은 이름이 동일한 필드를 자동으로 매핑하지만 TypeMap을 사용하면 다른 이름을 가진 필드 or 복잡한 매핑 규칙을 설정할 수 있다. 

4. 특정 필드만 매핑하거나, 일부 필드를 제외하고 매핑할 수 있습니다.

 

📝 서로 다른 이름을 가진 필드의 매핑 규칙 설정

import org.modelmapper.ModelMapper;
import org.modelmapper.TypeMap;

public class TypeMapExample {
    public static void main(String[] args) {
        ModelMapper modelMapper = new ModelMapper();

        // TypeMap 설정: Source -> Destination
        TypeMap<Source, Destination> typeMap = modelMapper.createTypeMap(Source.class, Destination.class);

        // 필드 이름이 다를 경우 매핑 규칙을 설정
        typeMap.addMappings(mapper -> mapper.map(Source::getName, Destination::setFullName));

        Source source = new Source("John", 30);
        Destination destination = modelMapper.map(source, Destination.class);

        System.out.println(destination.getFullName());  // John
        System.out.println(destination.getAge());      // 30
    }
}

class Source {
    private String name;
    private int age;

    public Source(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

class Destination {
    private String fullName;
    private int age;

    public String getFullName() {
        return fullName;
    }

    public void setFullName(String fullName) {
        this.fullName = fullName;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

(* gpt 가 알려준 코드입니다.)

위 코드를 보면 Source 의 name 필드를 Destiniation의 fullName에 매핑하고 싶은데 필드 이름이 다르기 때문에 typeMap을 사용해 

위 필드가 매핑되도록 설정해준 것을 확인해 볼 수 있습니다.

 

📝 복잡한 매핑 규칙 적용

TypeMap<Source, Destination> typeMap = modelMapper.createTypeMap(Source.class, Destination.class);

typeMap.addMappings(mapper -> {
    // 소스의 나이를 2배로 변환하여 타겟 객체에 설정
    mapper.map(source -> source.getAge() * 2, Destination::setAge);
});

(* gpt 가 알려준 코드입니다.)

위 코드에서는 Source 을 Destination 클래스로 매핑 시 Source의 나이를 2배하여 매핑하는 규칙을 적용한 것을 확인할 수 있습니다.

 

📝 특정 필드만 매핑하거나 제외하기

TypeMap<Source, Destination> typeMap = modelMapper.createTypeMap(Source.class, Destination.class);

typeMap.addMappings(mapper -> {
    mapper.skip(Destination::setAge);  // Destination의 age 필드는 매핑하지 않음
});

(* gpt 가 알려준 코드입니다.)

 

위 코드를 자세히 보면 gpt 는 예시로 createTypeMap 메서드를 사용해 TypeMap 타입을 사용하는 예제를 보여줬는데 

맨 위에 첫 코드 캡처 부분을 보면 modelMapper.typeMap() 메서드를 사용하는 것을 볼 수 있다. 

또한 특별히 매핑 규칙을 정의하지 않은 것을 볼 수 있는데 

먼저 typeMap() 와 createTypeMap() 메서드의 차이를 확인해 보고 

typeMap() 메서드를 커스텀 매핑 규칙을 정의하지 않고 사용한 이유를 확인해 보자 

👉 modelMapper.typeMap() vs modelMapper.createTypeMap()


📝 typeMap()

  /**
   * Returns the TypeMap for the {@code sourceType}, {@code destinationType}, creates TypeMap
   * automatically if none exists.
   *
   * @param <S> source type
   * @param <D> destination type
   * @throws IllegalArgumentException is {@code sourceType}, {@code destinationType} are null
   */
  public <S, D> TypeMap<S, D> typeMap(Class<S> sourceType, Class<D> destinationType) {
    Assert.notNull(sourceType, "sourceType");
    Assert.notNull(destinationType, "destinationType");
    return config.typeMapStore.getOrCreate(null, sourceType, destinationType, null, engine);
  }

 

typeMap 메서드가 정의된 코드를 그대로 가져왔습니다. 

메서드에 대한 설명을 보면 typeMap 메서드 사용시 SourceType & DestinationType 가 정의된 TypeMap 타입이 없는 경우 

TypeMap 타입을 만들고 해당하는 값을 반환해줍니다.반환해 줍니다. 이미 정의된 TypeMap 이 있다면 해당 값을 반환해 줍니다. 

 

📝 createTypeMap()

  /**
   * Creates a TypeMap for the {@code source}'s type and {@code destinationType} identified by the
   * {@code typeMapName} using the ModelMapper's configuration. Useful for creating TypeMaps for
   * generic source data structures.
   * 
   * @param <S> source type
   * @param <D> destination type
   * @param source
   * @param destinationType
   * @param typeMapName
   * @throws IllegalArgumentException if {@code source}, {@code destinationType} or
   *           {@code typeMapName} are null
   * @throws IllegalStateException if a TypeMap already exists for {@code source}'s type,
   *           {@code destinationType} and {@code typeMapName}
   * @throws ConfigurationException if the ModelMapper cannot create the TypeMap
   * @see #getTypeMap(Class, Class, String)
   */
  public <S, D> TypeMap<S, D> createTypeMap(S source, Class<D> destinationType, String typeMapName) {
    return this.<S, D>createTypeMap(source, destinationType, typeMapName, config);
  }

createTypeMap() 메서드의 설명을 보면 TypeMap 을 생성하는데 이미 정의된 타입이 있는 경우 IllegalStateException 예외가 발생하는 것을 확인할 수 있습니다. 

 

즉 typeMap() 과 createTypeMap() 메서드 모두 source, destination에 해당하는 typeMap 이 없는 경우 매핑 규칙을 새로 생성하지만, 이미 존재하는 경우 typeMap() 은 이미 정의된 규칙을 반환하고 createTypeMap()는 예외를 던진다는 것을 확인할 수 있습니다.

 

👉 커스텀 매핑 규칙을 정의하지 않고 modelMapper.typeMap() 을 사용한 이유  


 기본적으로 modelMapper.map() 을 사용해 객체 매핑을 진행할 경우 동적으로 매핑을 처리하게 됩니다.

즉  런타임에 객체 매핑을 해야할때 동적으로 어떤 필드에 어떤 객체가 매핑되어야 할지 동적으로 결정해 매핑을 수행하게 되는데

이렇게 될 경우 같은 규칙을 사용해 객체간 매핑이 이뤄질 때도 매번 객체의 필드와 타입을 확인하여 변환 규칙을 찾아야 합니다.

 

modelMapper.typeMap()을 통해 매핑이 이뤄질 객체 타입을 미리 정의한 경우 런타임에 규칙을 다시 계산할 필요 없이 정의된 규칙을 바로 사용할 수 있습니다. 즉 한번 설정한 매핑 규칙을 재사용하여 매번 계산을 반복하지 않아 성능상 유리하게 됩니다.

 

한계는 modelMapper을 통해 TypeMap을 정의하는 것이 많아질 경우 애플리케이션 로딩시간이 매우 느려지게 됩니다.

한계를 해결하기 위해서는 MapStruct로 변경하면 애플리케이션 로딩시점이 느려지는 것을 보완할 수 있습니다.

 

 

-> 다음 포스팅으로는 MapStruct에 대해 알아보도록 하겠습니다. 

 

 

포스팅에서 궁금한 점이나 틀린 부분이 보인다면 편하게 댓글 달아주시면 감사하겠습니다. 😀 

 

 

 

 

'Develop > 💜Java' 카테고리의 다른 글

MapStruct 에 대해 알아보자  (0) 2025.01.19
자바 제네릭 완전정복 (2)  (0) 2024.02.18
자바 제네릭 완전정복 (1)  (1) 2024.01.21