성능테스트중 부하가 많을때에도 타임아웃예외가 터지지 않는 것에서 의문이 생겼다. 아무리 데이터셋이 적더라도 이게 가능한가..?
확인한 결과 0초는 정상적용 되었으나, 1ms까지 타임아웃 시간을 줄여도 타임아웃이 동작하지 않는 것을 발견했다.
1000ms3000ms 를 걸어 예외가 터지는지 확인@ActiveProfiles("test") 을 통해 h2 환경 / postgreSQL 각각 테스트 @Lock(LockModeType.PESSIMISTIC_WRITE)
@QueryHints({
@QueryHint(name = "jakarta.persistence.lock.timeout", value = "1000"),
@QueryHint(name = "org.hibernate.dialect.lock.timeout", value = "1000")
})
@Query("SELECT s FROM Stock s WHERE s.id = :id AND s.deletedAt is null")
Optional<Stock> findByIdWithLock(@Param("id") StockId id);
@Override
@Transactional
public void prepareStock(PrepareStockRequestDto requestDto) {
try {
getSortedStocks(requestDto.stocks())
.forEach(stockItem ->
getStockWithLock(stockItem.stockId()).decreaseStock(stockItem.quantity()));
sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
@ActiveProfiles("test")Caused by: org.hibernate.PessimisticLockException: JDBC exception executing SQL [select s1_0.hub_id,s1_0.product_id,s1_0.created_at,s1_0.created_by,s1_0.deleted_at,s1_0.deleted_by,s1_0.quantity,s1_0.updated_at,s1_0.updated_by from p_stock s1_0 where (s1_0.hub_id,s1_0.product_id)=(?,?) and s1_0.deleted_at is null for update] [Timeout trying to lock table "P_STOCK"; SQL statement:
select s1_0.hub_id,s1_0.product_id,s1_0.created_at,s1_0.created_by,s1_0.deleted_at,s1_0.deleted_by,s1_0.quantity,s1_0.updated_at,s1_0.updated_by from p_stock s1_0 where (s1_0.hub_id,s1_0.product_id)=(?,?) and s1_0.deleted_at is null for update [50200-232]] [n/a]
at org.hibernate.dialect.H2Dialect.lambda$buildSQLExceptionConversionDelegate$3(H2Dialect.java:771) ~[hibernate-core-6.6.8.Final.jar:6.6.8.Final]
at org.hibernate.exception.internal.StandardSQLExceptionConverter.convert(StandardSQLExceptionConverter.java:58) ~[hibernate-core-6.6.8.Final.jar:6.6.8.Final]
at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:108) ~[hibernate-core-6.6.8.Final.jar:6.6.8.Final]
at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:94) ~[hibernate-core-6.6.8.Final.jar:6.6.8.Final]
at org.hibernate.sql.results.jdbc.internal.DeferredResultSetAccess.executeQuery(DeferredResultSetAccess.java:268) ~[hibernate-core-6.6.8.Final.jar:6.6.8.Final]
at org.hibernate.sql.results.jdbc.internal.DeferredResultSetAccess.getResultSet(DeferredResultSetAccess.java:171) ~[hibernate-core-6.6.8.Final.jar:6.6.8.Final]
at org.hibernate.sql.results.jdbc.internal.JdbcValuesResultSetImpl.<init>(JdbcValuesResultSetImpl.java:74) ~[hibernate-core-6.6.8.Final.jar:6.6.8.Final]
at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.resolveJdbcValuesSource(JdbcSelectExecutorStandardImpl.java:355) ~[hibernate-core-6.6.8.Final.jar:6.6.8.Final]
at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.doExecuteQuery(JdbcSelectExecutorStandardImpl.java:137) ~[hibernate-core-6.6.8.Final.jar:6.6.8.Final]
at org.hibernate.sql.exec.internal.JdbcSelectExecutorStandardImpl.executeQuery(JdbcSelectExecutorStandardImpl.java:102) ~[hibernate-core-6.6.8.Final.jar:6.6.8.Final]
at org.hibernate.sql.exec.spi.JdbcSelectExecutor.executeQuery(JdbcSelectExecutor.java:91) ~[hibernate-core-6.6.8.Final.jar:6.6.8.Final]
at org.hibernate.sql.exec.spi.JdbcSelectExecutor.list(JdbcSelectExecutor.java:165) ~[hibernate-core-6.6.8.Final.jar:6.6.8.Final]
at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.lambda$new$1(ConcreteSqmSelectQueryPlan.java:152) ~[hibernate-core-6.6.8.Final.jar:6.6.8.Final]
at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.withCacheableSqmInterpretation(ConcreteSqmSelectQueryPlan.java:442) ~[hibernate-core-6.6.8.Final.jar:6.6.8.Final]
at org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan.performList(ConcreteSqmSelectQueryPlan.java:362) ~[hibernate-core-6.6.8.Final.jar:6.6.8.Final]
at org.hibernate.query.sqm.internal.QuerySqmImpl.doList(QuerySqmImpl.java:380) ~[hibernate-core-6.6.8.Final.jar:6.6.8.Final]
at org.hibernate.query.spi.AbstractSelectionQuery.list(AbstractSelectionQuery.java:143) ~[hibernate-core-6.6.8.Final.jar:6.6.8.Final]
at org.hibernate.query.spi.AbstractSelectionQuery.getSingleResult(AbstractSelectionQuery.java:275) ~[hibernate-core-6.6.8.Final.jar:6.6.8.Final]@ActiveProfiles("local") - default (postgreSQL) 

