본문 바로가기
DEV/개발일기

PostgreSQL - Spring JPA Lock Timeout 적용하기

by 화트마 2023. 12. 10.

 

Spring JPA에서 비관적 락 (Pessimistic lock) 사용 시 Lock timeout을 함께 적용해주어야 한다. 그렇지 않으면 락을 얻기위해 무한정 대기하게 될 수 있다.

 

DB종류별 LockTimeout 기본값은 다음과 같다.

LockTimeout Oracle MySQL PostgreSQL H2 Apache Derby
unit of measurement sec sec ms ms sec
nowait (or similar) 0 0 1 1 0
min 0 0 1 1 0
max 2.147.483 oo (no internal limit) 2.147.483.647 2.147.483.647 oo (no internal limit)
infinite wait -1 N/A 0 N/A N/A
default -1 50 0 4000 60

https://blog.mimacom.com/testing-pessimistic-locking-handling-spring-boot-jpa/

  • Oracle and PostgreSQL, by default, never throw a LockTimeoutException (dangerous!);
  • MySQL waits up to 50 seconds by default before throwing it (too long!).

 

PostgreSQL의 경우 default로 무한정 대기하므로 비관적 락 사용 시 Lock timeout을 지정해주어야한다.

 

JPA에서 Lock timeout을 지정하는 방법으로 아래와 같이 Hint를 지정해주는 방식이 많이 소개되고 있다.

@QueryHints({
       @QueryHint(name = "javax.persistence.lock.timeout", value = "2000")
})
@Query(nativeQuery = true, value = "select * from users u where u.id = :id for update")
Optional<User> findByIdForUpdate(Long id);

 

하지만 PostgreSQL에서는 JPA가 해당 방식을 지원하지 않는다.

jpa feature Oracle MySQL PostgreSQL H2 Apache Derby
LockModeType.PESSIMISTIC_WRITE v v v v v
javax.persistence.lock.timeout v        
javax.persistence.lock.timeout=0 (nowait) v   v    
handling of LockTimeoutException v v v   v

 

따라서 수기로 Lock timeout을 수행해주어야한다.

 

Lock timeout을 사용하는 경우가 많을 것으로 예상되어, Spring AOP를 활용하여 특정 Annotation에 대해 쿼리 수행전 Lock timeout을 걸도록 개발해보자.

 

 

먼저 어노테이션을 생성하자.

@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LockTimeout {
    int timeout() default 5000; // 5s
}

timeout값을 받아올 수 있도록 필드도 추가하였다. (기본값 5초)

 

Lock timeout을 걸기 위한 Repository를 정의하자.

@Component
@RequiredArgsConstructor
public class CommonRepository {
    private final EntityManager em;

    public void setLockTimeout(int timeout) {
        Query query = em.createNativeQuery("set local lock_timeout = " + timeout);
        query.executeUpdate();
    }
}

 

다음으로 AOP를 적용해보자.

쿼리 수행 전에 수행되도록 Before 어노테이션을 활용한다. 

@Aspect
@Component
@RequiredArgsConstructor
public class LockTimeoutAspect {

    private final CommonRepository commonRepository;

    @Before("@annotation(com.app.config.jpa.annotation.LockTimeout)")
    public void beforeLockTimeout(JoinPoint jointPoint) {
        MethodSignature methodSignature = (MethodSignature) jointPoint.getSignature();
        LockTimeout lockTimeout = methodSignature.getMethod().getAnnotation(LockTimeout.class);
        commonRepository.setLockTimeout(lockTimeout.timeout());
    }
}

 

이제 해당 어노테이션을 사용해보자.

비관적 락을 사용하는 쿼리에 해당 어노테이션을 사용한다. 

@LockTimeout(timeout = 3000)
@Query(nativeQuery = true, value = "select * from users u where u.id = :id for update")
Optional<User> findByIdForUpdate(Long id);

 

이제 쿼리를 수행해보면, 쿼리 수행 전에 Lock timeout을 먼저 걸게 된다.

/* dynamic native SQL query */ set local lock_timeout = 3000;
/* dynamic native SQL query */ select * from users u where u.id = ? for update

 

 

만약 timeout 시간 전에 lock을 획득하지 못하면 PessimisticLockingFailureException이 발생하게 된다.

org.springframework.dao.PessimisticLockingFailureException: could not extract ResultSet; SQL [n/a]; nested exception is org.hibernate.PessimisticLockException: could not extract ResultSet

 

 


 

추후 DB Lock 이 아닌, Redis의 Redisson 분산락을 활용해 어플리케이션 레벨에서 Lock을 걸어볼 수도 있을것 같다.

댓글