프로젝트에서 JPA를 사용하면서 진행중인데 , 어떠한 특정 값이 Null로 들어오는 NullPointerException이 발생하는 것을 확인했다. 결국 디버깅을 통해서 특정 값이 null로 들어오는 것을 확인했는데 , 이제 코드로 설명하겠다.
Controller
/**유효한 이메일인지 check**/
@GetMapping("/check-email-token")
public String checkEmailToken(String token,String email,Model model) {
Account account = accountRepository.findByEmail(email);
String view = "account/checked-email";
//1.email이 존재하는지
//2.emailCheckToken이 일치하는지
if (account == null) {
model.addAttribute("error", "wrong.email");
return view;
}
if (!account.getEmailCheckToken.equals(token)) {
model.addAttribute("error", "wrong.token");
return view;
}
account.setEmailVerified(true);
account.setJoinedAt(LocalDateTime.now());
model.addAttribute("numberOfUser", accountRepository.count());
model.addAttribute("nickname", account.getNickname());
return view;
}
하지만 문제는 2번째 검사를 해주는 조건문 안의 account.getEmailCheckToken의 값이 계속해서 null로 들어오는 것이었다. 그렇다. 잘 생각해보면 문제는 애초에 회원가입에서의 발급해주는 이메일 인증 토큰 값이 데이터베이스에 저장이 안된것이다. 그러면 무엇이 문제였을까?
그것을 확인하기위해 회원가입에서의 service 단을 살펴보겠다.🔽
회원가입 service
public void processNewAccount(@Valid SignUpForm signUpForm) {
//회원 저장
Account newAccount = save(signUpForm);
//저장 완료 시 이메일 체크에 필요한 토큰 발급🔽
newAccount.generateEmailCheckToken(); /**newAccount라는 객체는 detached 상태이기 때문에 @Transactional을 붙여줘서 persist상태를 유지시켜준다. **/
sendSignUpEmail(newAccount);
}
/**회원 가입 save 메서드**/
private Account save(@Valid SignUpForm signUpForm) {
String encode = passwordEncoder.encode(signUpForm.getPassword());
Account account = signUpForm.of(encode);
return accountRepository.save(account);
}
/**계정 생성시 UUID를 사용하여 랜덤한 토큰 값 발급**/
public void generateEmailCheckToken() {
this.emailCheckToken = UUID.randomUUID().toString();
this.emailCheckTokenGeneratedAt = LocalDateTime.now();
}
이미 Detached 상태가 되어버린 객체
문제는 바로 두번째 메서드에서 이미 📢영속성 컨텍스트에 있는 것을 commit 해줘서 persist상태가 아닌 detached 상태가 되어버린것이다.
즉, 디비에 저장 후 newAccount 객체는 detached 객체라서 generateEmailCheckToken() 메서드를 사용해서 emailCheckToken값을 넣어주려고 할 때 데이터베이스와 Sync가 안 맞게된다.
쉽게 다시말하자면 하나의 객체를 이미 commit하여서 그 객체는 detached 객체가 되었다.그래서 디비와의 Sync가 안 맞게 된것이다. 따라서 트랜잭션의 범위를 넓혀주어야한다. 즉 회원 가입시 emailCheckToken값도 디비에 저장하게 하려면 processNewAccount메서드에 @Transactional을 붙여주어야한다.
@Transactional
public void processNewAccount(@Valid SignUpForm signUpForm) {
//회원 저장
Account newAccount = save(signUpForm);
//저장 완료 시 이메일 체크에 필요한 토큰 발급🔽
newAccount.generateEmailCheckToken(); /**newAccount라는 객체는 detached 상태이기 때문에 @Transactional을 붙여줘서 persist상태를 유지시켜준다. **/
sendSignUpEmail(newAccount);
}