30. ModelMapper 적용
http://modelmapper.org/
>> 객체의 프로퍼티를 다른 객체의 프로퍼티로 매핑해주는 유틸리티
1. 의존성 추가
2. 토크나이저 설정
의존성 추가하기
버전 관리 해주는 의존성 추가
spring-boot-starter-data-jpa
버전 관리 안해주는 의존성 추가
ModelMapper: version을 명시해주어야 함
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>2.3.6</version>
</dependency>
gradle
// model mapper
implementation group: 'org.modelmapper', name: 'modelmapper', version: '2.3.6'
토크나이저 설정하기
UNDERSCORE(_)를 사용했을 때에만 nested 객체를 참조하는 것으로 간주하고 그렇지 않은 경우에는 해당 객체의 직속 프로퍼티에 바인딩한다
// ModelMapper
@Bean
public ModelMapper modelMapper() {
ModelMapper modelMapper = new ModelMapper();
modelMapper.getConfiguration()
.setDestinationNameTokenizer(NameTokenizers.UNDERSCORE)
.setSourceNameTokenizer(NameTokenizers.UNDERSCORE);
return modelMapper;
}
실습
1. ModelMapper 빈 생성
AppConfig.java
@Configuration
public class AppConfig {
//...
@Bean
public ModelMapper modelMapper(){
return new ModelMapper();
}
}
2. map() 메소드를 통해 변경된 Data를 Model객체에 적용
ModelMapper.map()
public void map(Object source, Object destination) {
Assert.notNull(source, "source");
Assert.notNull(destination, "destination");
mapInternal(source, destination, null, null);
}
AccountService.java
@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class AccountService implements UserDetailsService {
// ...
private final ModelMapper modelMapper;
// ...
public void updateProfile(Account account, Profile profile) {
// source : profile -> destination : account
// account정보 변경, profile과 account의 변수가 매핑(이름 일치)이 되는 것만 적용됨
modelMapper.map(profile, account);
// account.setUrl(profile.getUrl());
// account.setOccupation(profile.getOccupation());
// account.setLocation(profile.getLocation());
// account.setBio(profile.getBio());
// account.setProfileImage(profile.getProfileImage());
accountRepository.save(account); // save : 기존 데이터에 merge를 시킴 -> update발생
// TODO : 문제가 하나 더 남음 (프로필 이미지 변경할 때 발견)
}
public void updateNotifications(Account account, Notifications notifications) {
// StudyCreatedByWeb -> modelMapper입장에서는 Nested객체와 혼동함
modelMapper.map(notifications, account);
// account.setStudyCreatedByWeb(notifications.isStudyCreatedByWeb());
// account.setStudyCreatedByEmail(notifications.isStudyCreatedByEmail());
// account.setStudyEnrollmentResultByWeb(notifications.isStudyEnrollmentResultByWeb());
// account.setStudyEnrollmentResultByEmail(notifications.isStudyEnrollmentResultByEmail());
// account.setStudyUpdatedByWeb(notifications.isStudyUpdatedByWeb());
// account.setStudyUpdatedByEmail(notifications.isStudyUpdatedByEmail());
accountRepository.save(account);
}
}
주석 처리된 코드들이 map()메소드 하나로 다 처리된다.
profile에 속한 변수들 중 변경된 값들이 이름이 매칭되는 account변수에 적용되어 코드를 간결하게 줄일 수 있다.
updateNotifications 에러 발생
그런데 updateNotifications같은 경우에는 에러가 발생한다.
이유는 ModelMapper에서는 다양한 형태의 객체를 매핑해주는데 notifications의 변수가 'StudyCreatedByEmail'와 같이 애매하게 되어있으므로 Nested객체와 혼동할 수 있다 (ex. study.created.~)
실제로 해당 위의 코드로 진행했을 때 아래와 같이 에러가 발생하였다.
ModelMapper가 StudyCreatedByEmail와 같은 변수 이름을 제대로 찾지 못하고 account의 Email 설정으로 착각하고 있는 것이다.
해결하기 위해서는 토크나이저 설정을 해주면 된다.
' NameTokenizers._UNDERSCORE '
이렇게 설정하면 modelMapper는 변수가 UNDERSCORE(ex. study_created)가 아닌 이상 하나의 변수(프로퍼티)로 간주하게 된다
@Configuration
public class AppConfig {
@Bean
public ModelMapper modelMapper(){
ModelMapper modelMapper = new ModelMapper();
modelMapper.getConfiguration()
.setDestinationNameTokenizer(NameTokenizers.UNDERSCORE)
.setSourceNameTokenizer(NameTokenizers.UNDERSCORE);
return modelMapper;
}
}
이렇게하면 updateNotificatios()메소드도 정상적으로 작동이 된다
DTO 생성자 단축하기
DTO의 생성자도 ModelMapper를 통해 단축시킬 수 있다
프로필 DTO
package com.study.Jpawebapp.settings;
import com.study.Jpawebapp.domain.Account;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;
/**
* setting form을 채울 Data (DTO)
*/
@Data
//@NoArgsConstructor
public class Profile {
// ...
// public Profile(Account account) {
// this.bio = account.getBio();
// this.url = account.getUrl();
// this.occupation = account.getOccupation();
// this.location = account.getLocation();
// this.profileImage = account.getProfileImage();
// }
}
알림 DTO
package com.study.Jpawebapp.settings;
import com.study.Jpawebapp.domain.Account;
import lombok.Data;
import lombok.NoArgsConstructor;
// 알림 DTO
@Data
//@NoArgsConstructor
public class Notifications {
// public Notifications(Account account) {
// this.studyCreatedByEmail = account.isStudyCreatedByEmail();
// this.studyCreatedByWeb = account.isStudyCreatedByWeb();
// this.studyEnrollmentResultByEmail = account.isStudyEnrollmentResultByEmail();
// this.studyEnrollmentResultByWeb = account.isStudyEnrollmentResultByWeb();
// this.studyUpdatedByEmail = account.isStudyUpdatedByEmail();
// this.studyUpdatedByWeb = account.isStudyUpdatedByWeb();
// }
}
위의 주석처리된 코드를 Controller단에서 가볍게 처리할 수 있다
* DTO클래스는 빈이 아니기 때문에 ModelMapper를 주입할 수 는 없기 때문에 controller단에서 처리
@Controller
@RequiredArgsConstructor
public class SettingsController {
// ...
private final ModelMapper modelMapper;
@GetMapping(SETTINGS_PROFILE_URL)
public String updateProfileForm(@CurrentUser Account account, Model model){
model.addAttribute(account);
// ModelMapper // new Profile(account);
model.addAttribute(modelMapper.map(account, Profile.class));
return SETTINGS_PROFILE_VIEW_NAME;
}
/
// Password 변경 페이지
@GetMapping(SETTINGS_PASSWORD_URL)
public String updatePasswordForm(@CurrentUser Account account, Model model) {
model.addAttribute(account);
model.addAttribute(new PasswordForm());
return SETTINGS_PASSWORD_VIEW_NAME;
}
//...
@GetMapping(SETTINGS_NOTIFICATIONS_URL)
public String updateNotificationsForm(@CurrentUser Account account, Model model) {
model.addAttribute(account);
// ModelMapper // new Notifications(account)
model.addAttribute(modelMapper.map(account, Notifications.class));
return SETTINGS_NOTIFICATIONS_VIEW_NAME;
}
}
참고
'Dot Programming > Spring Clone' 카테고리의 다른 글
[스프링 웹앱 프로젝트 #32] 패스워드를 잊어버렸습니다 (0) | 2021.03.09 |
---|---|
[스프링 웹앱 프로젝트 #31] 닉네임 수정 (0) | 2021.03.09 |
[스프링 웹앱 프로젝트 #29] 알림 설정 (0) | 2021.03.05 |
[스프링 웹앱 프로젝트 #27 #28] 패스워드 수정 및 테스트 (0) | 2021.03.04 |
[스프링 웹앱 프로젝트 #26] 프로필 이미지 변경 (0) | 2021.03.04 |