본문 바로가기

Server Programming/Spring Boot Full-Stack Programming

[스프링 풀스택 클론 코딩 - 계정 설정] (2-8) ModelMapper 적용

반응형

ModelMapper

: 객체의 프로퍼티를 다른 객체의 프로퍼티로 맵핑해주는 유틸리티

http://modelmapper.org/

 

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

 

의존성 추가

<!-- https://mvnrepository.com/artifact/org.modelmapper/modelmapper -->
<dependency> 
<groupId>org.modelmapper</groupId> 
<artifactId>modelmapper</artifactId> 
<version>2.3.6</version> 
</dependency>

 

토크나이저 설정

modelMapper.getConfiguration() 
.setSourceNameTokenizer(NameTokenizers.UNDERSCORE) 
.setDestinationNameTokenizer(NameTokenizers.UNDERSCORE)

:UNDERSCORE(_)를 사용했을 때에만 nested 객체를 참조하는 것으로 간주하고 그렇지 않은 경우에는 해당 객체의 직속 프로퍼티에 바인딩 한다.

 


 

AccountService updateProfile 메서드, updateNotifications 메서드

//프로필 변경 -> 이미지 변경시에도 사용하므로, profileImage 추가
	public void updateProfile(Account account, Profile profile) {
		// TODO Auto-generated method stub
		account.setUrl(profile.getUrl());
		account.setOccupation(profile.getOccupation());
		account.setLocation(profile.getLocation());
		account.setBio(profile.getBio());
		account.setProfileImage(profile.getProfileImage());
		
		//트랜잭션이 끝난 detached상태이기 때문에 직접 DB에 반영을 해준다. 
		//즉 id값이 있으면 merge를 수행한다.
		accountRepository.save(account);
	}


//form객체인 notifications에서 가져와서  account에 변경
	public void updateNotifications(Account account, @Valid Notifications notifications) {
		// TODO Auto-generated method stub
		account.setStudyCreatedByEmail(notifications.isStudyCreatedByEmail());
		account.setStudyCreatedByWeb(notifications.isStudyCreatedByWeb());
		account.setStudyEnrollmentResultByEmail(notifications.isStudyEnrollmentResultByEmail());
		account.setStudyEnrollmentResultByWeb(notifications.isStudyEnrollmentResultByWeb());
		account.setStudyUpdatedByEmail(notifications.isStudyUpdatedByEmail());
		account.setStudyUpdatedByWeb(notifications.isStudyUpdatedByWeb());
		accountRepository.save(account);
	}

 

Profile 생성자

	// 프로필 폼에 채울 객체에 정보 넣기 -> ModelMapper를 이용해 생성
	public Profile(Account account) {
		this.bio = account.getBio();
		this.url = account.getUrl();
		this.occupation = account.getOccupation();
		this.location = account.getLocation();
		this.profileImage = account.getProfileImage();
	}

 

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

 

 


객체 안의 객체 또한 지원을 하기 때문에 정확한 경로를 지정해줘야한다.

-> 패턴을 지정해줘야한다.

 

기본값 : 유사한 변수에 매핑을 시킨다.

-> 유사한 변수가 존재한다면, 사용자가 원하는 패턴을 직접 설정해줘야 한다.

 

AppConfig에 사용할 모델맵퍼에 대한, 빈을 등록시킨다.

	//모델맵퍼 사용을 위한 빈 등록
	@Bean
	public ModelMapper modelMapper() {
		return new ModelMapper();
	}

 

AccountService의 updateProfile메서드

	//프로필 변경 -> 이미지 변경시에도 사용하므로, profileImage 추가
	public void updateProfile(Account account, Profile profile) {
		// TODO Auto-generated method stub
		
		//modelMapper의 map이라는 메서드를 이용해, 모델맵퍼 이용
		//원본 위치의 데이터를 원하는 객체로 복사
		//프로퍼티에 있는 값을 어카운트에 담기 -> (profile에서 account로 복사)
		modelMapper.map(profile, account);
		
		
		//트랜잭션이 끝난 detached상태이기 때문에 직접 DB에 반영을 해준다. 
		//즉 id값이 있으면 merge를 수행한다.
		accountRepository.save(account);
	}

 

-> 해당 경우의 경우에는 문제가 발생하지않는다.

 

public class Profile {
	@Length(max = 35)
	private String bio;
	@Length(max = 50)
	private String url;
	@Length(max = 50)
	private String occupation;
	@Length(max = 50)
	private String location;

	// 이미지
	private String profileImage;

