이 포스팅의 코드 및 정보들은 강의를 들으며 정리한 내용을 토대로 작성한 것이 일부 존재합니다.
프로필 정보를 수정해서 회원 정보로 저장하는 로직이 있다고 하자. 수정하려는 회원 프로필 정보인 Profile이라는 DTO가 있고, 이를 Account라는 엔티티에 데이터를 옮기는 상황이다.
// ModelMapper 설정 소스 - 기본값 상태
@Configuration
public class AppConfig {
...
@Bean
public ModelMapper modelMapper() {
return new ModelMapper();
}
}
public void updateProfile(Account account, Profile profile) {
account.setUrl(profile.getUrl());
account.setOccupation(profile.getOccupation());
account.setLocation(profile.getLocation());
account.setBio(profile.getBio());
account.setProfileImage(profile.getProfileImage());
}
이렇게 일일이 등록하게 되면 코드의 양도 길어지는 건 그렇다고 해도 눈이 아플 것이다...
한 번에 값을 세팅해줄 수 있는 API 중에서 ModelMapper라는 것이 있다. 그중 map(Object source, Object destination)
을 사용할 것이다.
먼저 ModelMapper의 공식문서에 나온 프로토타입은 다음과 같다.
/**
* Maps source to destination. Mapping is performed according to the corresponding
* TypeMap. If no TypeMap exists for source.getClass() and destination.getClass()
* then one is created.
*
* @param source object to map from
* @param destination object to map to
* @throws IllegalArgumentException if source or destination are null
* @throws ConfigurationException
* if the ModelMapper cannot find or create a TypeMap for the arguments
* @throws MappingException if an error occurs while mapping
*/
public void map(Object source, Object destination) {
Assert.notNull(source, "source");
Assert.notNull(destination, "destination");
mapInternal(source, destination, null, null);
}
소스를 대상에 매핑합니다. 매핑은 해당 TypeMap에 따라 수행됩니다. source.getClass() 및 destination.getClass()에 대한 TypeMap이 없으면 하나가 생성됩니다.
map(Object source, Object destination)
에서 source에 있는 데이터를 destination으로 복사한다고 생각하면 된다.
이를 적용한 결과는 다음과 같다.
// modelMapper는 의존성 주입을 통해 가져옴
public void updateProfile(Account account, Profile profile) {
modelMapper.map(profile, account);
}
물론 여기서 Profile에 있는 프로퍼티들이 매핑이 되어있어야 하며, url, occupation, location, bio, profileImage 등의 이름도 Account에 있던 프로퍼티들 중에 같은 이름으로 존재하는 것들이 있어야 한다.
만약 매핑 시켜야 할 url, occupation, location, bio, profileImage 등의 이름들이 Profile과 Account가 서로 다르다면 modelMapper 입장에서 어떻게 잘라서 읽어야되는 건지, 어떠한 패턴에 맞춰서 프로퍼티를 복사해야 되는 건지에 대한 설정이 필요하다.
NamingConventions.NONE
Represents no naming convention, which applies to all property names
기본 설정으로는 모든 Tokenizer와 네이밍 패턴이 다 적용이 되어 있다.
public void updateNotifications(Account account, Notifications notifications) {
account.setxxxCreatedByEmail(notifications.isxxxCreatedByEmail());
account.setxxxCreatedByWeb(notifications.isxxxCreatedByWeb());
account.setxxxEnrollmentResultByEmail(notifications.isxxxEnrollmentResultByEmail());
account.setxxxResultByWeb(notifications.isxxxEnrollmentResultByWeb());
account.setxxxUpdatedByEmail(notifications.isxxxUpdatedByEmail());
account.setxxxUpdatedByWeb(notifications.isxxxUpdatedByWeb());
}
xxxCreatedByEmail 이런 이름이라면 xxx . createdBy . email 혹은 xxx 안에 들어있는 createdBy email 이런 식으로 매핑되어서 ModelMapper가 잘 이해를 못할 수 있고, 실제로도 그런 상황이 벌어질 수 있다.
즉, Notifications에 있는 프로퍼티 이름을 Account에 같은 이름이 있음에도 불구하고 ModelMapper가 잘 못찾아 매핑을 못 시키는 것이다.
이런 경우에는 ModelMapper에 토크나이저 설정을 추가해야 한다.
modelMapper.getConfiguration()
.setSourceNameTokenizer(NameTokenizers.UNDERSCORE)
.setDestinationNameTokenizer(NameTokenizers.UNDERSCORE)
위의 소스에서는 언더스코어()를 안 썼기 때문에 setDestinationNameTokenizer()
와 setSourceNameTokenizer()
NameTokenizers를 통한 인수값으로 NameTokenizers.UNDERSCORE를 넣었다. 를 사용하여 지은 이름 외에는 다 하나의 프로퍼티로 통째로 처리하게 된다. 일단 xxxCreatedByEmail
이런 식의 카멜 케이스 표기법과 bio, url 등의 간단한 이름들에 대해서는 이름 그대로 매핑 작업이 되는 것이다.
즉, UNDERSCORE(_)를 사용했을 때에만 nested 객체를 참조하는 것으로 간주하고 그렇지 않은 경우에는 해당 객체의 직속 프로퍼티에 바인딩 한다.
/**
* 생성자를 직접 호출해서 만든 객체들이기 때문에 Bean이 아님
*/
@Data
@NoArgsConstructor
public class Notifications {
private boolean xxxCreatedByEmail;
private boolean xxxCreatedByWeb;
private boolean xxxEnrollmentResultByEmail;
private boolean xxxEnrollmentResultByWeb;
private boolean xxxUpdatedByEmail;
private boolean xxxUpdatedByWeb;
public Notifications(Account account) {
this.xxxCreatedByEmail = account.isxxxCreatedByEmail();
this.xxxCreatedByWeb = account.isxxxCreatedByWeb();
this.xxxEnrollmentResultByEmail = account.isxxxEnrollmentResultByEmail();
this.xxxEnrollmentResultByWeb = account.isxxxEnrollmentResultByWeb();
this.xxxUpdatedByEmail = account.isxxxUpdatedByEmail();
this.xxxUpdatedByWeb = account.isxxxUpdatedByWeb();
}
}
엔티티, DTO 클래스에는 ModelMapper를 이용하여 빈을 주입받아서 값을 매핑하기에는 어려움이 있다. 이런 클래스들은 생성자를 직접 호출해서 만든 객체들이기 때문에 Bean이 아니 때문일 것이다.
이 경우에는 생성해뒀던 생성자를 없애고 Controller 레이어에서 ModelMapper 객체를 생성하여 Bean을 주입받아서 map(Object source, Class<D> destinationType)
로 매핑하면 된다.
// ModelMapper 빈 주입
...
public void updateProfileForm(Account account) {
...
modelMapper.map(account, Profile.class);
}
Profile 타입의 인스턴스가 만들어지고, 거기에 해당되는 account에 들어있는 데이터로 채워질 것이다.
Reference