동시성 문제를 공부하다 보면 race condition 다음으로 자주 만나는 개념이 데드락입니다.
데드락은 단순히 프로그램이 느려지는 문제가 아니라, 여러 작업이 서로를 기다리다가 아무도 앞으로 나아가지 못하는 상태입니다.
운영체제, 멀티스레드 프로그래밍, DB 트랜잭션, 서버 장애 분석에서 모두 등장하는 개념이라 면접에서도 자주 묻습니다.
데드락은 자원을 가진 작업들이 서로 다른 자원을 기다리면서 무한 대기 상태에 빠지는 문제입니다.
이번 글에서는 데드락이 발생하는 조건, 실제 코드와 DB 상황에서 어떻게 나타나는지, 실무에서는 어떻게 예방하고 추적하는지 정리해보겠습니다.
1. 데드락은 무엇인가?
데드락은 여러 실행 주체가 서로 자원을 기다리면서 더 이상 진행하지 못하는 상태입니다.
여기서 실행 주체는 프로세스, 스레드, 트랜잭션, 작업 큐의 worker가 될 수 있습니다.
자원은 mutex, 파일, DB row lock, connection, thread pool, 외부 API quota처럼 한 번에 제한된 수만 사용할 수 있는 대상입니다.
예를 들어 Thread A는 Lock 1을 가지고 Lock 2를 기다리고, Thread B는 Lock 2를 가지고 Lock 1을 기다리면 둘 다 끝나지 못합니다.
이 상태가 바로 데드락입니다.
| 개념 | 설명 |
| 작업 | 자원을 사용하려는 프로세스, 스레드, 트랜잭션 |
| 자원 | 락, 메모리, 파일, DB row, connection 등 제한된 대상 |
| 대기 | 다른 작업이 가진 자원이 풀리기를 기다리는 상태 |
| 데드락 | 서로가 가진 자원을 기다려 아무도 진행하지 못하는 상태 |
데드락은 CPU 사용률이 높지 않은데도 요청이 끝나지 않는 형태로 보일 수 있습니다.
그래서 단순히 서버가 바쁘다고 판단하면 원인을 놓치기 쉽습니다.
2. 데드락의 4가지 필요 조건
운영체제에서는 데드락이 발생하기 위한 필요 조건으로 보통 4가지를 설명합니다.
이 조건들이 동시에 만족될 때 데드락이 발생할 수 있습니다.
| 조건 | 의미 | 예시 |
| 상호 배제 | 자원을 한 번에 하나의 작업만 사용할 수 있음 | mutex lock, DB row lock |
| 점유와 대기 | 이미 가진 자원을 놓지 않은 채 다른 자원을 기다림 | Lock A를 잡고 Lock B 대기 |
| 비선점 | 다른 작업이 가진 자원을 강제로 빼앗을 수 없음 | 락 소유자가 unlock해야 함 |
| 순환 대기 | 작업들이 원형으로 서로의 자원을 기다림 | A는 B를, B는 A를 기다림 |
중요한 점은 이 4가지가 모두 필요 조건이라는 것입니다.
따라서 데드락 예방은 보통 이 조건 중 하나가 성립하지 않도록 설계하는 방식으로 접근합니다.
실무에서는 특히 순환 대기를 끊는 전략을 많이 사용합니다.
예를 들어 여러 락을 잡아야 한다면 항상 같은 순서로 잡도록 규칙을 정합니다.
3. 그림으로 이해하기
데드락 상황을 단순화하면 다음처럼 볼 수 있습니다.
- Deadlock wait cycle
Thread A
|
| holds
v
Lock 1
^
| waits for
|
Thread B
|
| holds
v
Lock 2
^
| waits for
|
Thread A
Thread A는 Lock 1을 가진 상태로 Lock 2를 기다립니다.
Thread B는 Lock 2를 가진 상태로 Lock 1을 기다립니다.
두 스레드 모두 상대가 락을 풀어야 진행할 수 있지만, 둘 다 대기 중이기 때문에 프로그램은 멈춘 것처럼 보입니다.
4. C++ 코드로 보는 데드락
C++에서 mutex를 잘못 다루면 데드락이 쉽게 발생할 수 있습니다.
다음 예제는 두 계좌 사이에서 돈을 옮기는 상황입니다.
#include <chrono>
#include <iostream>
#include <mutex>
#include <thread>
struct Account {
std::mutex mutex;
int balance = 0;
};
void transfer(Account& from, Account& to, int amount) {
std::lock_guard<std::mutex> first(from.mutex);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
std::lock_guard<std::mutex> second(to.mutex);
from.balance -= amount;
to.balance += amount;
}
int main() {
Account a{std::mutex{}, 100};
Account b{std::mutex{}, 100};
std::thread t1([&]() {
transfer(a, b, 10);
});
std::thread t2([&]() {
transfer(b, a, 20);
});
t1.join();
t2.join();
std::cout << a.balance << ' ' << b.balance << '\n';
}
t1은 a.mutex를 먼저 잡고 b.mutex를 기다립니다.
t2는 b.mutex를 먼저 잡고 a.mutex를 기다릴 수 있습니다.
타이밍이 맞으면 서로가 가진 락을 기다리게 되고 데드락이 발생합니다.
이 예제에서 sleep_for는 데드락 가능성을 더 잘 드러내기 위한 장치입니다.
5. C++에서는 어떻게 예방할까?
가장 단순한 예방 방법은 여러 락을 항상 같은 순서로 잡는 것입니다.
하지만 두 객체의 주소나 호출 방향에 따라 순서가 달라질 수 있다면 표준 라이브러리의 도구를 사용하는 편이 안전합니다.
C++17에서는 std::scoped_lock을 사용할 수 있습니다.
#include <iostream>
#include <mutex>
#include <thread>
struct Account {
std::mutex mutex;
int balance = 0;
};
void transfer(Account& from, Account& to, int amount) {
std::scoped_lock lock(from.mutex, to.mutex);
from.balance -= amount;
to.balance += amount;
}
int main() {
Account a{std::mutex{}, 100};
Account b{std::mutex{}, 100};
std::thread t1([&]() {
transfer(a, b, 10);
});
std::thread t2([&]() {
transfer(b, a, 20);
});
t1.join();
t2.join();
std::cout << a.balance << ' ' << b.balance << '\n';
}
std::scoped_lock은 여러 mutex를 한 번에 잠글 때 데드락을 피하도록 설계된 방식으로 락을 획득합니다.
또한 RAII 방식이라 스코프를 벗어나면 자동으로 unlock됩니다.
실무에서는 직접 lock, unlock을 흩뿌리기보다 lock_guard, unique_lock, scoped_lock 같은 RAII 도구를 우선 사용하는 것이 좋습니다.
| 도구 | 용도 |
| std::lock_guard | 하나의 mutex를 간단히 보호할 때 |
| std::unique_lock | lock/unlock 시점을 세밀하게 제어해야 할 때 |
| std::scoped_lock | 여러 mutex를 한 번에 안전하게 잠글 때 |
6. DB에서도 데드락이 발생한다
데드락은 운영체제나 C++ 스레드에서만 발생하지 않습니다.
DB 트랜잭션에서도 자주 발생합니다.
예를 들어 두 트랜잭션이 서로 다른 row를 먼저 수정한 뒤, 상대가 가진 row를 다시 수정하려고 하면 데드락이 발생할 수 있습니다.
- DB transaction deadlock
Transaction 1:
UPDATE account SET balance = balance - 10 WHERE id = 1;
UPDATE account SET balance = balance + 10 WHERE id = 2;
Transaction 2:
UPDATE account SET balance = balance - 20 WHERE id = 2;
UPDATE account SET balance = balance + 20 WHERE id = 1;
Transaction 1은 id 1 row lock을 잡고 id 2를 기다릴 수 있습니다.
Transaction 2는 id 2 row lock을 잡고 id 1을 기다릴 수 있습니다.
이 경우 DBMS는 데드락을 감지하고 둘 중 하나의 트랜잭션을 실패시킬 수 있습니다.
애플리케이션 입장에서는 이 오류를 단순 실패로만 보지 말고, 재시도 가능 여부와 트랜잭션 순서를 함께 확인해야 합니다.
7. 실무에서 데드락을 줄이는 방법
데드락을 완전히 없애기보다, 발생 가능성을 줄이고 발생했을 때 빠르게 복구할 수 있게 설계하는 것이 중요합니다.
- 여러 락을 잡아야 한다면 항상 같은 순서로 잡는다.
- 락을 잡은 채 오래 걸리는 I/O 작업을 하지 않는다.
- 락의 범위를 가능한 작게 유지한다.
- 수동 lock/unlock보다 RAII 기반 락 도구를 사용한다.
- DB 트랜잭션에서는 row 접근 순서를 통일한다.
- 트랜잭션을 짧게 유지하고 사용자 입력 대기를 트랜잭션 안에 넣지 않는다.
- 데드락 오류가 발생할 수 있는 구간에는 제한된 재시도 전략을 둔다.
특히 서버 개발에서는 락을 잡은 채 네트워크 호출이나 외부 API 호출을 하는 코드를 조심해야 합니다.
외부 호출이 느려지면 락 보유 시간이 길어지고, 다른 요청이 연쇄적으로 대기할 수 있습니다.
처음에는 단순한 지연처럼 보이지만, 실제로는 내부 락 경합이나 데드락에 가까운 대기 구조일 수 있습니다.
8. 데드락과 starvation은 다르다
데드락과 함께 자주 헷갈리는 개념이 starvation입니다.
둘 다 작업이 진행되지 않는 것처럼 보이지만 원인은 다릅니다.
| 구분 | 데드락 | Starvation |
| 원인 | 서로가 가진 자원을 순환 대기 | 특정 작업이 계속 우선순위에서 밀림 |
| 진행 가능성 | 외부 개입 없이는 진행 어려움 | 언젠가 진행될 수 있지만 계속 밀릴 수 있음 |
| 예시 | A와 B가 서로의 락을 기다림 | 낮은 우선순위 작업이 계속 실행되지 않음 |
| 해결 방향 | 락 순서, 타임아웃, 감지와 복구 | 공정한 스케줄링, 우선순위 조정 |
면접에서 이 차이를 물어보면, 데드락은 순환 대기 구조가 핵심이고 starvation은 공정성 문제가 핵심이라고 설명하면 좋습니다.
9. 데드락을 어떻게 추적할까?
실무에서 데드락을 만났을 때는 증상부터 확인해야 합니다.
요청이 끝나지 않는지, 특정 API만 멈추는지, DB 쿼리가 대기 중인지, thread dump에서 같은 락을 기다리는 스레드가 있는지 봐야 합니다.
추적할 때는 다음 정보를 모읍니다.
- 어떤 요청이나 작업이 멈췄는가?
- 어떤 락이나 DB row를 기다리고 있는가?
- 누가 그 자원을 잡고 있는가?
- 락을 잡은 상태에서 어떤 작업을 하고 있는가?
- 최근 배포에서 락 순서나 트랜잭션 순서가 바뀌었는가?
C++ 서버라면 thread dump, 로그, mutex 주변 로깅, sanitizer 도구를 함께 볼 수 있습니다.
DB라면 DBMS가 제공하는 lock wait 정보, deadlock log, slow query log, transaction history를 확인합니다.
중요한 것은 “어디서 멈췄는가”보다 “누가 무엇을 가진 채 무엇을 기다리는가”를 찾는 것입니다.
10. 면접 질문 예시
질문 1. 데드락의 4가지 조건을 설명해주세요.
데드락의 4가지 조건은 상호 배제, 점유와 대기, 비선점, 순환 대기입니다.
상호 배제는 자원을 하나의 작업만 사용할 수 있다는 뜻이고, 점유와 대기는 이미 자원을 가진 상태에서 다른 자원을 기다리는 상황입니다.
비선점은 다른 작업이 가진 자원을 강제로 빼앗을 수 없다는 뜻이고, 순환 대기는 작업들이 원형으로 서로의 자원을 기다리는 구조입니다.
질문 2. 데드락을 예방하는 방법은 무엇인가요?
데드락 예방은 4가지 조건 중 하나가 성립하지 않도록 만드는 방식입니다.
실무적으로는 여러 락을 항상 같은 순서로 획득하고, 락을 잡은 상태로 오래 걸리는 작업을 하지 않으며, 트랜잭션을 짧게 유지하는 방식이 많이 사용됩니다.
C++에서는 std::scoped_lock처럼 여러 mutex를 안전하게 잠그는 도구를 사용할 수 있습니다.
질문 3. DB 데드락이 발생하면 애플리케이션은 어떻게 처리해야 하나요?
DBMS는 데드락을 감지하면 보통 한 트랜잭션을 중단시키고 오류를 반환합니다.
애플리케이션은 이 오류를 기록하고, 재시도해도 안전한 작업인지 확인한 뒤 제한된 횟수로 재시도할 수 있습니다.
동시에 row 접근 순서, 트랜잭션 범위, 인덱스 상태를 점검해 데드락이 반복되는 원인을 줄여야 합니다.
11. 실무에서는 어떻게 봐야 할까?
실무에서 데드락은 “락을 조심하자” 정도로 끝낼 문제가 아닙니다.
락 순서, 트랜잭션 순서, 외부 호출, connection pool, thread pool이 모두 연결될 수 있습니다.
예를 들어 요청 처리 스레드가 DB connection을 잡은 채 외부 API를 기다리고, 다른 요청은 그 connection이나 row lock을 기다리면 전체 처리량이 급격히 떨어질 수 있습니다.
그래서 데드락을 볼 때는 코드 한 줄보다 자원 흐름을 봐야 합니다.
- 어떤 자원을 잡는가?
- 얼마나 오래 잡는가?
- 잡은 상태에서 다른 자원을 기다리는가?
- 모든 요청이 같은 순서로 자원을 잡는가?
- 실패했을 때 재시도와 롤백이 안전한가?
이 질문에 답할 수 있으면 데드락을 단순 CS 암기 개념이 아니라 실무 장애 분석 도구로 사용할 수 있습니다.
12. 정리
데드락은 여러 작업이 서로 자원을 기다리면서 아무도 진행하지 못하는 상태입니다.
핵심만 정리하면 다음과 같습니다.
- 데드락은 상호 배제, 점유와 대기, 비선점, 순환 대기가 동시에 만족될 때 발생할 수 있다.
- C++ mutex, DB row lock, 파일, connection 등 제한된 자원에서 데드락이 생길 수 있다.
- 여러 락을 항상 같은 순서로 잡으면 순환 대기를 줄일 수 있다.
- C++17에서는 std::scoped_lock으로 여러 mutex를 더 안전하게 잠글 수 있다.
- DB 데드락은 트랜잭션 순서, row 접근 순서, 재시도 전략과 함께 봐야 한다.
- 실무에서는 누가 어떤 자원을 가진 채 무엇을 기다리는지 추적하는 것이 핵심이다.
데드락을 이해한다는 것은 단순히 운영체제 이론을 외우는 것이 아닙니다.
프로그램과 DB가 실제로 어떤 자원을 어떤 순서로 잡고 놓는지 이해하는 것입니다.
이 관점이 있으면 동시성 버그나 서버 장애를 훨씬 더 구체적으로 분석할 수 있습니다.
'CS' 카테고리의 다른 글
| TCP 3-way handshake 쉽게 이해하기 (0) | 2026.06.30 |
|---|---|
| DB 인덱스가 있는데도 쿼리가 느린 이유 (0) | 2026.06.26 |
| Race condition이란? 동시성 버그와 임계 구역 이해하기 (0) | 2026.06.23 |