javax.persistence.lock.timeout이나 jakarta.persistence.lock.timeout 설정을 했더라도, 사용 중인 데이터베이스의 JDBC 드라이버가 이 기능을 지원하지 않으면 설정이 적용되지 않을 수 있다.
예를 들어, MySQL, Oracle 등의 드라이버는 이 기능을 지원하지만, 일부 다른 드라이버는 지원하지 않을 수 있다.
의심이 확신이 되는 순간이었다.
물론 NOWAIT 설정은 제대로 동작한다.javax.persistence.lock.timeout = 0 으로 두면 NOWAIT으로 동작했다 (이것 때문에 더 헤맸던 것도 있다.)
네이티브 쿼리를 aop로 적용하여 해결했다@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LockTimeout {
int timeout() default 3000;
}
@Inherited: 이 어노테이션이 부모 클래스에서 자식 클래스로 상속됨을 나타낸다. 즉, 부모 클래스의 메서드에 이 어노테이션이 있으면 자식 클래스에서 오버라이드된 메서드에도 적용된다.@Retention(RetentionPolicy.RUNTIME): 이 어노테이션 정보가 런타임에도 유지되어야 함을 명시한다. 이렇게 해야 실행 중인 애플리케이션에서 리플렉션을 통해 어노테이션 정보를 읽을 수 있다.@Target(ElementType.METHOD): 이 어노테이션이 메서드에만 적용될 수 있음을 나타낸다.public @interface LockTimeout: LockTimeout이라는 이름의 어노테이션을 정의한다.int timeout() default 3000: 이 어노테이션의 속성으로 timeout을 정의하며, 기본값은 3000이다. 어노테이션 사용 시 이 값을 지정하지 않으면 기본값이 사용된다.테스트 환경은 h2, 실제 로컬 db 는 postgres 로 사용했는데, 둘의 문법이 달라 인터페이스로 정의해줬다.
public interface LockManager {
void setLockTimeout(int timeout);
}
@Slf4j
@Profile("Test")
@Component
@RequiredArgsConstructor
public class H2LockManager implements LockManager {
@Override
public void setLockTimeout(int timeout) {
log.info("테스트 환경에서는 락 타임아웃 설정이 무시됩니다: {}ms", timeout);
}
}
@Profile("!Test")
@Component
@RequiredArgsConstructor
public class PostgresLockManager implements LockManager {
private final EntityManager em;
public void setLockTimeout(int timeout) {
Query query = em.createNativeQuery("SET LOCAL lock_timeout = '" + timeout + "ms'");
query.executeUpdate();
}
}
@Aspect
@Component
@RequiredArgsConstructor
public class LockTimeoutAspect {
private final LockManager lockManager;
@Before("@annotation(takeoff.logistics_service.msa.product.stock.infrastructure.persistence"
+ ".aspect.LockTimeout)")
public void beforeLockTimeout(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
LockTimeout annotation = signature.getMethod().getAnnotation(LockTimeout.class);
lockManager.setLockTimeout(annotation.timeout());
}
}
이제 락 타임아웃 설정이 필요한 메서드에 다음과 같이 어노테이션을 추가하면 된다:
@LockTimeout(timeout = 1000)
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT s FROM Stock s WHERE s.id = :id AND s.deletedAt is null")
Optional<Stock> findByIdWithLock(@Param("id") StockId id);
정말 상상도 못한 부분에서 오류가 나서 당황했다..
그래도 빨리 확인 한 것이 다행이지 않나 싶기도하고.
지금에서는 타임아웃이 의미가 있나 싶긴하지만~ (무한대기라던지!)
해당 예외로 CircuitBreaker 같은 설정도 할 수 있지 않을까!
혹시라도 제가 간과한 부분이 있다면 언제든지 알려주세요!