[Java/Spring] ModelMapper

2젼·2023년 8월 10일
1

Spring

목록 보기
1/1

Spring을 이용한 프로젝트를 진행하던 중에 ModelMapper를 사용하게 되어 이에 대해 한 번 정리해보려 합니다.

ModelMapper란?

ModelMapper

어떠한 Object에 있는 필드 값들을 원하는 Object에 자동으로 mapping 시켜주는 라이브러리이다.
즉, Source Object → Destination Object 를 자동으로 해준다고 생각하면 된다.

그렇다면, 어떤 경우에 ModelMapper를 사용할까?

보통 DTO를 통해 데이터를 받아 다른 Object(엔티티 등...)에 넣거나 이와 반대로 객체를 DTO로 변환할 때, getter와 setter를 이용해 원하는 필드들을 일일이 넣는다. 아니면, offrom이라는 정적 메서드(static method)를 만들어 사용한다.

이 작업들은 솔직히 귀찮은 일이고, 실수도 발생할 수 있다. 번거로운게 제일 큰 것 같다...

이러한 단점을 해결해줄 수 있는 것이 바로 ModelMapper 라이브러리이다!

ModelMapper 설정

ModelMapper 의존성 추가

우선 ModelMapper를 사용하기 위해 dependency를 추가해주자!

Maven 프로젝트의 경우, pom.xml에 다음 dependency를 추가하자.

<dependency>
  <groupId>org.modelmapper</groupId>
  <artifactId>modelmapper</artifactId>
  <version>3.0.0</version>
</dependency>

Gradle 프로젝트의 경우, build.gradle에 다음 dependency를 추가하자.

dependencies{
	...
	implementation 'org.modelmapper:modelmapper:3.0.0'
    ...
}

클래스 정의

매핑 예시를 설명하기 위해 아래와 같은 클래스들을 선언해주자.

Source Model

// 각 클래스에 Getter와 Setter, Constructor가 구현되어 있다고 가정하자.
class Order {
  Customer customer;
  Address billingAddress;
}

class Customer {
  Name name;
}

class Name {
  String firstName;
  String lastName;
}

class Address {
  String street;
  String city;
}

Destination Model

// Getter와 Setter, Constructor가 구현되어 있다고 가정하자.
class OrderDTO {
  String customerFirstName;
  String customerLastName;
  String billingStreet;
  String billingCity;
}

ModelMapper 사용하기

간단하게 다음과 같이 사용할 수 있다.

ModelMapper.map(Object source, Class<D> destinationType)

이를 이용해 order 인스턴스를 orderDTO에 mapping 시켜보자.

// Class에 값 넣기
Order order = new Order(
	new Customer(new Name("LastName", "FirstName")),
    new Address("Street", "City")
);

// ModelMapper 선언
ModelMapper mapper = new ModelMapper();

// mapping 하기
OrderDTO orderDto = mapper.map(order, OrderDTO.class);

실행해보면, 위와 같이 연관관계를 자동으로 판단해 필드값들이 mapping된 결과를 확인할 수 있다.

어떻게 동작하지?

이처럼, ModelMapper에서는 map(source, destination) 메서드가 호출되면 sourcedestination의 타입을 분석해 매칭 전략이나 기타 설정값들에 따라 일치하는 속성을 결정하게 된다.

위의 예시처럼, sourcedestination의 객체 타입이나 프로퍼티가 다른 경우에도 ModelMapper는 설정된 매칭 전략에 따라 최선의 mapping 과정을 수행한다.

하지만, ModelMappersourcedestination을 암묵적으로 일치시키기 위해 최선을 다함에도 속성 간의 매핑을 명시적으로 정의해야 하는 경우도 있다.

아래에서 이와 관련된 내용을 좀 더 자세하게 예시를 통해 알아보자.


TypeMap

아래와 같은 객체 매핑이 필요한 경우를 예시로 들어보자.

// Getter와 Setter, Constructor가 구현되어 있다고 가정하자.
class Item {
    private String name;
    private Integer stock;
    private Integer price;
    private Boolean sale;
}
class Bill {
    private String itemName;
    private Integer qty;
    private Integer singlePrice;
    private Double discount;
}
Item item = new Item("item", 10, 1500, true);
Bill bill = mapper.map(item, Bill.class);

처음의 예제보다 간단해 보이지만, mapper.map()의 결과는 다음과 같다.

ModelMapper의 기본 매칭 전략으로는 모호한 연관 관계들이 매핑되지 않는다.

이를 해결하기 위해, TypeMap을 제공한다.

TypeMap<S, D>

TypeMap 인터페이스를 구현해 ModelMapper 객체의 매핑 관계를 설정해줄 수 있다.

사용법은 아래와 같다.

TypeMap<BaseScr, BaseDest> typeMap = 
		modelMapper.createTypeMap(BaseScr.class, BaseDest.class)
        		.addMapping(BaseScr::getFirstName, BaseDest::setName);
typeMap.include(ScrA.class, DesTA.class);

addMapping() 메서드를 이용하여 sourcegetterdestinationsetter를 연결시켜주면 된다.

그럼 이를 이용해 위의ItemBill을 mapping 시켜보자.
우리가 원하는 매핑 전략은 다음과 같다.

  • Item.stockBill.qty
  • Item.priceBill.singlePrice
  • Item.saleBill.discount

