Post

deadlock과 대응 및 예방

deadlock과 대응 및 예방

deadlock과 대응 및 예방

Deadlock: 대응 및 예방 전략

1. Deadlock 개요

Deadlock(교착 상태)은 둘 이상의 프로세스 또는 스레드가 서로가 점유한 자원을 기다리며 영원히 진행되지 못하는 상태를 의미한다.
주로 멀티스레드, 병렬 처리, 데이터베이스 트랜잭션 환경에서 발생한다.


2. Deadlock 발생 조건 (Coffman Conditions)

Deadlock은 다음 4가지 조건이 동시에 성립할 때만 발생한다.

조건설명
Mutual Exclusion (상호 배제)자원은 한 번에 하나의 프로세스만 사용 가능
Hold and Wait (점유 대기)자원을 가진 상태에서 다른 자원을 기다림
No Preemption (비선점)자원을 강제로 빼앗을 수 없음
Circular Wait (순환 대기)프로세스 간 자원 대기 관계가 순환 구조를 형성

3. 대응 전략 1) Detection & Recovery (탐지 후 복구)

이 방식은 Deadlock을 예방하지 않음 대신, 발생 가능성을 감수하고 자원 효율을 높인다.

즉, 프로세스나 트랜잭션이 자원을 요청하고 점유하는 과정에서 서로 기다리는 상황이 실제로 발생하는 것 자체는 허용

대신 시스템이 다음 순서로 처리합니다.

  1. 현재 자원 할당 상태를 관찰
  2. Deadlock이 발생했는지 탐지
  3. 발생했다면 희생 대상을 정함
  4. 강제로 종료하거나 자원을 빼앗아 순환 대기를 끊음

장점

  • 자원 활용률 높음

단점

  • 탐지 비용 발생
  • 복구 시 데이터 손실 가능

3.1 MySQL Deadlock 탐지하기

1
2
Transaction 12345 → Transaction 12346
Transaction 12346 → Transaction 12345

위와 같은 트랜잭션 구조에서 아래의 SQL을 통해 트랜잭션 구조를 분석해 보자면 아래와 같이 수행할 수 있음

1
SHOW ENGINE INNODB STATUS;
1
2
3
4
5
6
7
8
9
10
------------------------
LATEST DETECTED DEADLOCK
------------------------
2026-04-15 14:32:10 0x70000abc1234
*** (1) TRANSACTION:
TRANSACTION 12345, ACTIVE 5 sec updating or deleting
mysql tables in use 1, locked 1
LOCK WAIT 5 lock struct(s), heap size 1128, 3 row lock(s)
MySQL thread id 101, OS thread handle 0x70000abc, query id 555 localhost user
UPDATE account SET balance = balance - 100 WHERE id = 1
  • 트랜잭션 ID: 12345
  • 현재 수행 중 SQL
  • 상태: lock wait
1
2
3
4
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 42 page no 3 n bits 72 index PRIMARY of table `bank`.`account`
trx id 12345 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format
1
2
3
4
5
6
*** (2) TRANSACTION:
TRANSACTION 12346, ACTIVE 4 sec updating or deleting
mysql tables in use 1, locked 1
4 lock struct(s), heap size 1128, 2 row lock(s)
MySQL thread id 102, OS thread handle 0x70000def, query id 556 localhost user
UPDATE account SET balance = balance - 200 WHERE id = 2
  • X lock (exclusive lock) 요청 중
  • 이미 다른 트랜잭션이 점유 → 대기 상태
1
2
3
4
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 42 page no 3 n bits 72 index PRIMARY of table `bank`.`account`
trx id 12346 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: ...
  • 이 트랜잭션이 실제로 쥐고 있는 락
1
2
3
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 42 page no 4 n bits 72 index PRIMARY of table `bank`.`account`
trx id 12346 lock_mode X locks rec but not gap waiting
  • (2)도 다른 락을 기다리고 있음
1
2
*** WE ROLL BACK TRANSACTION (2)
 **/
  • MySQL이 victim 선택
  • (2) 트랜잭션 rollback 수행

3.2 왜 이런 방식을 쓰는가

Deadlock Prevention이나 Avoidance는 안전하지만 보통 제약이 큼

예를 들어:

