프로필 수정 처리

Yuri Lee·2020년 11월 12일
0

폼 처리

  • 비어있는 값을 허용. (기존에 있던 값을 삭제하고 싶을 수도 있기 때문)
  • 중복된 값을 고민하지 않아도 된다.
  • 확인할 내용은 입력 값의 길이
  • 에러가 있는 경우 폼 다시 보여주기
  • 에러가 없는 경우
    • 저장하고,
    • 프로필 수정 페이지 다시 보여주기 (리다이렉트)
    • 수정 완료 메시지

Controller

SettingsController.java

    @PostMapping(SETTINGS_PROFILE_URL)
    public String updateProfile(@CurrentUser Account account, @Valid Profile profile, Errors errors,
                                Model model, RedirectAttributes attributes) {
        if (errors.hasErrors()) {
            model.addAttribute(account);
            return SETTINGS_PROFILE_VIEW_NAME;
        }

        accountService.updateProfile(account, profile);
        attributes.addFlashAttribute("message", "프로필을 수정했습니다.");
        return "redirect:" + SETTINGS_PROFILE_URL;
    }

현재 CurrentUser 정보를 변경할 것이고 form에서 입력한 값들은 model attribute를 이용하여 Profile로 받아올 것이다. model attribute 로 데이터를 바인딩하고 Validation 할 때 발생하는 에러들을 바로 errors로 받아올 것이다. 항상 이렇게 짝을 지어 다닌다. errors가 모델 에트리뷰트 객체의 바인딩 에러들을 받아주는 형태로, 오른쪽에 두어야 한다.

  • 에러가 있는 경우
    form 을 채웠던 데이터들은 자동으로 들어가고 더불어 에러에 대한 정보, 모델에 대한 정보도 들어간다. 따라서 account만 다시 넣어주면 된다. 참고로 url 에러가 발생할 수 있으므로 변수를 설정해서 사용했다.

  • 에러가 없는 경우
    데이터를 변경하는 쪽은 서비스쪽에 위임을 해서 트랜잭션 안에다 처리를 할 것, 그럼 서비스를 받아와야 한다. 서비스에서 업데이트를 진행하도록 한다. account 정보를 profile로 변경을 한다.
    get-post redirect 패턴: 사용자가 화면을 refresh 하더라도 폼 서브밋이 다시 일어나지 않도록 redirect 하는 것. update하는 form으로 다시 보낸다.

Service

AccountService.java

    public void updateProfile(Account account, Profile profile) {
        account.setUrl(profile.getUrl());
        account.setOccupation(profile.getOccupation());
        account.setLocation(profile.getLocation());
        account.setBio(profile.getBio());

    }

이렇게 하면 정상적으로 작동할 것 같지만 문제가 있다.

    1. profile로 바인딩을 받을 때 null point 익셉션이 발생한다.

NullPointerException

profile.java

package com.yuri.studyolle.settings;
import com.yuri.studyolle.domain.Account;
import lombok.Data;

@Data
public class Profile {
    public Profile(Account account) { //생성자 만듦
        this.bio = account.getBio();
        this.url = account.getUrl();
        this.occupation = account.getOccupation();
        this.location = account.getLocation();
    }

	private String bio;
    private String url;
    private String occupation;  // 직업
    private String location;
}

만들었던 profile.java에 생성자를 하나 만들어 놨었다. 위에 보이다시피 기본 생성자가 없다. 이 상황에서 스프링 Mvc가 이 모델 attribute로 데이터를 받아 오려고 할 때 Profile의 인스턴스를 먼저 만든 다음에 setter를 사용해서 주입하려고 한다. (현재 Profile의 인스턴스를 만드려고 하는데 생성자가 하나밖에 없기 때문에 public Profile(Account account) 이 생성자를 이용해서 만드려고 함) 하지만 이 순간에는 account가 없다. 이때 account는 우리가 모델에다 전달해준 account를 사용할 수 있는 게 아니다. 참조할 수 없다. 그래서 default 생성자를 만들어 줘야 한다! 이를 해결하기 위한 두가지 방법이 있다.

  • @NoArgsConstructor(파라미터가 없는 기본 생성자를 생성) 사용 (✅)
  • 혹은 기본 생성자를 생성해주기

public Profile() { }

