사용자에게 이메일 인증을 보내면 이메일 토큰을 생성하고 사용자가 해당 인증 링크를 클릭하면 이메일 토큰을 검증한다.
검증하는 것은 두가지로 1. 해당 uuid 값이 존재하는 값인지 2. 해당 이메일 토큰이 만료된 토큰인지 를 검사한다
이메일 토큰을 생성할때 생성시간+5분 으로 만료시간을 설정해서 값을 저장해둔다.
이후에 사용자가 전달해온 uuid로 이메일 토큰을 찾고, 해당 이메일 토큰에 저장되어있는 만료시각과 현재 시각을 비교하여 만료 여부를 판단한다.
그래서 만료 되었다면 isEmailExpired = true 로 필드의 값을 수정한다. 이후 오류 메세지를 반환한다
문제 상황
오류 메세지는 잘 반환이 되는데 해당 이메일 토큰의 필드값이 업데이트 되지 않았다. (sql 쿼리가 작성되지 않았다)
그런데 이상한건 원래 설정값이 false라서 true가 아닌 이상 오류 메세지를 출력할 수 없는데 오류 메세지는 잘 출력 되었다.
원인 파악
일단 오류 메세지가 나왔다는 것은 필드의 값이 수정은 되었다는 것, 그런데 바뀐값이 db에 반영되지 않았다는 것이다.
아마 문제의 원인은 트랜잭션에 있는 것 같다.
UserService
@Transactional
public class UserService {
@SuppressWarnings("all")
public UserTemp checkEmailToken(UUID emailTokenId){
// 토큰 검증
EmailToken emailToken = emailService.checkEmailToken(emailTokenId);
// 임시 회원의 이메일 인증 완료
Optional<UserTemp> findUserTemp = userTempRepository
.findById(emailToken.getUserTempId());
findUserTemp.ifPresent(UserTemp::emailVerifiedSuccess);
return findUserTemp.get();
}
}
url을 통해 받아온 uuid를 이용하여 토큰을 검증하고 검증이 완료되었다면 임시회원의 이메일 인증 완료 처리를 한 후 userTemp 객체를 반환한다.
이때 userService 트랜잭션이 설정되어있기 때문에 checkEmailToken 메서드가 성공적으로 끝나야 그 변화가 db에 저장된다.
EmailService
public class EmailService {
// 유효한 토큰 가져오기
@SuppressWarnings("all")
public EmailToken checkEmailToken(UUID emailTokenId) {
// emailToken 이 존재하는지 검사
EmailToken emailToken = emailTokenRepository.findByEmailTokenId(emailTokenId)
.orElseThrow(() -> new NoSuchElementException("해당 emailTokenId 를 가진 회원이 없습니다"));
// 해당 토큰의 만료 시간 검사
if (!emailToken.isValid()) {
expired(emailToken);
throw new IllegalStateException("해당 이메일은 만료되었습니다. 이메일을 재인증 해주세요");
}
emailToken.usedToken();
return emailToken;
}
@Transactional
public void expired(EmailToken emailToken){
emailToken.setEmailTokenExpired(true);
emailTokenRepository.save(emailToken);
}
}
EmailToken
public class EmailToken {
// 토큰 만료 시간 검증
public boolean isValid() {
return !LocalDateTime.now().isAfter(certificationTime);
}
}
이메일 토큰의 만료 시간을 검증할때 isValid 메서드를 호출하여 만료 시간이 지난뒤 인증 요청이 들어온 경우
expired 메서드를 호출하여 해당 이메일 토큰의 필드값을 수정한 후 오류 메세지를 띄우려고 했다.
expired 메서드에 트랜잭션을 설정했기 때문에 expired 메서드를 통해 생긴 변화( 필드값 변경후 다시 db에 저장) 가
db에 반영될 것이라 생각했다. 그런데 로그를 보면 sql이 작성되지 않았다.
물론 userService의 checkEmailToken 에 트랜잭션이 설정되어있지만 checkEmail에서 호출한 emailService 안의 expired에도 트랜잭션이 설정되어있기 때문에 expired의 트랜잭션이 먼저 끝나서 해당 변화가 db에 저장될 것이라고 생각했다.
GPT 답변
Transaction Committing:
- Since UserService.checkEmailToken is a transactional method, the transaction will only be committed after this method completes. Any changes made within this transaction, including changes made in EmailService.expired, will not be committed to the database until the UserService.checkEmailToken method completes successfully.
Transaction Rollback:
- If an exception is thrown before the transaction commits, all changes will be rolled back. Since you're throwing an IllegalStateException after marking the token as expired, this might cause the transaction to roll back if not handled properly.
해결 방법
@Transaction 어노테이션을 클래스 범위에서 삭제했다. ( 굳이 클래스단에 붙여줄 이유가 있을까? )
불필요한 중첩이 일어나지 않도록 필요한 메서드들에만 따로 설정해주자