수량가격의 경우는 아래와 같이 메서드 레퍼런스를 통해 간단히 설정이 가능하다.

mapper.typeMap(Item.class, Bill.class).addMappings(m -> {
	m.map(Item::getStock, Bill::setQty);
    m.map(Item::getPrice, Bill::setSinglePrice);
});

Bill bill2 = mapper.map(item, Bill.class);

위의 결과에서 볼 수 있듯 임의로 설정한 수량가격의 매핑 관계가 정상적으로 적용된 것을 확인할 수 있다.

하지만, Item.saleBill.discount와 같이 클래스 타입이 다른 경우에는 추가적인 방법이 필요하다.

파라미터 타입 변환 : Converter

매핑하고자 하는 sourcedestination의 타입이 다른 경우, Converter 인터페이스를 사용해 값을 설정해줄 수 있다.

위의 예제에서 Item.sale == true일 경우에 Bill.discount 값을 30.0으로 설정한다고 가정하자.

mapper.using(Converter<S, D>)의 패턴을 이용하면 유연한 타입 변환이 가능하다.

mapper.typeMap(Item.class, Bill.class).addMappings(m -> {
	m.map(Item::getStock, Bill::setQty);
    m.map(Item::getPrice, Bill::setSinglePrice);
    m.using((Converter<Boolean, Double>) ctx -> ctx.getSource() ? 30.0 : 0.0)
    	.map(Item::isSale, Bill::setDiscount);
});

Bill bill2 = mapper.map(item, Bill.class);

실행 결과는 아래와 같다.

Converter를 이용해 정상적으로 타입이 변환된 것을 확인할 수 있다.

매핑 skip

매핑을 원하지 않을 경우 skip을 통해 매핑이 이루어지지 않도록 설정할 수도 있다.
아래와 같이 사용할 수 있다.

typeMap.addMapping(mapper -> mapper.skip(Destination::setName));

위의 예시에서는 아래와 같이 사용할 수 있다.

mapper.typeMap(Item.class, Bill.class).addMappings(m -> {
	m.map(Item::getStock, Bill::setQty);
    m.map(Item::getPrice, Bill::setSinglePrice);
    m.using((Converter<Boolean, Double>) ctx -> ctx.getSource() ? 30.0 : 0.0)
    	.map(Item::isSale, Bill::setDiscount);
    m.skip(Bill::setItemName);
});

Null인 속성 값만 매핑 skip

객체에 새로운 값들을 한꺼번에 업데이트해줄 때, ModelMapper의 기본 매칭 전략을 사용하면 null 값까지 함께 업데이트되는 문제가 발생하기 때문에 이를 위해 매핑 설정을 할 수 있다.

ModelMapper mapper = new ModelMapper();
mapper.getConfiguration().setSkipNullEnabled(true);

Validation

ModelMapper는 기본적으로 매칭 전략에 맞지 않는 속성들은 null 값으로 초기화한다. 이때 개발하는 입장에선 어떤 객체에 대해 모든 속성이 올바르게 매핑되었는지 검증이 필요할 때가 있다.

이때 ModelMapper().validate()를 이용해 매핑 검증이 실패하는 경우 예외 처리를 해주기 때문에 추가적인 Exception Handling이 가능하다.

...
try{
	modelMapper.validate();
} catch(ValidationException e){
	// Exception Handling
}
...

Strategies

앞서 설정한 여러 가지 조건들에 의해 ModelMapper는 지능적인 Object Mapping을 수행한다.

아래와 같이 STANDARD, LOOSE, STRICT 전략이 있다.

modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STANDARD);
modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.LOOSE);
modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);

STANDARD

가장 기본적인 매칭 전략으로써, STANDARD 전략을 사용하면 sourcedestination의 속성들을 지능적으로 매칭시킬 수 있다.

  • 토큰은 어떤 순서로든 일치될 수 있다.
  • 모든 destination 속성 이름 토큰이 일치해야 한다.
  • 모든 source 속성 이름은 일치하는 토큰이 하나 이상 있어야 한다.

위 조건들을 충족하지 못하는 경우 매칭에 실패하여 null을 반환한다.

LOOSE

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

  • 토큰은 어떤 순서로든 일치될 수 있다.
  • 마지막 destination 속성 이름에는 모든 토큰이 일치해야 한다.
  • 마지막 source 속성 이름은 일치하는 토큰이 하나 이상 있어야 한다.

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

→ 처음 예시의 Order, OrderDTO와 같이 객체의 속성이 계층 구조를 가지는 경우에 효과적이다.

STRICT

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

  • 토큰들은 엄격한 순서로 일치해야 한다.
  • 모든 destination 속성 이름 토큰이 일치해야 한다.
  • 모든 source 속성 이름에는 모든 토큰이 일치해야 한다.

STRICT 전략을 통해 앞서 다룬 TypeMap을 사용하지 않고도 모호함이나 예기치 않은 매핑이 발생하지 않도록 하는 경우에 간편하게 사용이 가능하다. 하지만, 반드시 매칭되어야 하는 속성의 이름들이 서로 정확하게 일치해야 한다!


References

profile
안녕하세요:)

0개의 댓글