티스토리 뷰
도입
이번 레벨2 미션에서 예약이 취소된 경우 대기 1번 예약을 자동으로 승격시키는 API를 구현하였다. 한 트랜잭션 내에서 기존 예약을 취소하고, 대기 예약 조회하고, 승격되는 로직이 담겨, 예약 정보 Row에 대해 베타 락을 걸어주게 되었다.
이때, 사용자 요청이 집중되어 락 경합(lock contention)이 발생하는 경우 어떤 방식으로 처리해야 하는지, 특히 데드락(Deadlock)이 발생한 경우 실제 데이터베이스는 어떤 방식으로 해결하는지 알아보기로 하였다.
데드락(Deadlock)이란?
데드락은 기본적으로 멀티 트랜잭션 환경에서 발생하는 락 경합 상황이다. 둘 이상의 트랜잭션이 서로 점유한 자원을 상대방이 해제해주기를 기다리며 무한히 대기하는 현상으로, 어느 트랜잭션도 더 이상 진행할 수 없는 상태를 의미한다.

데드락이 발생하는 경우
언제 데드락이 발생할 수 있을까? 이론적으로는 아래의 케이스를 모두 만족하는 경우가 데드락이 발생할 수 있는 조건이다. 즉, 아래 4가지는 필요 조건이다.
- 상호 배제: 하나의 자원은 한 번에 하나의 트랜잭션만 사용할 수 있다.
- 점유와 대기: 이미 어떤 락을 가진 트랜잭션이 다른 락을 추가로 기다리는 상황이어야 한다.
- 비선점: 다른 트랜잭션이 가진 락을 강제로 빼앗을 수 없다.(즉, 락을 가진 트랜잭션이 직접 commit, rollback 해야 락이 해제되고 획득할 수 있다.)
- 순환 대기: 트랜잭션들이 서로가 가진 자원을 원형으로 기다린다.
사실 이런 원론적인 개념은 데드락의 정의를 온전히 이해하였다면, 당연한 조건들이다. 그래서 우리는 이론적인 내용을 넘어, 실제 서버 환경에서 어떤 케이스에 데드락이 발생할 수 있을지를 알아 볼 필요가 있다. 대표적으로 다음과 같은 상황에서 데드락이 발생한다.
- 여러 행을 서로 다른 순서로 업데이트
- 부모 - 자식 테이블(FK)로 인한 잠금 전파
- 인덱스 범위 조회(next-key lock)
- INSERT와 UNIQUE 인덱스 충돌
- 배치 작업과 일반 트랜잭션이 동시에 같은 데이터를 수정
위 사례 중 2번 케이스인 "외래키 락 전파 상황"을 조금 더 자세히 살펴보자. 실제로 비교적 자주 발생할 수 있는 패턴이고, 여러 연관 관계가 맺힌 테이블 구조에서는 트랜잭션이 실제로 어떤 락을 획득하는지 정확히 인지하지 못한 채 넘어가기 쉽다.
부모 - 자식 테이블(FK)로 인한 잠금 전파
해당 포스트는 이 예시를 기반으로 토이 프로젝트를 작성했으며(아래 데드락 발생 시나리오 테스트 바로가기), 이를 기반으로 설명합니다.
외래 키 제약으로 인해 부모 테이블과 자식 테이블 사이에 락 대기 관계가 전파되면, 단순한 단일 테이블 업데이트처럼 보이는 작업도 데드락의 원인이 될 수 있다. 그 이유는 외래키 컬럼에 대한 변경 작업(Insert, Update)이 수행되는 경우, 부모, 자식 테이블에 데이터가 존재하는 확인하는 작업이 선행되기 때문이다.
만약 이때, 부모 혹은 자식 테이블의 대상 Row에 락이 걸렸다면, 연관 관계를 맺고 있는 여러 테이블로 잠금이 전파된다. 해당 변경 작업을 위해 외래키 컬럼에 조회(공유)락이 걸리게 되고, 이로 인해 데드락이 발생할 수 있다.
예시 케이스로, 다음과 같은 테이블 구조를 살펴보자.

