Open EntityManager (또는 Session) In View 필터

Yuri Lee·2020년 11월 11일
0

BUG

가입을 하고 회원 인증을 했음에도 불구하고 가입 날짜가 업데이트 되지 않았다. 이유를 알기 위해서는 인증 메일 처리하는 부분을 살펴봐야 한다.

AccountController

    @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;
    	}
    	
    	account.completeSignUp();
    	accountService.login(account);
    	
    	//이메일과 토큰이 정확한 경우 가입 완료 처리
    	model.addAttribute("numberOfUser", accountRepository.count());
    	model.addAttribute("nickname", account.getNickname());
    	return view;
    }

Account.java

    public void completeSignUp() {
        this.emailVerified = true;
        this.joinedAt = LocalDateTime.now();
    }

account.completeSignUp();
accountService.login(account);

가입 날짜를 업데이트 하지 않는다. 왜 일까? 객체는 바뀌었다. 객체의 데이터는 분명히 바뀌었다. 코드 적용이 되고 로그인도 되었다. 그런데 프로필 뷰에서 날짜 데이터가 없다고 나온다.

AccountController.java

    @GetMapping("/profile/{nickname}")
    public String viewProfile(@PathVariable String nickname, Model model, @CurrentUser Account account) {
        Account byNickname = accountRepository.findByNickname(nickname);
        if (nickname == null) {
            throw new IllegalArgumentException(nickname + "에 해당하는 사용자가 없습니다.");
        }

        model.addAttribute(byNickname);
        model.addAttribute("isOwner", byNickname.equals(account));
        return "account/profile";
    }

프로필에서 보여주는 데이터는 db 에서 읽어오는 것이다. 즉 db 에 반영이 안되었다는 의미이다.

account.completeSignUp();
accountService.login(account);

여기서 변경된 객체의 상태가 db 에 반영이 안된 것이다. 왜일까?

open Entity manager in view

기본적으로 open Entity manager in view 라는 필터는 스프링 부트에 기본적으로 등록되어있고 활성화 되어있다.

JPA EntityManager(영속성 컨텍스트)를 요청을 처리하는 전체 프로세스에 바인딩 시켜주는 필터.

  • 뷰를 랜더링 할때까지 영속성 컨텍스트를 유지하기 때문에 필요한 데이터를 랜더링 하는
    시점에 추가로 읽어올 수 있다. (지연 로딩, Lazy Loading)
  • 엔티티 객체 변경은 반드시 트랜잭션 안에서 할것
    • 그래야 트랜잭션 종료 직전 또는 필요한 시점에 변경사항을 DB에 반영

db 에서 읽어온 객체들을 관리하는 컨텍스트가 있다. 이 안에서 persistent 상태의 객체들을 관리한다. 이 객체들은 트랜잭션 범위 안에서 객체 상태의 변경을 감지하다가 트랜잭션에 종료될 때 db 에 변경 사항을 반영을 해준다. 업데이트 쿼리가 발생한다. account.completeSignUp(); 이 부분과 같은 경우에서! 하지만 이 부분에는 트랜잭션이 반영되어있지 않는다. processNewAccount 함수의 경우 트랜잭션이 존재한다.

변경사항

AccountService.java

    @Transactional(readOnly = true)
    @Override
    public UserDetails loadUserByUsername(String emailOrNickname) throws UsernameNotFoundException {
        Account account = accountRepository.findByEmail(emailOrNickname);
        if (account == null) { // 이메일로도 찾아보고 닉네임으로도 찾아보고
            account = accountRepository.findByNickname(emailOrNickname);
        }

        if (account == null) { // 그래도 null 이라면 예외던지기! 해당하는 유저가 없다. 그런 경우에는 이메일 혹은 패스워드가 잘못되었다고 보여주면 된다.
            throw new UsernameNotFoundException(emailOrNickname);
        }
        // 프린시펄에 해당하는 UserAccount
        return new UserAccount(account);
    }
    public void completeSignUp(Account account) {
        account.completeSignUp();
        login(account);
    }
  • 메서드마다 트랜잭션 어노테이션을 붙이는 게 번거로움으로 AccountService class 위에다 바로 붙여줌
  • UserDetails 에 @Transactional(readOnly = true) 설정하기. 데이터를 변경하는 게 아니라 로그인 할때 체크하기 위해 데이터를 읽어오는 것이므로 readOnly 설정해준다. 성능에 조금 유리해질 수 있다.
  • completeSignUp 메서드는 데이터를 변경하는 것이므로 기본 트랜잭션을 사용하면 된다.

기본적으로 영속성 컨텍스트가 유지되는 한 어떠한 데이터를 읽어오던 그 모든 읽어온 데이터는 persistent 상태가 된다. 영속성 컨텍스트가 끝나기 전까지. 이것은 뷰 렌더링이 끝나고 끝난다. 이 말은 뷰를 렌더링 할때도 영속성 컨텍스트를 통해서 무언가 할 수 있다는 것이다. (ex. 조회)

어떤 데이터를 랜더링 할때 뷰에다 전달해줘야 할 것들을 모든 것들을 전달해주고 모델에다 담아주는 게 아니라 뷰에서 도메인 기반으로 네비게이션하면서 추가로 로딩할 수 있다. 도메인 기반으로 코딩하는 게 더 수월해진다. 원하지 않게 수많은 쿼리가 발생하는 경우가 생기기도 하지만, 그때그때마다 최적화 하는 방법을 찾아야 한다.

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

기본적으로 스프링부트 웹앱은 open Entity manager in view가 활성화 되어있기 때문에 이러한 특성이 있다. 위 코드의 Account account = accountRepository.findByEmail(email); 의 account 는 persistent 상태이다. (모든 읽어온 데이터들은 persistent 상태이다.) 기본적으로 모든 repository는 트랜잭션이 적용되어있다.

AccountRepository.java

package com.goodmoim.account;

import com.goodmoim.domain.Account;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.transaction.annotation.Transactional;

@Transactional(readOnly = true)
public interface AccountRepository extends JpaRepository<Account, Long> {
    boolean existsByEmail(String email);

    boolean existsByNickname(String nickname);

    Account findByEmail(String email);

    Account findByNickname(String nickname);
}

따라서 repository를 통해서 작업을 할 경우 다 트랜잭션을 통해 하는 것이다.

결과


가입 후, 이메일 인증 하고 프로필을 확인해본 결과 가입 날짜가 잘 반영되었다.

앞으로

  • 데이터 변경은 서비스 계층으로 위임해서 트랜잭션 안에서 처리하자
  • 데이터 조회는 굳이 트랜잭션이 없어도 된다. 뷰를 렌더링 할때 lazy loading 을 사용 & 리파지토리 또는 서비스 사용

출처 : 인프런 백기선님의 스프링과 JPA 기반 웹 애플리케이션 개발

profile
Step by step goes a long way ✨

0개의 댓글