프로젝트

[트러블슈팅] 객체 삭제 시 org.hibernate.StaleObjectStateException 에러 발생

SY 키키 2024. 6. 24. 20:34

유저 RefreshTokenRepository에서 accessToken과 refreshToken 을 이용해서 데이터를 삭제해주는 로직을 작성하였습니다.

 

// Service
@Transactional
public void tokenRemove(HttpServletResponse response, String accessToken, String refreshToken) {
    // 쿠키에서 삭제 -> 클라이언트에게 감
    log.info("토큰 삭제");
    tokenCookieRemove(response, TYPE_ACCESS);
    tokenCookieRemove(response, TYPE_REFRESH);
    refreshTokenRepository.deleteByAccessTokenAndRefreshToken(accessToken, refreshToken); // 서버에서 삭제 -> 비교군
}

// Repository
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {
    void deleteByAccessTokenAndRefreshToken(String accessToken, String refreshToken);
}

 

그런데 실제 서비스에서 로그아웃시, 다음과 같은 에러를 발생하였습니다.

2024-06-24T09:58:14.123Z ERROR 1 --- [nio-8080-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.orm.ObjectOptimisticLockingFailureException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.restgram.domain.user.entity.RefreshToken#9]] with root cause

org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.restgram.domain.user.entity.RefreshToken#9]

 

이러한 StaleObjectStateException은 이미 삭제한 데이터를 다시 삭제하려고 할 경우 발생하는 에러입니다.

따라서 저는 객체를 가져온 후 삭제하는 방식으로 수정한 후, Pessimistic_write 어노테이션(비관적 락)을 추가해주었습니다.

// service
@Transactional
public void tokenRemove(HttpServletResponse response, String accessToken, String refreshToken) {
    // 쿠키에서 삭제 -> 클라이언트에게 감
    log.info("토큰 삭제");
    tokenCookieRemove(response, TYPE_ACCESS);
    tokenCookieRemove(response, TYPE_REFRESH);

    Optional<RefreshToken> optionalRefreshToken = refreshTokenRepository.findByAccessTokenAndRefreshToken(accessToken, refreshToken);
    if (optionalRefreshToken.isPresent()) refreshTokenRepository.delete(optionalRefreshToken.get());
}

// repository
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {

    @Lock(value = LockModeType.PESSIMISTIC_WRITE)
    Optional<RefreshToken> findByAccessTokenAndRefreshToken(String accessToken, String refreshToken);

    boolean existsByAccessTokenAndRefreshToken(String accessToken, String refreshToken);
}