개발하는 자몽

[Spring Data JPA] TransactionRequiredException 본문

Java/Spring

[Spring Data JPA] TransactionRequiredException

jaamong 2023. 8. 19. 16:25

아래 블로그 글을 참고하여 정리했습니다.

 

Spring DeleteAllBy...In 호출시 에러 ( TransactionRequiredException )

문제 상황 : deleteAllByIdxIn 호출 시 entitymanager가 왜 없을까? JPA OSIV라면 기본적으로 트랜잭션 범위는 서비스 단까지 있을테고, entity manager는 생성됐을 것이다. 그런데 왜 아래와 같은 에러가 났을까

happyer16.tistory.com

 

 

 

상황

에러 문구

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는 쿼리 메소드로 만들었기 때문에 정확한 동작 방식을 알 수는 없지만, 대략적으로 아래와 같은 순서로 동작한다.

  1. Opening JPA EntityManager
  2. SELECT 쿼리 실행
  3. Closing JPA EntityManager
  4. 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 사용 못하도록 등록
    }

 

Comments