	// @NoArgsConstructor를 대신해 사용한다면
	// public Profile(){};
public class Account {
	// 기본키와 생성 전략
	@Id	@GeneratedValue
	private Long id;
	
	
	//로그인 방식에서 이메일과 닉네임을 이용한 방식 지원하기 위해
	@Column(unique =true) //중복 방지
	private String email;
	@Column(unique =true)
	private String nickname;
	
	private String password;
	
	//이메일 인증 관련 참거짓판단
	private boolean emailVerified;

	//이메일 검증 토큰 값
	private String emailCheckToken;
	
	//회원가입날짜 변수
	private LocalDateTime joinedAt;
	
	private String bio;
	
	private String url;
	
	private String occupation;
	
	private String location;
	
	//이미지파일은 varchar 데이터형식보다 크기가 커서 사용하는 어노테이션
	//로딩 시간을 설정하는 어노테이션 즉시 로딩과 지연 로딩이 존재
	//즉시 로딩 EAGER, 지연 로딩 LAZY
    @Lob @Basic(fetch = FetchType.EAGER)
    private String profileImage;
	
	//생성, 가입, 갱신정보 알림 설정 -> Email, Web, 둘다
	private boolean studyCreatedByEmail;
	
	private boolean studyCreatedByWeb=true;
	
	private boolean studyEnrollmentResultByEmail;
	
	private boolean studyEnrollmentResultByWeb=true;
	
	private boolean studyUpdatedByEmail;
	
	private boolean studyUpdatedByWeb=true;

	//1시간 이내인지 확인하기 위해 전송시간 담는 변수
	private LocalDateTime emailCheckTokenGeneratedAt;

 

 

 

 

하지만, notification의 경우 modelmapper의 기본 매칭 설정에 의해 모호한 연관관계는 매핑이 되지 않기때문에 실패한다.

	//form객체인 notifications에서 가져와서  account에 변경
	public void updateNotifications(Account account, @Valid Notifications notifications) {
		// TODO Auto-generated method stub
		

		//modelMapper의 map이라는 메서드를 이용해, 모델맵퍼 이용
		//원본 위치의 데이터를 원하는 객체로 복사
		//프로퍼티에 있는 값을 어카운트에 담기 -> (notifications에서 account로 복사)
		modelMapper.map(notifications, account);
		
		accountRepository.save(account);
	}

 

public class Notifications {
	private boolean studyCreatedByEmail;
	private boolean studyCreatedByWeb;
	private boolean studyEnrollmentResultByEmail;
	private boolean studyEnrollmentResultByWeb;
	private boolean studyUpdatedByEmail;
	private boolean studyUpdatedByWeb;

 

public class Account {
	// 기본키와 생성 전략
	@Id	@GeneratedValue
	private Long id;
	
	
	//로그인 방식에서 이메일과 닉네임을 이용한 방식 지원하기 위해
	@Column(unique =true) //중복 방지
	private String email;
	@Column(unique =true)
	private String nickname;
	
	private String password;
	
	//이메일 인증 관련 참거짓판단
	private boolean emailVerified;

	//이메일 검증 토큰 값
	private String emailCheckToken;
	
	//회원가입날짜 변수
	private LocalDateTime joinedAt;
	
	private String bio;
	
	private String url;
	
	private String occupation;
	
	private String location;
	
	//이미지파일은 varchar 데이터형식보다 크기가 커서 사용하는 어노테이션
	//로딩 시간을 설정하는 어노테이션 즉시 로딩과 지연 로딩이 존재
	//즉시 로딩 EAGER, 지연 로딩 LAZY
    @Lob @Basic(fetch = FetchType.EAGER)
    private String profileImage;
	
	//생성, 가입, 갱신정보 알림 설정 -> Email, Web, 둘다
	private boolean studyCreatedByEmail;
	
	private boolean studyCreatedByWeb=true;
	
	private boolean studyEnrollmentResultByEmail;
	
	private boolean studyEnrollmentResultByWeb=true;
	
	private boolean studyUpdatedByEmail;
	
	private boolean studyUpdatedByWeb=true;

	//1시간 이내인지 확인하기 위해 전송시간 담는 변수
	private LocalDateTime emailCheckTokenGeneratedAt;

 

패턴 지정을 위한 모델맵퍼 설정 변경