1
2
3
- 모든 자원을 한 번에 요청하게 하면 병렬성이 떨어짐
- 자원 요청 순서를 강제하면 설계가 복잡해짐
- Banker’s Algorithm 같은 회피 기법은 상태 계산 비용이 큼

반면 Detection & Recovery는:

1
2
3
- 평소에는 비교적 자유롭게 자원 사용 가능
- 시스템 활용률이 높음
- 실제 Deadlock이 드문 환경에서는 효율적

즉, 평상시 성능과 유연성을 우선하고, 문제 발생 시 대가를 치르는 전략


3.2 Avoidance (회피)

방식

  • Deadlock이 발생할 가능성이 있는 상태를 사전에 회피

대표 알고리즘

  • Banker’s Algorithm

핵심 개념

  • 시스템이 항상 Safe State를 유지하도록 자원 할당

장점

  • Deadlock 발생 자체를 방지

단점

  • 자원 사용 패턴을 사전에 알아야 함
  • 현실 시스템에서는 적용 어려움

3.3 Prevention (예방)

방식

  • Deadlock의 4가지 조건 중 하나 이상을 제거

전략

조건 제거방법
Mutual Exclusion불가능 (대부분 자원은 공유 불가)
Hold and Wait 제거자원을 한 번에 요청
No Preemption 제거자원을 강제로 회수
Circular Wait 제거자원 요청 순서 정의

4. Spring에서의 대응 전략

4.1 Deadlock Detection (DB 레벨)

대부분의 RDBMS는 Deadlock 발생 시:

  • 하나의 트랜잭션을 강제 롤백
  • 예외 발생

4.2 Retry 전략

Deadlock은 “일시적 충돌”이므로 재시도하면 대부분 해결됨

1
2
3
4
5
@Retryable(
  value = DeadlockLoserDataAccessException.class,
  maxAttempts = 3,
  backoff = @Backoff(delay = 100)
)

4.2.1 retry가 만능은 아니다.

deadlock이 많다는 것은 이미 설계 상태가 나쁘다는 뜻.

  • lock 경쟁 심함
  • 트랜잭션 충돌 빈번

여기에 retry가 추가되면:

1
요청 증가 → Deadlock 증가 → retry 증가 → DB 부하 증가 → 더 많은 Deadlock

이러한 구조는 양의 피드백 루프를 통해 비선형적(폭발적)으로 리소스 사용량이 증가할 수 있음.

4.3 트랜잭션 범위 최소화

❌ 잘못된 예

1
2
3
4
5
@Transactional
public void process() {
    externalApiCall();   // 절대 금지
    repository.save(...);
}

✅ 올바른 구조

1
2
3
4
5
6
7
8
9
10
public void process() {
  externalApiCall();
  transactionalSave();
}

@Transactional
public void transactionalSave() {
  repository.save(...);
}

5. Deadlock 예방 기법 (실무 중심)

5.1 Access Order 통일 (가장 중요)

  • 모든 스레드가 같은 순서로 Lock 획득
  • Circular Wait 제거
1
List<Account> accounts = accountRepository.findAllByIdOrderByIdAsc(ids);

5.2 Select For Update 전략

명시적으로 Lock 순서를 통제

1
2
3
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT a FROM Account a WHERE a.id = :id")
Account findForUpdate(Long id);

5.3 Isolation Level 조정

기본:

  • MySQL: REPEATABLE READ
  • PostgreSQL: READ COMMITTED

튜닝 전략:

  • Deadlock 많으면 → READ COMMITTED 고려
  • 대신 Phantom Read 허용

5.4 Index 최적화

  • WHERE 조건에 맞는 인덱스 필수
  • 불필요한 Range Lock 방지

5.5 작은 단위 트랜잭션

  • Batch 쪼개기
  • Loop 내부 트랜잭션 분리
1
2
3
for (...) {
    processSingle(); // @Transactional
}

5.6 Optimistic Lock

1
2
@Version
private Long version;
  • 충돌 시 실패 → 재시도
  • Deadlock 자체를 구조적으로 회피

6. 안티패턴

  • ❌ 가장 위험
    • 긴 @Transactional
    • 외부 API 포함
    • 랜덤 순서 데이터 접근
  • ❌ 흔한 실수
    • JPA Lazy Loading 방치
    • 인덱스 없이 UPDATE
    • Batch 한 트랜잭션 처리
This post is licensed under CC BY 4.0 by the author.