@NoArgsConstructor 를 넣어주면 더이상 null point exception 은 발생하지 않는다. 이 null point는 생성자에서 발생한다. account가 null 이기 때문에 이 어노테이션을 사용해서 기본 생성자를 만들어줘야 한다. 이제 기본 생성자를 이용해서 Profile 바인딩을 받을 Profile 객체를 생성하기 때문에 바인딩을 잘 받을 수 있다.

    1. 하지만 문제는 update가 안된다. 서비스로 위임했고, @Transactional 도 붙어있으므로 당연히 트랜잭션 처리가 되어야 하는 것 아닌가라는 의문이 들지만 현재 코드에서는 트랜잭션 처리는 되지만 업데이터 처리가 안된다. 왜 그럴까? 이를 이해하면 JPA를 잘 아는 사용자이다.

AccountService.java

	public void completeSignUp(Account account) {
		account.completeSignUp();
    	login(account);
	}

completeSignUp 여기 부분은 account가 persistent 상태이다. 영속성 컨텍스트에 이미 들어와있다. 뷰 랜더링 한 다음에 없어질 것이다.

AccountController.java

    @GetMapping("/check-email-token")
    public String checkEmailToken(String token, String email, Model model) {
    	Account account = accountRepository.findByEmail(email);
    	String view = "account/checked-email";
    	 
    	// 이메일이 정확하지 않은 경우에 대한 에러 처리
    	if (account == null) {
    		model.addAttribute("error", "wrong email");
    		return view;
    	}

    	// 토큰이 정확하지 않은 경웨 대한 에러 처리
    	if (!account.isValidToken(token)) {
    		model.addAttribute("error", "wrong token");
    		return view;
    	}
    	
    	accountService.completeSignUp(account);
    	//이메일과 토큰이 정확한 경우 가입 완료 처리
    	model.addAttribute("numberOfUser", accountRepository.count());
    	model.addAttribute("nickname", account.getNickname());
    	return view;
    }

Account account = accountRepository.findByEmail(email); -> 영속성 컨텍스트가 이미 있는 상태에서 트랜잭션을 통해서 데이터를 읽어왔다. repository가 제공하는 모든 메소드는 트랜잭션 처리가 가능하다. 여기서 읽어온 account 객체는 persistent 상태로 영속성 컨텍스트에서 관리하는 객체이다. 그래서 트랜잭션 안에서 상태의 값을 변경했을 때 영속성 컨텍스트가 객체의 상태 변화를 감지하고 있다가 트랜잭션이 끝났을 때 db에 싱크를 한다.

SettingsController.java

    @PostMapping("/settings/profile")
    public String updateProfile(@CurrentUser Account account, @Valid Profile profile, Errors errors,
                                Model model, RedirectAttributes attributes) {
        if (errors.hasErrors()) {
            model.addAttribute(account);
            return SETTINGS_PROFILE_VIEW_NAME;
        }

        accountService.updateProfile(account, profile);
        attributes.addFlashAttribute("message", "프로필을 수정했습니다.");
        return "redirect:" + SETTINGS_PROFILE_URL;
    	
    }

AccountService.java

    public void updateProfile(Account account, Profile profile) {
        account.setUrl(profile.getUrl());
        account.setOccupation(profile.getOccupation());
        account.setLocation(profile.getLocation());
        account.setBio(profile.getBio());

    }

