본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 28. · 0 Views
교착 상태 (Deadlock) 완벽 가이드
운영체제에서 발생하는 교착 상태의 개념부터 예방, 회피, 탐지까지 완벽하게 이해할 수 있는 가이드입니다. 실무에서 마주칠 수 있는 동시성 문제를 해결하는 핵심 지식을 담았습니다.
목차
1. 교착 상태란?
어느 날 김개발 씨가 멀티스레드 프로그램을 작성하다가 이상한 현상을 발견했습니다. 프로그램이 아무런 오류 메시지도 없이 그냥 멈춰버린 것입니다.
CPU 사용률은 0%인데, 프로그램은 끝나지도 않고 그대로 얼어붙어 있습니다. 도대체 무슨 일이 일어난 걸까요?
**교착 상태(Deadlock)**는 두 개 이상의 프로세스나 스레드가 서로가 가진 자원을 기다리며 무한히 대기하는 상태를 말합니다. 마치 좁은 골목에서 두 자동차가 마주쳐 서로 비켜주기를 기다리는 상황과 같습니다.
어느 쪽도 먼저 양보하지 않으면 영원히 그 자리에 갇히게 됩니다.
다음 코드를 살펴봅시다.
// 교착 상태 발생 예제
public class DeadlockExample {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) {
// 스레드 1: lockA를 먼저 획득한 후 lockB를 요청
Thread thread1 = new Thread(() -> {
synchronized (lockA) {
System.out.println("Thread 1: lockA 획득");
try { Thread.sleep(100); } catch (Exception e) {}
synchronized (lockB) {
System.out.println("Thread 1: lockB 획득");
}
}
});
// 스레드 2: lockB를 먼저 획득한 후 lockA를 요청
Thread thread2 = new Thread(() -> {
synchronized (lockB) {
System.out.println("Thread 2: lockB 획득");
try { Thread.sleep(100); } catch (Exception e) {}
synchronized (lockA) {
System.out.println("Thread 2: lockA 획득");
}
}
});
thread1.start();
thread2.start();
}
}
김개발 씨는 입사 6개월 차 주니어 개발자입니다. 오늘 그에게 주어진 미션은 여러 사용자가 동시에 접속하는 채팅 서버를 개발하는 것이었습니다.
멀티스레드 프로그래밍을 공부한 대로 열심히 코드를 작성했는데, 테스트 중에 갑자기 프로그램이 멈춰버렸습니다. "어?
왜 갑자기 안 움직이지?" 김개발 씨가 당황해하며 화면을 들여다봅니다. 에러 메시지도 없고, 프로그램이 종료된 것도 아닙니다.
그냥 아무것도 하지 않고 가만히 있을 뿐입니다. 옆자리 선배 박시니어 씨가 다가와 상황을 살펴봅니다.
"아, 이거 교착 상태에 빠진 거네요. Deadlock이라고도 부르는데, 멀티스레드 프로그래밍에서 가장 무서운 버그 중 하나예요." 그렇다면 교착 상태란 정확히 무엇일까요?
쉽게 비유하자면, 교착 상태는 마치 좁은 일방통행 도로에서 두 자동차가 마주친 상황과 같습니다. A 자동차는 B 자동차가 후진해주기를 기다리고, B 자동차는 A 자동차가 후진해주기를 기다립니다.
둘 다 "상대방이 먼저 비켜줘야 해"라고 생각하며 꿈쩍도 하지 않습니다. 결국 둘 다 영원히 그 자리에 갇히게 됩니다.
컴퓨터 세계에서도 마찬가지입니다. 스레드 1은 자원 A를 가지고 있으면서 자원 B를 기다립니다.
동시에 스레드 2는 자원 B를 가지고 있으면서 자원 A를 기다립니다. 서로가 서로의 자원을 기다리고 있으니, 어느 쪽도 진행할 수 없습니다.
위의 코드를 살펴보면 이 상황이 명확하게 보입니다. thread1은 lockA를 먼저 획득한 뒤 lockB를 요청합니다.
반면 thread2는 lockB를 먼저 획득한 뒤 lockA를 요청합니다. 두 스레드가 거의 동시에 실행되면, thread1은 lockA를 갖고 lockB를 기다리고, thread2는 lockB를 갖고 lockA를 기다리게 됩니다.
교착 상태의 가장 무서운 점은 조용히 일어난다는 것입니다. 프로그램이 크래시 나거나 에러 메시지를 출력하지 않습니다.
그냥 아무 일도 하지 않고 멈춰 있을 뿐입니다. 때문에 디버깅하기가 매우 까다롭습니다.
실제 현업에서 교착 상태는 데이터베이스 트랜잭션에서 자주 발생합니다. 예를 들어, 은행 시스템에서 A 계좌에서 B 계좌로 송금하는 트랜잭션과 B 계좌에서 A 계좌로 송금하는 트랜잭션이 동시에 실행될 때 교착 상태가 발생할 수 있습니다.
박시니어 씨가 김개발 씨에게 말합니다. "교착 상태를 이해하는 것이 첫 번째 단계예요.
이제 왜 발생하는지, 어떻게 해결하는지 하나씩 알아볼까요?" 김개발 씨는 고개를 끄덕이며 노트를 꺼냈습니다. 오늘 배울 것이 많을 것 같습니다.
실전 팁
💡 - 교착 상태는 에러 메시지 없이 프로그램이 멈추므로, 일정 시간이 지나도 응답이 없으면 교착 상태를 의심해보세요
- 스레드 덤프를 분석하면 어떤 스레드가 어떤 락을 기다리고 있는지 확인할 수 있습니다
2. 교착 상태 4가지 조건
박시니어 씨가 화이트보드 앞으로 김개발 씨를 불렀습니다. "교착 상태가 발생하려면 반드시 4가지 조건이 동시에 만족해야 해요.
이걸 코프만 조건이라고 부르는데, 하나라도 깨뜨리면 교착 상태를 막을 수 있어요."
교착 상태의 4가지 필요조건은 상호 배제, 점유와 대기, 비선점, 순환 대기입니다. 이 네 가지 조건이 모두 동시에 성립할 때만 교착 상태가 발생합니다.
반대로 말하면, 이 중 하나라도 성립하지 않게 만들면 교착 상태를 예방할 수 있습니다.
다음 코드를 살펴봅시다.
// 교착 상태 4가지 조건을 보여주는 의사 코드
public class DeadlockConditions {
// 1. 상호 배제 (Mutual Exclusion)
// 자원은 한 번에 하나의 프로세스만 사용할 수 있음
private final Lock printerLock = new ReentrantLock();
// 2. 점유와 대기 (Hold and Wait)
// 자원을 가진 채로 다른 자원을 기다림
public void holdAndWait() {
synchronized (resourceA) { // resourceA를 점유한 상태에서
synchronized (resourceB) { // resourceB를 대기
// 작업 수행
}
}
}
// 3. 비선점 (No Preemption)
// 다른 프로세스의 자원을 강제로 빼앗을 수 없음
// OS가 강제로 락을 해제하지 않음
// 4. 순환 대기 (Circular Wait)
// P1 -> R1 -> P2 -> R2 -> P1 형태의 순환 구조
}
박시니어 씨가 화이트보드에 큰 원 네 개를 그렸습니다. "자, 이게 바로 코프만 조건이에요.
1971년에 에드워드 코프만이라는 학자가 정리한 건데, 교착 상태가 발생하려면 이 네 가지가 반드시 동시에 만족해야 해요." 첫 번째 조건은 **상호 배제(Mutual Exclusion)**입니다. 이건 자원을 한 번에 하나의 프로세스만 사용할 수 있다는 뜻입니다.
프린터를 생각해보세요. 두 사람이 동시에 하나의 프린터로 서로 다른 문서를 출력하면 엉망이 되겠죠?
그래서 프린터는 한 번에 한 사람만 사용합니다. 두 번째 조건은 **점유와 대기(Hold and Wait)**입니다.
이미 자원을 하나 가지고 있으면서 다른 자원을 추가로 요청하는 상황입니다. 마치 식당에서 포크는 이미 들고 있는데 나이프가 없어서 기다리는 상황과 비슷합니다.
포크를 내려놓지 않고 나이프를 기다리는 것이죠. 세 번째 조건은 **비선점(No Preemption)**입니다.
다른 프로세스가 가진 자원을 강제로 빼앗을 수 없다는 뜻입니다. 누군가 프린터를 사용하고 있을 때 "비켜!
내가 먼저 쓸 거야!"라고 하며 강제로 빼앗을 수 없습니다. 사용이 끝날 때까지 기다려야 합니다.
네 번째 조건은 **순환 대기(Circular Wait)**입니다. 이게 가장 중요합니다.
프로세스들 사이에 자원 요청이 원형으로 순환하는 구조가 만들어지는 것입니다. P1이 P2가 가진 자원을 기다리고, P2가 P3이 가진 자원을 기다리고, P3이 다시 P1이 가진 자원을 기다리는 상황입니다.
김개발 씨가 손을 들었습니다. "그러면 이 네 가지 중 하나라도 안 되면 교착 상태가 안 일어나는 건가요?" "정확해요!" 박시니어 씨가 답했습니다.
"교착 상태를 예방하는 전략도 바로 이 조건들 중 하나를 깨뜨리는 거예요. 어떤 조건을 깨뜨리느냐에 따라 다양한 해결 방법이 있어요." 실제로 데이터베이스 시스템들은 이 원리를 활용합니다.
예를 들어, 모든 테이블에 순서를 부여하고 항상 낮은 번호의 테이블부터 락을 획득하도록 강제하면 순환 대기 조건을 깰 수 있습니다. 김개발 씨가 노트에 열심히 적었습니다.
"상호 배제, 점유와 대기, 비선점, 순환 대기... 이 네 가지 조건이 동시에 만족하면 교착 상태, 하나라도 깨지면 교착 상태 없음!"
실전 팁
💡 - 교착 상태를 분석할 때 이 4가지 조건이 모두 성립하는지 확인해보세요
- 순환 대기 조건을 깨는 것이 가장 실용적인 예방 방법입니다
3. 자원 할당 그래프
박시니어 씨가 화이트보드에 동그라미와 네모를 그리기 시작했습니다. "교착 상태를 눈으로 쉽게 확인하는 방법이 있어요.
바로 자원 할당 그래프예요. 이걸 그려보면 교착 상태가 있는지 한눈에 알 수 있어요."
**자원 할당 그래프(Resource Allocation Graph)**는 프로세스와 자원 사이의 관계를 그래프로 표현한 것입니다. 프로세스는 원으로, 자원은 사각형으로 표시하고, 화살표로 요청과 할당 관계를 나타냅니다.
그래프에 순환(cycle)이 있으면 교착 상태가 발생할 가능성이 있습니다.
다음 코드를 살펴봅시다.
// 자원 할당 그래프를 코드로 표현
class ResourceAllocationGraph {
// 프로세스: P1, P2, P3
// 자원: R1(인스턴스 1개), R2(인스턴스 2개)
// 할당 간선 (Assignment Edge): 자원 -> 프로세스
// R1 -> P1 (R1이 P1에 할당됨)
// R2 -> P2 (R2의 인스턴스가 P2에 할당됨)
// 요청 간선 (Request Edge): 프로세스 -> 자원
// P1 -> R2 (P1이 R2를 요청)
// P2 -> R1 (P2가 R1을 요청)
// 순환 탐지 예시
public boolean hasCycle(Map<String, List<String>> graph) {
Set<String> visited = new HashSet<>();
Set<String> recursionStack = new HashSet<>();
for (String node : graph.keySet()) {
if (detectCycle(node, graph, visited, recursionStack)) {
return true; // 교착 상태 가능성 있음
}
}
return false;
}
}
박시니어 씨가 화이트보드에 원 두 개와 사각형 두 개를 그렸습니다. "자, 여기 P1과 P2는 프로세스예요.
동그라미로 표시해요. 그리고 R1과 R2는 자원이에요.
네모로 표시하죠." 김개발 씨가 고개를 끄덕였습니다. "그러면 화살표는 뭔가요?" "좋은 질문이에요.
화살표는 두 가지가 있어요." 박시니어 씨가 화살표를 그리며 설명했습니다. "프로세스에서 자원으로 가는 화살표는 요청 간선이에요.
'이 자원을 쓰고 싶어요'라는 뜻이죠. 반대로 자원에서 프로세스로 가는 화살표는 할당 간선이에요.
'이 자원은 이 프로세스가 사용 중이에요'라는 뜻입니다." 박시니어 씨가 예시를 그렸습니다. P1에서 R1으로 화살표, R1에서 P2로 화살표, P2에서 R2로 화살표, 그리고 R2에서 P1으로 화살표를 그렸습니다.
"자, 이 그림을 보세요. P1 -> R1 -> P2 -> R2 -> P1...
어떤가요?" "원을 그리네요!" 김개발 씨가 외쳤습니다. "맞아요, 이게 바로 **순환(Cycle)**이에요.
순환이 있으면 교착 상태가 발생할 가능성이 있어요." 박시니어 씨가 설명을 이어갔습니다. "자원의 인스턴스가 하나뿐일 때 순환이 있으면 반드시 교착 상태예요.
하지만 자원의 인스턴스가 여러 개면 순환이 있어도 교착 상태가 아닐 수 있어요." 김개발 씨가 고개를 갸웃거렸습니다. "인스턴스가 여러 개라는 건 뭔가요?" "예를 들어 프린터가 3대 있다면, 프린터라는 자원의 인스턴스가 3개인 거예요.
한 프로세스가 프린터 하나를 사용 중이어도 나머지 두 대는 사용 가능하잖아요?" 박시니어 씨가 설명했습니다. 자원 할당 그래프는 운영체제 수업에서 교착 상태를 설명할 때 가장 많이 사용되는 도구입니다.
복잡한 상황도 그림으로 그려보면 한눈에 이해할 수 있기 때문입니다. 실제 시스템에서는 이 그래프를 자동으로 생성하고 순환을 탐지하는 알고리즘을 사용합니다.
위의 코드처럼 DFS(깊이 우선 탐색)를 사용해서 그래프에 순환이 있는지 확인할 수 있습니다. 김개발 씨가 노트에 그래프를 따라 그리며 말했습니다.
"이제 교착 상태를 그림으로 볼 수 있게 됐어요. 순환만 찾으면 되는 거네요!"
실전 팁
💡 - 자원 할당 그래프를 그릴 때 요청 간선과 할당 간선의 방향을 헷갈리지 마세요
- 자원의 인스턴스 개수에 따라 순환이 있어도 교착 상태가 아닐 수 있음을 기억하세요
4. 교착 상태 예방
김개발 씨가 질문했습니다. "그러면 교착 상태가 아예 일어나지 않게 하려면 어떻게 해야 하나요?" 박시니어 씨가 미소 지으며 답했습니다.
"바로 4가지 조건 중 하나를 깨뜨리면 돼요. 이걸 교착 상태 예방이라고 해요."
**교착 상태 예방(Deadlock Prevention)**은 교착 상태의 4가지 필요조건 중 하나 이상을 원천적으로 성립하지 않게 만드는 방법입니다. 가장 실용적인 방법은 자원에 순서를 부여하여 순환 대기 조건을 깨는 것입니다.
이 방법은 많은 실제 시스템에서 사용됩니다.
다음 코드를 살펴봅시다.
// 교착 상태 예방: 자원 순서 부여로 순환 대기 방지
public class DeadlockPrevention {
// 자원에 순서 부여: lockA=1, lockB=2
// 항상 낮은 번호의 락부터 획득
private static final Object lockA = new Object(); // 순서 1
private static final Object lockB = new Object(); // 순서 2
// 올바른 방법: 두 스레드 모두 같은 순서로 락 획득
public void thread1Work() {
synchronized (lockA) { // 먼저 순서 1
synchronized (lockB) { // 그다음 순서 2
// 작업 수행
}
}
}
public void thread2Work() {
synchronized (lockA) { // 먼저 순서 1 (동일한 순서!)
synchronized (lockB) { // 그다음 순서 2
// 작업 수행
}
}
}
// 이제 순환 대기가 발생할 수 없음!
}
박시니어 씨가 화이트보드의 4가지 조건을 다시 가리켰습니다. "교착 상태 예방은 이 조건들 중 하나를 아예 불가능하게 만드는 거예요.
하나씩 살펴볼까요?" "먼저 상호 배제를 깨는 방법이 있어요. 하지만 이건 현실적으로 어려워요.
프린터처럼 물리적으로 한 번에 하나만 쓸 수 있는 자원이 있으니까요. 다만 읽기 전용 파일처럼 공유 가능한 자원은 상호 배제가 필요 없죠." "점유와 대기를 깨는 방법도 있어요." 박시니어 씨가 이어갔습니다.
"프로세스가 시작할 때 필요한 모든 자원을 한꺼번에 요청하는 거예요. 전부 받거나 아무것도 못 받거나, 둘 중 하나죠.
하지만 이 방법은 자원 낭비가 심해요. 나중에 쓸 자원까지 미리 점유하고 있으니까요." "비선점을 깨는 방법은 자원을 강제로 빼앗을 수 있게 하는 거예요.
CPU 같은 자원은 타이머 인터럽트로 강제로 빼앗을 수 있죠. 하지만 프린터 중간에 빼앗으면 출력이 엉망이 되겠죠?
그래서 모든 자원에 적용하긴 어려워요." "가장 실용적인 건 순환 대기를 깨는 거예요!" 박시니어 씨가 강조했습니다. "모든 자원에 번호를 부여하고, 항상 낮은 번호의 자원부터 요청하게 하는 거예요." 위의 코드를 보면 lockA에는 순서 1, lockB에는 순서 2를 부여했습니다.
이제 모든 스레드는 반드시 lockA를 먼저 획득한 후에 lockB를 획득해야 합니다. 이렇게 하면 순환 구조가 만들어질 수 없습니다.
김개발 씨가 물었습니다. "그러면 처음 제가 짠 코드에서 thread2가 lockA를 먼저 획득하도록만 바꾸면 되는 건가요?" "정확해요!" 박시니어 씨가 답했습니다.
"두 스레드가 같은 순서로 락을 획득하면 한 스레드가 lockA를 갖고 있을 때 다른 스레드는 lockA를 기다리죠. lockB까지 갈 일이 없어요.
그래서 순환이 생길 수 없어요." 이 방법은 실제 데이터베이스에서도 많이 사용됩니다. 테이블 이름순으로 락을 획득하거나, ID 순서대로 락을 획득하는 규칙을 정해두는 것입니다.
실전 팁
💡 - 자원 순서를 정할 때는 팀 전체가 공유하는 문서로 관리하세요
- 락을 획득하는 순서를 역순으로 해제하면 더 안전합니다
5. 교착 상태 회피 (은행원 알고리즘)
박시니어 씨가 새로운 주제를 꺼냈습니다. "예방은 조건 자체를 막는 거였다면, 회피는 조금 다른 접근이에요.
자원을 줄 때마다 '이거 줘도 안전한가?' 확인하는 거예요. 마치 은행에서 대출해줄 때 상환 능력을 확인하는 것처럼요."
**교착 상태 회피(Deadlock Avoidance)**는 자원을 할당하기 전에 그 할당이 안전한지 미리 검사하는 방법입니다. 대표적인 알고리즘이 **은행원 알고리즘(Banker's Algorithm)**인데, 마치 은행원이 대출 가능 여부를 판단하듯이 시스템이 안전 상태를 유지할 수 있는지 확인합니다.
다음 코드를 살펴봅시다.
// 은행원 알고리즘 간단 구현
public class BankersAlgorithm {
int[] available; // 현재 사용 가능한 자원
int[][] max; // 각 프로세스의 최대 요구량
int[][] allocation; // 현재 각 프로세스에 할당된 자원
int[][] need; // 각 프로세스가 추가로 필요한 자원 (max - allocation)
// 안전 상태 검사
public boolean isSafeState() {
int[] work = available.clone();
boolean[] finish = new boolean[numProcesses];
// 모든 프로세스가 끝날 수 있는 순서 찾기
for (int count = 0; count < numProcesses; count++) {
boolean found = false;
for (int i = 0; i < numProcesses; i++) {
if (!finish[i] && canAllocate(need[i], work)) {
// 프로세스 i 실행 가능 -> 자원 반납
addResources(work, allocation[i]);
finish[i] = true;
found = true;
break;
}
}
if (!found) return false; // 안전하지 않음
}
return true; // 모든 프로세스 완료 가능 = 안전 상태
}
}
박시니어 씨가 재미있는 비유를 들었습니다. "은행을 생각해보세요.
고객 A가 1000만 원을 빌려달라고 해요. 은행에는 1500만 원이 있어요.
줄 수 있을까요?" 김개발 씨가 답했습니다. "당연히 줄 수 있죠.
돈이 있으니까요." "맞아요. 근데 여기서 더 생각해봐야 해요." 박시니어 씨가 말했습니다.
"고객 A가 나중에 추가로 500만 원을 더 빌려야 완전히 사업을 끝내고 돈을 갚을 수 있다면요? 그리고 고객 B도 700만 원을 빌려야 하는데, B는 그 돈으로 바로 사업을 끝낼 수 있다면요?" "음...
그러면 A에게 1000만 원 주면 500만 원 남는데, B에게 700만 원 줄 수가 없네요?" 김개발 씨가 생각했습니다. "맞아요!
그리고 A도 500만 원이 더 필요한데 줄 수 없으니 A도 끝날 수 없어요. 교착 상태예요!" 박시니어 씨가 설명했습니다.
"그래서 현명한 은행원은 먼저 B에게 700만 원을 줘요. B가 사업 끝내고 700만 원 갚으면 1200만 원이 생기고, 그다음 A에게 줄 수 있거든요." 이것이 바로 은행원 알고리즘의 핵심입니다.
자원을 할당하기 전에 "이 자원을 주면 모든 프로세스가 무사히 끝날 수 있는 순서가 존재하는가?"를 확인합니다. 그 순서가 존재하면 **안전 상태(Safe State)**라고 하고, 존재하지 않으면 **불안전 상태(Unsafe State)**라고 합니다.
코드를 살펴보면, available은 현재 남아있는 자원, max는 각 프로세스가 최대로 필요한 자원, allocation은 이미 할당된 자원, need는 추가로 필요한 자원입니다. isSafeState 함수는 work라는 임시 변수로 시뮬레이션을 합니다.
"이 프로세스에게 필요한 자원을 줄 수 있나? 줄 수 있으면 프로세스가 끝나고 자원을 반납하니까 work에 더하자.
이렇게 모든 프로세스를 끝낼 수 있으면 안전 상태야!" 하지만 은행원 알고리즘에는 단점도 있습니다. 모든 프로세스가 미리 자신의 최대 자원 요구량을 알려야 합니다.
실제 시스템에서는 이걸 미리 알기 어려운 경우가 많습니다. 또한 프로세스 수와 자원 수가 많아지면 계산량이 급격히 늘어납니다.
그래서 은행원 알고리즘은 주로 교육 목적으로 많이 다루어지고, 실제 운영체제에서는 다른 방법들을 더 많이 사용합니다.
실전 팁
💡 - 은행원 알고리즘은 이론적으로 완벽하지만 실제 적용은 어렵다는 점을 기억하세요
- 안전 순서(Safe Sequence)가 하나라도 존재하면 안전 상태입니다
6. 교착 상태 탐지와 복구
김개발 씨가 질문했습니다. "예방이나 회피를 안 하면 어떻게 해요?" 박시니어 씨가 답했습니다.
"그때는 교착 상태가 발생하도록 놔두고, 발생하면 그때 처리하는 방법이 있어요. 탐지와 복구라고 해요.
소 잃고 외양간 고치기지만, 때로는 이게 더 효율적일 수 있어요."
**교착 상태 탐지(Deadlock Detection)**는 시스템이 주기적으로 교착 상태가 발생했는지 검사하는 방법입니다. 교착 상태가 발견되면 복구(Recovery) 작업을 수행합니다.
복구 방법에는 프로세스 종료, 자원 선점 등이 있습니다. 이 방법은 교착 상태가 드물게 발생하는 시스템에서 효과적입니다.
다음 코드를 살펴봅시다.
// 교착 상태 탐지 및 복구 예시
public class DeadlockDetectionAndRecovery {
// 주기적으로 교착 상태 탐지
public void detectDeadlock() {
ResourceAllocationGraph graph = buildGraph();
if (graph.hasCycle()) {
System.out.println("교착 상태 탐지됨!");
List<Process> deadlockedProcesses = graph.getDeadlockedProcesses();
recover(deadlockedProcesses);
}
}
// 복구 방법 1: 프로세스 종료
public void recover(List<Process> processes) {
// 방법 A: 모든 교착 프로세스 종료 (간단하지만 손실 큼)
for (Process p : processes) {
p.terminate();
releaseResources(p);
}
// 방법 B: 하나씩 종료하며 확인 (손실 최소화)
for (Process p : selectVictim(processes)) {
p.terminate();
releaseResources(p);
if (!hasDeadlock()) break; // 해결되면 중단
}
}
// 희생자 선택 기준: 우선순위, 실행 시간, 사용 자원 등
private Process selectVictim(List<Process> processes) {
return processes.stream()
.min(Comparator.comparing(Process::getPriority))
.orElse(null);
}
}
박시니어 씨가 새로운 관점을 제시했습니다. "예방이나 회피는 사전에 막는 거예요.
하지만 이건 비용이 들어요. 자원 활용도가 떨어지거나, 매번 안전 검사를 해야 하니까요.
만약 교착 상태가 1년에 한 번 정도만 발생한다면요?" 김개발 씨가 대답했습니다. "그냥 발생하면 그때 처리하는 게 나을 수도 있겠네요?" "맞아요!
그게 바로 탐지와 복구 전략이에요." 박시니어 씨가 설명을 이어갔습니다. "시스템이 주기적으로 '지금 교착 상태인가?' 확인하고, 맞으면 그때 처리하는 거예요." 탐지 방법은 앞서 배운 자원 할당 그래프를 활용합니다.
그래프에서 순환을 찾으면 교착 상태입니다. 시스템은 타이머를 두고 주기적으로 이 검사를 수행합니다.
문제는 교착 상태를 발견한 다음입니다. 어떻게 복구할까요?
가장 간단한 방법은 프로세스 종료입니다. 교착 상태에 관련된 프로세스들을 강제로 종료시키는 거예요.
모든 프로세스를 한꺼번에 종료시킬 수도 있고, 하나씩 종료시키면서 교착 상태가 풀리는지 확인할 수도 있습니다. 하나씩 종료시킬 때는 **희생자(Victim)**를 선택해야 합니다.
누구를 먼저 종료시킬까요? 보통 우선순위가 낮은 프로세스, 실행 시간이 짧은 프로세스, 사용 중인 자원이 적은 프로세스를 먼저 선택합니다.
종료되어도 손실이 적기 때문입니다. 또 다른 방법은 자원 선점입니다.
특정 프로세스의 자원을 강제로 빼앗아 다른 프로세스에게 주는 것입니다. 하지만 이 방법은 복잡합니다.
자원을 빼앗긴 프로세스는 어떻게 되나요? 처음부터 다시 시작해야 할 수도 있습니다.
실제로 데이터베이스 시스템들은 이 방식을 많이 사용합니다. 교착 상태가 발생하면 트랜잭션 하나를 **롤백(Rollback)**시키고, 그 트랜잭션이 가진 락을 해제합니다.
롤백된 트랜잭션은 나중에 다시 시도됩니다. MySQL이나 PostgreSQL 같은 데이터베이스는 자체적인 교착 상태 탐지 메커니즘을 가지고 있습니다.
교착 상태가 발생하면 "Deadlock found when trying to get lock"과 같은 에러 메시지를 보여주고 트랜잭션 하나를 강제로 롤백시킵니다. 김개발 씨가 정리했습니다.
"그러니까 예방은 아예 못 일어나게, 회피는 일어날 것 같으면 피하고, 탐지와 복구는 일어나면 그때 처리하는 거네요?" "완벽해요!" 박시니어 씨가 엄지를 들었습니다. "각 방법마다 장단점이 있어서, 시스템 특성에 맞게 선택해야 해요."
실전 팁
💡 - 교착 상태 탐지 주기가 너무 짧으면 오버헤드가 크고, 너무 길면 탐지가 늦어집니다
- 데이터베이스에서 Deadlock 에러가 발생하면 트랜잭션을 재시도하는 로직을 추가하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
가상 메모리 완벽 가이드
운영체제의 핵심 개념인 가상 메모리를 초급 개발자도 쉽게 이해할 수 있도록 설명합니다. 요구 페이징부터 스래싱까지, 실무에서 마주치는 메모리 관리의 모든 것을 다룹니다.
페이징과 세그멘테이션 완벽 가이드
운영체제의 메모리 관리 핵심 기법인 페이징과 세그멘테이션을 다룹니다. 가상 메모리가 물리 메모리로 변환되는 과정부터 TLB, 다단계 페이지 테이블까지 차근차근 이해할 수 있습니다.
메모리 관리 기초 완벽 가이드
운영체제가 메모리를 어떻게 관리하는지 기초부터 차근차근 알아봅니다. 논리 주소와 물리 주소의 차이부터 단편화 문제까지, 메모리 관리의 핵심 개념을 실무 스토리와 함께 쉽게 설명합니다.
동기화 도구 완벽 가이드
멀티스레드 프로그래밍에서 핵심이 되는 동기화 도구들을 알아봅니다. 뮤텍스, 세마포어, 모니터 등 운영체제의 동기화 메커니즘을 실무 예제와 함께 쉽게 이해할 수 있습니다.
프로세스 동기화 완벽 가이드
멀티스레드 환경에서 여러 프로세스가 공유 자원에 안전하게 접근하는 방법을 알아봅니다. Race Condition부터 Peterson 알고리즘까지 동기화의 핵심 개념을 실무 예제와 함께 쉽게 설명합니다.