이지은님의 블로그
250502 - Java Spring 도서 앱 구현: 주문 후 재고 감소의 동시성 제어(2) - 비관락 트러블 슈팅 본문
▷ 오늘 배운 것
비관락 구현하면서 발생한 트러블 슈팅에 관해 포스팅해보겠다.
<<목차>>
1. 문제상황
2. 원인 분석
3. 해결 방법
1. 문제상황
비관락을 구현하고 비관락에 대한 테스트코드를 작성해보았다. 테스트 케이스는 2개의 테스트 멀티스레드를 실행시키는데 둘중 하나는 락 획득을 하고 5초 대기한다. 그리고 다른 스레드는 락이 있는 데이터에 접근하면서 예외를 던지도록 했다. 그런데 이런 에러가 발생한다.
org.h2.jdbc.JdbcSQLTimeoutException: Timeout trying to lock table "BOOKS"; SQL statement:
could not prepare statement [Table "USERS" not found (this database is empty)]
기존 코드는 다음과 같다.
@Test
void 락타임아웃_테스트() throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(2);
CountDownLatch latch = new CountDownLatch(2);
executorService.submit(() -> {
try {
bookStockService.lockAndHold(bookId); // 예: 락 잡고 5초 대기
} catch (InterruptedException e) {
System.out.println("락 유지 중 예외 발생: " + e.getMessage());
} finally {
latch.countDown();
}
});
executorService.submit(() -> {
try {
Thread.sleep(1000);
bookStockService.decreaseStock(bookId, 1);
} catch (Exception e) {
System.out.println("예외 발생: " + e.getClass().getSimpleName() + " - " + e.getMessage());
} finally {
latch.countDown();
}
});
latch.await();
executorService.shutdown();
}
두번째 스레드가 락을 획득하지 못하고 H2내부 타임아웃에 의해 예외가 발생했다.
2. 원인 분석
5초동안 락을 획득하고 대기하도록 발생하는데 비해 테스트용 데이터베이스인 H2의 특성상 예외가 조기에 발생
- H2 Database는 내부적으로 테이블 단위로 락을 걸며, 기본 설정에서 javax.persistence.lock.timeout을 완벽히 반영하지 못했다.
- 5초를 기다렸다가 타임아웃 발생하기보다는, 즉시 실패하는 경향이 있다.
이는 H2의 공식 문서에서도 H2타임아웃과 관련하여 설명했다.
"H2 uses table-level locking in some modes and might throw timeout exceptions earlier than expected in pessimistic scenarios."
따라서 해당 테스트는 H2에 적합하지 않다
하지만 H2를 사용한 이유는 다음과 같은 이유가 있었다. CI환경에서 DB를 사용할 일이 없도록 설정한 것이다.
✅ H2를 CI에서 쓰면 좋은 점
장점 | 설명 |
✔️ 설치 불필요 | 외부 DB(MySQL, PostgreSQL) 설치 필요 없음 |
✔️ 테스트 격리 | 테스트 실행 시마다 새로운 DB 인스턴스 사용 가능 (create-drop) |
✔️ 빠른 속도 | 메모리 내에서 동작하므로 매우 빠름 |
✔️ CI 친화적 | GitHub Actions, GitLab CI 등에서 추가 설치 없이 바로 사용 가능 |
다음과 같은 이유로 CI에 적합한 데이터베이스인 H2를 사용했는데 타임아웃 에러가 발생한 것이다.
3. 해결 과정
CI에 적합한 테스트 코드를 작성하기 위해 application-test.yml을 작성했다.
spring:
datasource:
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
database-platform: org.hibernate.dialect.H2Dialect
- mem:testdb: 메모리 기반 인메모리 DB 생성
- create-drop: 테스트 후 자동 삭제
- DB_CLOSE_DELAY=-1: 커넥션 종료 후에도 DB 유지 (세션 간 공유)
- DB_CLOSE_ON_EXIT=FALSE: JVM 종료 시에도 데이터베이스를 강제로 닫지 않도록 설정
- LOCK_TIMEOUT=10000: 비관적 락(Pessimistic Lock 등) 사용 시, 락을 기다릴 최대 시간을 설정.(해당 시간 안에 락을 획득하지 못하면 JdbcSQLTimeoutException 발생)
이를 설정하고 테스트 클래스에 @ActiveProfiles("test”)를 작성하면 해당 test로 작성한 H2를 사용할 수 있다.