테스트는 2개의 스레드에서 진행합니다.
테스트를 위한 퍼블리셔와 리스너 코드입니다.
// 퍼블리셔
@Service
public class DeadLockTestEventProducer {
private final Logger log = LoggerFactory.getLogger(DeadLockTestEventProducer.class);
private final ApplicationEventPublisher publisher;
public DeadLockTestEventProducer(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
@Transactional
public void publishEvent(TestEvent event) {
// 코드 작성
log.info("call DeadLockTestEventProducer.publish(). event-id: {}", event.getId());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("wake up! event-id: {}", event.getId());
publisher.publishEvent(event);
}
}
// 리스너
@Service
public class DeadLockTestAfterCommitEventListener {
private final Logger log = LoggerFactory.getLogger(DeadLockTestAfterCommitEventListener.class);
@Transactional(propagation = Propagation.REQUIRES_NEW)
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) // AFTER_COMMIT -> 기본값이지만 테스트 확인용 명시
public void handleEvent(TestEvent event) {
log.info("call DeadLockTestAfterCommitEventListener.handleEvent(). event-id: {}", event.getId());
}
}
1. 데드락 발생 테스트
우선 hikariCP 설정을 다음과 같이 진행합니다.
- MaximumPoolSize = 2 (데드락을 피할 수 있는 최소 pool size 공식: Tn x (cm - 1) + 1 에서 1을 뺀 값)
spring:
config:
activate:
on-profile: requires-new-deadlock-occur
datasource:
hikari:
maximum-pool-size: 2 # 최대 커넥션 수
connection-timeout: 30000 # 커넥션을 가져올 때 대기할 최대 시간 (밀리초)
max-lifetime: 1800000 # 커넥션이 유지될 최대 시간 (밀리초)
이후 위의 퍼블리셔와 리스너 코드를 2개의 쓰레드에서 실행합니다.
테스트 코드입니다.
@Test
void 데드락이_발생한다() throws InterruptedException {
// given
ExecutorService executorService = Executors.newFixedThreadPool(2);
CountDownLatch latch = new CountDownLatch(2);
// when
// 스레드 1: 이벤트 1 발행
executorService.submit(() -> {
try {
deadLockTestEventProducer.publishEvent(new TestEvent(1L));
} finally {
latch.countDown();
}
});
// 스레드 2: 이벤트 2 발행
executorService.submit(() -> {
try {
deadLockTestEventProducer.publishEvent(new TestEvent(2L));
} finally {
latch.countDown();
}
});
latch.await(); // 두 스레드의 작업이 끝날 때까지 대기
// then
// 데드락으로 인해 커넥션 타임아웃 예외가 발생하는 것을 검증
}
데드락 발생 시나리오 설명
- 커넥션 풀: maximum-pool-size: 2 설정으로 DB 커넥션은 2개입니다.
- 쓰레드 1 시작: publishEvent(1L) 호출. @Transactional에 의해 커넥션 1을 획득합니다. (남은 커넥션: 1개)
- 쓰레드 2 시작: publishEvent(2L) 호출. @Transactional에 의해 커넥션 2를 획득합니다. (남은 커넥션: 0개)
- 쓰레드 1 진행: Thread.sleep(2000) 후 publisher.publishEvent(event)를 호출합니다.
- 쓰레드 1 블로킹: DeadLockTestEventListener의 handleEvent가 호출됩니다. 이 메서드는 REQUIRES_NEW이므로 새로운 커넥션을 요청합니다. 하지만 남은 커넥션이 없으므로 쓰레드 1은 커넥션 풀에 커넥션이 반납되기를 기다리며 대기(Blocking) 상태가 됩니다.
- 쓰레드 2 진행: Thread.sleep(2000) 후 publisher.publishEvent(event)를 호출합니다.
- 쓰레드 2 블로킹: DeadLockTestEventListener의 handleEvent가 호출되고, 마찬가지로 새로운 커넥션을 요청합니다. 커넥션 풀이 비어있으므로 쓰레드 2도 대기(Blocking) 상태가 됩니다.
- 데드락 !:
- 쓰레드 1은 커넥션 1을 점유한 채, 다른 커넥션을 기다립니다.
- 쓰레드 2는 커넥션 2를 점유한 채, 다른 커넥션을 기다립니다.
- 두 쓰레드 모두 자신이 점유한 커넥션을 해제하지 못하고, 서로가 커넥션을 반납하기만을 무한히 기다리는
교착 상태(Deadlock)에 빠집니다.
결국 Hikari의 connection-timeout 설정 시간(기본 30초)이 지나면
SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out. 예외가 발생하며
테스트가 실패하게 됩니다. 이 실패를 통해 데드락 발생을 확인할 수 있습니다.
실제 결과

---------------------------------------------------
2. 데드락 발생 예방 테스트
- 쓰레드 개수 2개
- MaximumPoolSize = 3 (데드락을 피할 수 있는 최소 pool size 공식: Tn x (cm - 1) + 1 )
spring:
config:
activate:
on-profile: requires-new-deadlock-not-occur
datasource:
hikari:
maximum-pool-size: 3 # 최대 커넥션 수
connection-timeout: 30000 # 커넥션을 가져올 때 대기할 최대 시간 (밀리초)
max-lifetime: 1800000 # 커넥션이 유지될 최대 시간 (밀리초)
이제 1에서와 동일한 코드를 실행시켜 보겠습니다.
실제 결과

데드락이 발생하지 않고 핸들러 코드가 적절히 실행된 것을 확인할 수 있습니다.
'SSA > Back' 카테고리의 다른 글
| [SSA] 비동기 이벤트 발행기 테스트 (1) | 2025.09.08 |
|---|---|
| [SSA] 알림 모듈 구조 개선 및 간단한 동시성 제어 .. (2) | 2025.08.30 |
| [SSA] 트랜잭션을 새로 만들어서 사용하면 ? (0) | 2025.08.29 |
| [SSA] 500명 알림 발송 6.4초 -> 0.27초, 카프카 컨슈머 병렬 처리 적용기 (3) | 2025.08.28 |
| [SSA] 수강 기능 및 알림 기능을 한 곳에서 처리하면 발생할 수 있는 문제 (1) | 2025.07.19 |