	//모델맵퍼 사용을 위한 빈 등록
	@Bean
	public ModelMapper modelMapper() {
		ModelMapper modelMapper = new ModelMapper();
		modelMapper.getConfiguration()
		.setDestinationNameTokenizer(NameTokenizers.UNDERSCORE)
		.setSourceNameTokenizer(NameTokenizers.UNDERSCORE);
		//UNDERSCORE가 아니면 하나의 프로퍼티로 간주 
		return modelMapper;
	}

UNDERSCORE(_)가 아니라면 그 이름 전체를 하나의 프로퍼티 이름으로

 

단, nested 한 표현을 하고 싶다면 configuration에 설정해둔대로 UNDERSCORE를 이용하면 된다.

 

ModelMapper는 매핑 설정을 통해서 필드명이나 구조가 일치하지 않더라도 TypeMap을 이용하여 매핑시킬 수 있다.

 

#ModelMapper typeMap 설정

클래스 타입이 같은 경우

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

Bill bill2 = modelMapper.map(itemA, Bill.class);

 

클래스 타입이 다른 경우

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

Bill bill2 = modelMapper.map(request, Bill.class);

 

맵퍼 클래스의 메서드인 map, using, skip을 이용해 원하는 매핑 구조 설정할 수 있다.

 

 


Profile 생성자와 Notifications 생성자 모델맵퍼 적용 -> 해당 생성자를 지우고 SettingsController에서 직접 매핑

	// 프로필 폼에 채울 객체에 정보 넣기 -> ModelMapper를 이용해 생성
	public Profile(Account account) {
		this.bio = account.getBio();
		this.url = account.getUrl();
		this.occupation = account.getOccupation();
		this.location = account.getLocation();
		this.profileImage = account.getProfileImage();
	}

 

	//알림 메서드 -> 해당 계정에서 정보를 가져온다.
	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();
	}

 

SettingsController

//Profile과 Notifications 생성자를 modelmapper를 이용해서 매핑
	private final ModelMapper modelMapper;

 

기존의 profileUpdateForm, updateNotificationsForm 메서드

	//프로필 설정
    // Get으로 전달하면, 업데이트 폼 리턴
	@GetMapping(SETTINGS_PROFILE_URL)
	// 자신의 정보만 수정가능하므로, 현재 자기 자신 정보와 모델정보를 담을 인스턴스를 파라미터로
	public String profileUpdateForm(@CurrentUser Account account, Model model) {
		model.addAttribute(account);
		// 화면에 계정정보를 넣어주고
		model.addAttribute(new Profile(account));
		// 폼에 사용할 객체를 만들어서 정보를 넣어준다. -> 계정정보를 조회해 채운다.

		return SETTINGS_PROFILE_VIEW_NAME;
	}
	// return은 void로 메서드를 생성하면 생략가능 한데, 뷰네임트랜스레이터가 url네임과 같다고 추측
    
    
   //알림 설정
	@GetMapping(SETTINGS_NOTIFICATIONS_URL)
	public String updateNotificationsForm(@CurrentUser Account account, Model model) {
		//모델에 현재유저의 정보를 담는다.
		model.addAttribute(account);
		//또한, form을 채울 객체 생성
		model.addAttribute(new Notifications(account));
		
		return SETTINGS_NOTIFICATIONS_VIEW_NAME;
	}

 

변경 후

 

	//프로필 설정
	// Get으로 전달하면, 업데이트 폼 리턴
	@GetMapping(SETTINGS_PROFILE_URL)
	// 자신의 정보만 수정가능하므로, 현재 자기 자신 정보와 모델정보를 담을 인스턴스를 파라미터로
	public String profileUpdateForm(@CurrentUser Account account, Model model) {
		model.addAttribute(account);
		// 화면에 계정정보를 넣어주고
		model.addAttribute(modelMapper.map(account,Profile.class));
		// 폼에 사용할 객체를 만들어서 정보를 넣어준다. -> 계정정보를 조회해 채운다.
		//Profile.class를 사용하면, 프로필 타입의 인스턴스 생성되고, account의 데이터로 채워진다.

		return SETTINGS_PROFILE_VIEW_NAME;
	}
	// return은 void로 메서드를 생성하면 생략가능 한데, 뷰네임트랜스레이터가 url네임과 같다고 추측


	//알림 설정
	@GetMapping(SETTINGS_NOTIFICATIONS_URL)
	public String updateNotificationsForm(@CurrentUser Account account, Model model) {
		//모델에 현재유저의 정보를 담는다.
		model.addAttribute(account);
		//또한, form을 채울 객체 생성
		model.addAttribute(modelMapper.map(account,Notifications.class));
		// 폼에 사용할 객체를 만들어서 정보를 넣어준다. -> 계정정보를 조회해 채운다.
		//Notificatoins.class를 사용하면, 프로필 타입의 인스턴스 생성되고, account의 데이터로 채워진다.
		return SETTINGS_NOTIFICATIONS_VIEW_NAME;
	}
반응형