Java & Kotlin/Spring
[Spring Data JPA] TransactionRequiredException
jaamong
2023. 8. 19. 16:25
아래 블로그 글을 참고하여 정리했습니다.
상황
에러 문구
javax.persistence.TransactionRequiredException: No EntityManager with actual transaction available for current thread - cannot reliably process 'remove' call
에러 코드
@Service
@RequiredArgsConstructor
public class UserService implements UserDetailsService {
private final UserRepository userRepository;
private final RefreshTokenRepository rtRepository;
private final RedisUtil redisUtil;
...
public void logout(String accessToken, Long id) {
CustomUserDetails user = userRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, NOT_FOUND_USER.name()));
// 에러 발생 코드
rtRepository.deleteRefreshTokenByUsername(user.getUsername());
String token = accessToken.split(" ")[1];
int expiration = (int) jwtTokenUtils.getExpiration(token);
redisUtil.setBlackList(token, "accessToken", expiration);
}
...
}
원인
deleteRefreshTokenByUsername 메서드 호출 시 EntityManager가 없어서 발생한 에러이다.
JPA/Hibernate OSIV(Open Session In View)는 영속성 컨텍스트를 기본적으로 비즈니스 계층까지 열어둔다. 영속성 컨텍스트는 사용자 요청 시점에서 생성되지만, 데이터를 읽고, 수정할 수 있는 DB 트랜잭션은 비즈니스 계층에서만 사용되도록 트랜잭션이 일어난다. 따라서 해당 메서드를 호출할 때도 Entity Manager는 생성되어 있을 것이라고 추측했다. 그렇다면 해당 에러는 왜 발생했을까?
deleteRefreshTokenByUsername는 쿼리 메소드로 만들었기 때문에 정확한 동작 방식을 알 수는 없지만, 대략적으로 아래와 같은 순서로 동작한다.
- Opening JPA EntityManager
- SELECT 쿼리 실행
- Closing JPA EntityManager
- DELETE 쿼리 → 에러 발생
CrudRepository 인터페이스를 구현하고 있는 SimpleJpaRepository에는 @Transactional이 적용되어 있어서 똑같이 SELECT 쿼리가 나갈 deleteById는 문제없이 동작한다. 하지만 호출한 메서드는 커스텀이다 보니 기본적으로 트랜잭션이 적용되지 않고 조회 쿼리 이후 Entity Manager가 종료되는 것 같다.
해결
핵심은 Entity Manager가 DELETE 쿼리가 실행되기 전 종료되어 발생하는 오류이므로 오래 유지하기 위해 @Transactional을 메소드에 적용했다.
@Transactional
public void logout(String accessToken, Long userId) {
CustomUserDetails user = userRepository.findById(userId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, NOT_FOUND_USER.name()));
rtRepository.deleteRefreshTokenByUsername(user.getUsername()); //refresh token 삭제
String token = accessToken.split(" ")[1];
int expiration = (int) jwtTokenUtils.getExpiration(token); //access token 만료시간 조회
redisUtil.setBlackList(token, "accessToken", expiration); //redis에 accessToken 사용 못하도록 등록
}