[Java/ModelMapper] ModelMapper사용법 총정리

Juseong Han·2024년 7월 30일
1

ModelMapper?

Object의 값들을 자동으로 매핑시켜주는 라이브러리이다.
이를테면 보통 DTO와 Entity간의 데이터 전달을 하기 위해서는 통상적으로 생성자를 통해 직접 값을 매핑하거나 일일히 setter method를 사용해야했다.

물론 생성자를 사용하면 간단히 해결 가능하겠지만 DTO가 여러개의 Entity로부터 데이터를 받아야한다면? 혹은 매핑할 필드의 개수가 수십개가 넘어간다면 매우 많고 긴 생성자를 작성하다가 현타가 와서 키보드를 부숴버릴 수도 있다.

이럴 때 ModelMapper 라이브러리를 사용하면 된다.

build.gradle

사용하기 위해선 의존성주입이 필요하다

implementation('org.modelmapper:modelmapper:2.4.4')

Bean 등록

나는 개인적으로 Bean으로 등록해서 사용하는 것을 좋아한다. 싱글톤으로 생성되어 공통적으로 매핑전략을 공유할 수 있기 때문

@Configuration
public class CommonBeanConfiguration {
	public ModelMapper modelMapper() {
    	return new ModelMapper();
    }
}

기본적인 사용법

을 알기전에 ModelMapper가 어떻게 매핑을하는지 설정 구성을 먼저 알고가자

ModelMapper 설정구성

공식문서에 따르면 ModelMapper는 우선 필드의 이름을 토큰화하고 출발지에서 토큰화된 name이 목적지에 존재하는지의 여부를 판단하여 매핑한다.

Token은 default로 CamelCase규칙을 따른다. 이를테면
userName 은 user, name 두 개의 토큰으로 쪼개질 수 있다.

이렇게 쪼개진 토큰은 매핑전략(Strategy)에 따라 복사되는데 ModelMapper의 전략구성은 다음과 같다.

  1. MatchingStrategies.STANDARD

    기본 매칭 전략으로서 STANDARD 전략을 사용하면 source와 destination 의 속성들을 지능적으로 매치시킬 수 있다.

  • 토큰은 어떤 순서로든 일치될 수 있다.

  • 모든 destination 속성 이름 토큰이 일치해야 한다.

  • 모든 source 속성 이름은 일치하는 토큰이 하나 이상 있어야 한다.

    위 조건들을 충족하지 못하는 경우 매칭에 실패하게 된다. (null)

  1. MatchingStrategies.LOOSE

    느슨한 매칭 전략으로서 LOOSE 전략을 사용하면 계층 구조의 마지막 destination 속성만 일치하도록 요구하여 source와 destination 을 느슨하게 매치시킬 수 있다.

  • 토큰은 어떤 순서로든 일치될 수 있다.

  • 마지막 destination 속성 이름에는 모든 토큰이 일치해야 한다.

  • 마지막 source 속성 이름은 일치하는 토큰이 하나 이상 있어야 한다.

    느슨한 일치 전략은 속성 계층 구조가 매우 다른 source, destination 객체에 사용하는 데에 이상적이다.

  1. STRICT

    엄격한 일치 전략으로서 STRICT 전략을 사용하면 source 속성을 destination 속성과 엄격하게 일치시킬 수 있다. 따라서 불일치나 모호성이 발생하지 않도록 완벽한 일치 정확도를 얻을 수 있다. 하지만 source 와 destination 의 속성 이름들이 서로 정확하게 일치해야 한다.

  • 토큰들은 엄격한 순서로 일치해야 한다.

  • 모든 destination 속성 이름 토큰이 일치해야 한다.

  • 모든 source 속성 이름에는 모든 토큰이 일치해야 한다.

    STRICT 전략은 반드시 매칭되어야 하는 속성의 이름들이 서로 정확하게 일치해야 한다.

사용 방법

다음은 기본적인 사용법, map(Object o, Class<?> clazz) 메소드를 사용한다.

import org.modelmapper.ModelMapper;

public class Main {
    public static void main(String[] args) {
        ModelMapper modelMapper = new ModelMapper();
        
        Source source = new Source("John", "Doe");
        Destination destination = modelMapper.map(source, Destination.class);
        
        System.out.println(destination.getFirstName());  // John
        System.out.println(destination.getLastName());   // Doe
    }
}

class Source {
    private String firstName;
    private String lastName;

    // getter, setter, 생성자 생략
}

class Destination {
    private String firstName;
    private String lastName;

    // getter, setter, 생성자 생략
}

매핑 방법 정의