현재 Order 테이블의 account_id는 accounts 테이블의 외래키를 참조하고 있다. 만약 order_id가 1인 특정 Order Row에 대한 변경 작업(Insert, Update)이 실행된다면, account_id가 유효한지 Accounts 테이블의 테이블에 조회 작업이 선행되야 한다. 이때 데이터베이스는 FK의 무결성을 위해, Accounts의 해당 Row에 공유 락(S락)을 걸기 때문에 데드락이 발생한다. (FK 제약 검증 과정에서 InnoDB는 부모 레코드에 공유 record lock을 획득한다.)
1. 트랜잭션A가 accounts id = 1 레코드에 대한 X락 획득
UPDATE accounts SET balance = balance + 10000 WHERE id = 1;
2. 트랜잭션B가 accounts id = 2 레코드에 대한 X락 획득
UPDATE accounts SET balance = balance + 10000 WHERE id = 2;
3. 트랜잭션A가 account_id = 2를 갖는 orders의 새로운 레코드 생성
INSERT INTO orders (id, account_id, status) VALUES (101, 2, 'CREATED_BY_TX_A'); -- 이때, accounts의 id = 2 레코드에 대한 S락 획득 실패 후 대기
4. 트랜잭션B가 account_id = 1를 갖는 orders의 새로운 레코드 생성
INSERT INTO orders (id, account_id, status) VALUES (102, 1, 'CREATED_BY_TX_A'); -- 이때, accounts의 id = 1 레코드에 대한 S락 획득 실패 후 대기
이와 같이, FK 검증 과정에서 필요한 락 (여기서는 부모테이블 accounts의 S락) 이, 다른 트랜잭션이 이미 가진 호환되지 않는 락 (여기서는 X락) 과 충돌하고, 그 충돌이 서로 순환될 때 (여기서는 트랜잭션A와 B가 서로 순환), 데드락이 발생한다.
이 예시를 선택한 이유는 데드락이 항상 어플리케이션에서 명시적으로 설정한 락 때문에만 발생하는 것이 아닌 것을 설명하고 싶었다. FK 무결성을 보장하기 위해 데이터베이스가 내부적으로 획득하는 락처럼, 개발자가 직접 설정하지 않은 락도 데드락의 원인이 될 수 있다. 따라서 모든 데드락 발생 가능성을 코드 수준에서 완전히 예측하고 제거하기는 어렵고, 일관된 락 획득 순서와 함께 데드락 발생 시 재시도 같은 방어 전략이 필요하다.
데드락이 발생하는 경우 문제점
데드락이 발생하는 것을 사전에 완전히 차단하기 어렵다는 점은 이해했다. 그렇다면, 운영 중에 데드락이 발생한다면, 바로 DB에 장애가 일어나고 멈춰버리게 될까?
결론부터 말하자면, 그렇지 않다. 데드락으로 인해 데이터베이스의 역할을 온전히 수행하지 못한다면, 어플리케이션 전체가 멈춰버리는 문제가 발생한다. 특히 멀티스레드 환경에서 데드락이 발생하는 경우, 어플리케이션 단에서 커넥션을 계속 물고 있을 수 있고, 대기시간이 길어져 클라이언트가 재요청을 보내는 경우 같은 레코드에 대해 같은 이유로 데드락이 걸려, 커넥션 풀이 줄어드는 문제도 발생할 수 있다.
따라서 기본적으로 데이터베이스는 데드락을 탐지하는 기능을 제공한다. Mysql의 스토리지 엔진인 InnoDB는 주기적으로 데드락이 발생했는지를 감지하고, 관련 트랜잭션 중 하나를 희생자로 선택해 롤백시키는 작업으로 교착 상황을 풀어준다. 이에 따라 개발자는 희생자 트랜잭션을 재실행하여 작업을 완수할 수 있도록 구성해야한다.
데이터베이스가 데드락을 처리하는 방식(with Mysql InnoDB)
InnoDB 스토리지 엔진은 내부적으로 락이 교착 상태에 빠지지 않았는지 체크하기 위해, 잠금 대기 목록을 그래프(Wait-for Graph) 형태로 관리한다. InnoDB은 내부적으로 데드락 감지 스레드를 가지고 있어서, 잠금 대기 그래프를 조회하여 교착 상태에 빠진 트랜잭션을 찾아 하나를 강제 종료한다.
그렇다면, 데드락 감지 스레드는 언제 작동할까? 보통 트랜잭션이 락을 기다리게 되는 순간(대기 상태로 들어가는 순간), InnoDB는 데드락 여부를 확인하기 위해 wait-for graph를 탐색한다. 이때 순환 대기가 탐지된다면, 데드락으로 판단한다. 또한 아래와 같은 케이스도 데드락으로 판단한다.
- 대기 그래프(wait-for list)에 있는 트랜잭션 수가 200개를 초과한 경우
- 대기 중인 트랜잭션들이 보유한 락을 조사하는 과정에서 1,000,000개 이상의 락을 확인해야 하는 경우
데드락이 탐지되면, 관련된 트랜잭션 중 하나를 victim으로 선택해 롤백한다. 여기서 어떤 트랜잭션을 victim으로 선정할까? InnoDB는 일반적으로 삽입, 수정, 삭제한 행의 수가 적은 작은 트랜잭션을 우선적으로 victim으로 선정한다. 즉, 언두 로그를 더 적게 가진 트랜잭션이 일반적으로 롤백의 대상이 된다.
여기서 주의할 점은 InnoDB 스토리지 엔진은 상위 레이어인 MySQL 엔진에서 관리되는 테이블 잠금(LOCK TABLES 명령으로 잠긴 테이블)은 볼 수가 없어서 데드락 감지가 불확실할 수 있기에, innodb_table_locks 시스템 변수를 활성화하여, 테이블 레벨의 잠금까지 감지할 수 있도록 기능을 제공한다.
이처럼 데이터베이스는 자동으로 데드락을 탐지하고, 교착상태를 해결해준다.
자동 데드락 감지의 문제점
하지만 높은 동시성 환경에서 많은 트랜잭션이 동일한 락을 기다리는 경우, 데드락 탐지 스레드가 느려진다. 데드락 감지 스레드도 락 목록을 조회하기 위해 그래프에 새로운 락을 걸고 탐지 작업을 수행하기 때문이다. 결국 성능 저하를 유발할 수 있다. 이러한 상황에서는 innodb_deadlock_detect 시스템 변수를 OFF로 설정하여, 데드락 탐지를 비활성화하고, 대신innodb_lock_wait_timeout 값에 의존하여 락 대기 시간이 초과되었을 때 트랜잭션을 롤백하는 것이 더 효율적일 수 있다. (이는 Mysql 공식문서에 나와있는 실제가이드이다.)
래퍼런스
Mysql의 잠금(Lock)과 데드락(DeadLock) 발생
Mysql-Deadlock Detection
MySQL 데드락 자동 감지 및 비활성화 시 대응
'우아한테크코스' 카테고리의 다른 글
| [레벨1] 10가지 질문으로 살펴보는 장기 미션 회고 (1) | 2026.04.18 |
|---|---|
| [OOP] 객체 지향 프로그래밍 개론 (0) | 2026.04.03 |
| [테코톡] 기존 테스트를 넘어, 무작위 입력으로 살펴보는 Fuzz Testing (0) | 2026.03.21 |
| [우아한 테크코스 8기] 백엔드 최종 합격 후기 (0) | 2026.01.25 |
| 도메인 검증 프레임워크 Aegis 구현기 7편(완결): 오픈미션 최종 회고록 - Aegis를 만들며 배우고 깨달은 것들 (0) | 2025.11.19 |