AccountService.java의 updateProfile() 에서 사용한 Account객체(Account account) 는 SettingsController.java의 public String updateProfile(@CurrentUser Account account account에서 온 것이다. account 이 객체는 persistent 상태가 아니다. 이 객체는 우리가 세션에다가 넣어놨던 인증 정보 안에 들어있던 Principal 정보(Authentication안에 들어있는)이다. 이것은 이미 트랜잭션이 끝난지 오래이다. 지금 이 객체 값은 detached 상태 (JPA가 한번이라도 알던 객체, 즉 id값이 있음) 이다. 영속성 컨텍스트에서 관리하고 있는 객체는 아니다. detached 객체는 아무리 변경하더라도 변경이력을 감지하지 않는다. 트랜잭션이 끝나도 db에 반영하지 않는다.

accountRepository.save(account);

따라서 AccountService.java에 다음의 코드를 추가해줘야 한다. accountRepository에 save를 호출해주면 된다. save 안에서 id 값이 있는지 없는지 보고 id 값이 있으면 merge를 시킨다. 기존 데이터에 update를 시킨다.

리다이렉트시 간단한 데이터를 전달하고 싶다면?

업데이트가 되었는지 안되었는지 감이 잘 안잡힌다. 그래서 업데이트가 된 다음에 리다이렉트 하면서 메시지를 하나 보낼 것이다. RedirectAttributes 를 사용한다. 스프링 mvc에서 제공해주는 기능이다.

Tip

JPA를 사용할 때는 현재 수정하고 있는 객체가 어떤 상태인지, 트랜잭션이 있는 상태인지 없는 상태인지 잘 감안해서 코딩을 해야 한다.

Q&A

Q. 엔티티가 detached 되는 시점이 Transaction이 끝나는 시점 (@Transactional 애노테이션이 붙은 메서드의 호출 후)인 줄 알았는데 오늘 수업을 들으니 그게 아닌 것 같아요.

findByEmail로 조회를 하고나면 해당 트랜잭션이 끝나 detached 상태가 되는 줄 알았거든요.
"Transaction이 끝나는 시점"이 맞기도 하고 틀리기도 한데 OSIV(Open Session In View) 필터 때문에 영속성 관리자는 계속 열려 있어서 트랜잭션이 끝났어도 detached가 아니라 persist 상태일 수 있습니다.

OSIV 필터 때문에 영속성 관리자는 요청이 컨트롤러에 들어오기 전부터 만들어져 있고, 컨트롤러 안에서 리파지토리 사용해서 findByEmail로 엔티티를 조회할 때 당연히 리파지토리에 있는 @Transactional이 적용되서 엔티티 가져올 때 해당 엔티티는 persist 상태입니다. 그런데 영송성 관리자가 계속 현재 쓰레드에 열려 있기 때문에 컨트롤러가 @Transactional이 아니어도 해당 엔티티는 계속해서 persist 상태인 겁니다. OSIV 필터가 없는 상태에서는 트랜잭션이 끝나는 시점이 영속성 관리자도 종료되는 시점이라고 가정하는게 맞긴합니다.

Q. 어떻게 업데이트?

  1. @CurrentUser 에 있는 Account 객체는 deteched 상태. 즉, 영속성 컨택스트 내부에 아이디가 존재하지만, 연결이 되지 않은 상태라 아무리 set해줘도 반영되지 않는 상태입니다.

  2. 하지만 accountRepository가 save 구문을 실행해주면 deteched가 다시 영속성 컨택스트에 반영되면서 set 정보들이 merge 되게 된다. 따라서 account 객체도 업데이트 되고, DB에도 반영되는 효과를 가지게 된다.

  3. account 객체는 세션에 있는 객체이지만, 이 save 구문을 통해 정보가 업데이트 되었다.


what is a Persistenc 영속성?

  • 프로그램이 종료 되어도 사라지지 않는 데이터의 특성으로 이를 구현하기 위해 파일시스템, 관계형데이터베이스 등을 구현

what is a Persistence Context 영속성 컨텍스트?

  • EntityManger.persist(entity);
    • DB에 저장한다는게 아니라, 영속성 컨텍스트를 통해 entity를 영속화 한다는 것
      • 정확히는 영속성 컨텍스트에 저장한다.
  • 영속성 컨텍스트는 논리적인 개념

엔티티의 생명 주기

  • 비영속(new/transient)
    영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
  • 영속(managed)
    영속성 컨텍스트에 관리되는 상태
  • 준영속(detached)
    영속성 컨텍스트에 저장되었다가 분리된 상태
  • 삭제(removed)
    삭제된 상태


출처 : 인프런 백기선님의 스프링과 JPA 기반 웹 애플리케이션 개발
https://www.daleseo.com/lombok-popular-annotations/
https://velog.io/@cbbatte/JPA-%EC%98%81%EC%86%8D%EC%84%B1Persistence-1
https://m.blog.naver.com/PostView.nhn?blogId=goddlaek&logNo=220889229659&proxyReferer=https:%2F%2Fwww.google.com%2F
https://joochang.tistory.com/76
https://coding-factory.tistory.com/575

profile
Step by step goes a long way ✨

0개의 댓글