ModelMapper는 다양한 매핑 방법을 제공한다. 기본적으로는 필드명이 같은 경우 자동으로 매핑되긴하지만, 매핑 전략을 세부적으로 설정하려면 다음 옵션들을 활용할 수 있다.

  • Field Matching Enabled: 필드명을 기준으로 매핑할지 여부를 설정한다.
modelMapper.getConfiguration().setFieldMatchingEnabled(true);
  • Field Access Level: 접근 제한자 수준을 설정하여 private 필드에 접근할 수 있도록 한다.
modelMapper.getConfiguration().setFieldAccessLevel(Configuration.AccessLevel.PRIVATE);
  • Implicit Mapping: 암시적 매핑을 활성화하거나 비활성화한다. 기본값은 활성화 상태이다.
modelMapper.getConfiguration().setImplicitMappingEnabled(true);
  • Destination Name Transformer: 대상 객체의 필드명을 변환하는 규칙을 정의한다.
modelMapper.getConfiguration().setDestinationNameTransformer(name -> "prefix" + name);
  • Source Name Transformer: 소스 객체의 필드명을 변환하는 규칙을 정의한다.
modelMapper.getConfiguration().setSourceNameTransformer(name -> "prefix" + name);

매핑 조건 설정 예시

다음은 다양한 매핑 방법을 적용한 예시이다.

ModelMapper modelMapper = new ModelMapper();

// 필드 매칭과 접근 수준 설정
modelMapper.getConfiguration()
    .setFieldMatchingEnabled(true)
    .setFieldAccessLevel(Configuration.AccessLevel.PRIVATE);

// 암시적 매핑 설정
modelMapper.getConfiguration().setImplicitMappingEnabled(false);

// 소스와 대상 필드명 변환 규칙 설정
modelMapper.getConfiguration()
    .setSourceNameTransformer(name -> "source_" + name)
    .setDestinationNameTransformer(name -> "dest_" + name);

커스텀 매핑

  • 커스텀 매핑이 필요한 경우, PropertyMap을 사용하여 매핑 규칙을 정의할 수 있다.
PropertyMap<Source, Destination> sourceToDestinationMap = new PropertyMap<Source, Destination>() {
    protected void configure() {
        map().setFirstName(source.getGivenName());
        map().setLastName(source.getSurname());
    }
};
modelMapper.addMappings(sourceToDestinationMap);

TypeToken을 활용한 List 매핑

  • List와 같은 제네릭 타입을 매핑할 때는 TypeToken을 사용한다.
List<Source> sourceList = new ArrayList<>();
sourceList.add(new Source("John", "Doe"));
sourceList.add(new Source("Jane", "Doe"));

Type listType = new TypeToken<List<Destination>>() {}.getType();
List<Destination> destinationList = modelMapper.map(sourceList, listType);

destinationList.forEach(destination -> {
    System.out.println(destination.getFirstName() + " " + destination.getLastName());
});

같은 클래스 매핑 시 메모리 참조 주의사항

ModelMapper를 사용할 때, 같은 클래스로 매핑을 수행하면 원본 객체와 대상 객체가 동일한 메모리 참조를 공유하게 된다. 이는 원본 객체의 변경이 대상 객체에도 영향을 미치게 되어 의도치 않은 동작을 유발할 수 있다. 따라서 같은 클래스로 매핑을 수행할 때는 주의가 필요하다.

아래는 예시

public class Main {
    public static void main(String[] args) {
        ModelMapper modelMapper = new ModelMapper();
        
        Source source = new Source("John", "Doe");
        Source target = modelMapper.map(source, Source.class);
        
        target.setFirstName("Jane");

        // 원본 객체도 변경됨 ㅋㅋ
        System.out.println(source.getFirstName());  // Jane
        System.out.println(target.getFirstName());  // Jane
    }
}

class Source {
    private String firstName;
    private String lastName;

    // getter, setter, 생성자 생략
}
  • 이러한 문제를 방지하기 위해, 같은 클래스로 매핑을 수행할 때는 별도의 객체를 생성하거나 깊은 복사를 수행하는 방법을 고려해야 한다. (뭐 reflection을 쓰던지...)

결론

  • 이와같이 ModelMapper를 사용하면 객체 간의 매핑을 쉽게 수행할 수 있어 개발 생산성을 높일 수 있다. 특히, 복잡한 매핑 로직을 단순화하고, 일관된 매핑 전략을 적용하여 코드의 가독성을 향상시킬 수 있다.
profile
개발이 하고 싶어요💻 개발이 너무 재밌는 Juseong입니다.🖐

0개의 댓글

관련 채용 정보