본문 바로가기

Dot Programming/Spring Clone

[스프링 웹앱 프로젝트 #30] ModelMapper적용

30. ModelMapper 적용

http://modelmapper.org/
   >> 객체의 프로퍼티를 다른 객체의 프로퍼티로 매핑해주는 유틸리티

1. 의존성 추가 
2. 토크나이저 설정
 

ModelMapper - Simple, Intelligent, Object Mapping.

Why ModelMapper? The goal of ModelMapper is to make object mapping easy, by automatically determining how one object model maps to another, based on conventions, in the same way that a human would - while providing a simple, refactoring-safe API for handli

modelmapper.org

 

의존성 추가하기

버전 관리 해주는 의존성 추가

 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변수에 적용되어 코드를 간결하게 줄일 수 있다.

 

UpdateProfile정상적으로 작동

 

 

updateNotifications 에러 발생

그런데 updateNotifications같은 경우에는 에러가 발생한다.

이유는 ModelMapper에서는 다양한 형태의 객체를 매핑해주는데 notifications의 변수가 'StudyCreatedByEmail'와 같이 애매하게 되어있으므로 Nested객체와 혼동할 수 있다 (ex. study.created.~)

 

modelmapper 매칭전략

 

실제로 해당 위의 코드로 진행했을 때 아래와 같이 에러가 발생하였다.

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()메소드도 정상적으로 작동이 된다

updateNotifications 정상적으로 작동

 

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;
    }

    
}

 


참고

인프런 강의 - 스프링과 JPA 기반 웹 애플리케이션 개발