279. 재진입 가능 락 (Reentrant Lock / Recursive Lock)
⚠️ 이 문서는 "자물쇠를 쥐고 있는 사람이 그 자물쇠가 달린 다른 방의 문을 열려고 하면 어떻게 될까?"라는 동시성 프로그래밍의 딜레마(자기 자신에 의한 데드락)를 해결하기 위해, 이미 락의 소유권을 획득한 스레드가 똑같은 락을 여러 번 반복해서 획득할 수 있도록 허용하는 특수 자물쇠인 '재진입 가능 락'을 다룹니다.
핵심 인사이트 (3줄 요약)
- 본질: 재진입 가능 락(Reentrant Lock)은 스레드가 락을 요청할 때, 그 락을 이미 쥐고 있는 주인이 '나 자신(현재 스레드)'이라면, 멈춰 세우지(Block) 않고 카운터(Count)만 1 증가시킨 뒤 그대로 통과시켜 주는 뮤텍스의 변형 모델이다.
- 가치: 스레드 자신이 락을 쥔 상태에서 내부적으로 똑같은 락을 요구하는 다른 메서드(재귀 함수, 또는 연쇄 호출)를 불렀을 때, 스스로가 스스로를 기다리며 멈춰버리는 **셀프 데드락(Self-Deadlock)**의 멍청한 비극을 원천적으로 막아준다.
- 융합: Java의
synchronized블록이나ReentrantLock클래스 등 현대 고수준 언어가 제공하는 락은 개발자의 실수를 덮어주기 위해 대부분 기본적으로 '재진입 가능성'을 내장하고 있다.
Ⅰ. 개요: 스스로 함정에 빠진 스레드 (Context & Necessity)
아주 흔한 객체 지향 프로그래밍 상황을 상상해 보자.
은행 계좌 클래스에 출금() 메서드와 이체() 메서드가 있다. 송금액을 빼내는 로직은 똑같으니, 이체() 메서드 안에서 출금() 메서드를 호출해서 코드를 재사용했다.
그런데 데이터 보호를 위해 이체() 메서드에도 락(Lock)을 걸어두고, 출금() 메서드에도 똑같은 락을 걸어두었다.
- 순수 뮤텍스(Non-Reentrant)의 악몽:
- 스레드 A가
이체()를 호출하며 락을 꽉 쥔다. - 코드 내부에서
출금()을 호출한다. 출금()이 시작되려는데 "어? 락이 잠겨있네?" 하고 락이 풀리길 기다린다 (Sleep).- 락을 풀려면
이체()가 끝나야 하는데,이체()는출금()이 끝나길 기다리며 영원히 멈춰버렸다. (셀프 데드락 발생 💣)
- 스레드 A가
이 멍청한 상황을 본 설계자들은 이마를 탁 쳤다. "야, 락을 쥐고 있는 놈이 나 자신이면 그냥 통과시켜 줘야지!" 그렇게 자물쇠에 소유자 확인증과 진입 횟수 카운터를 달아놓은 것이 **재진입 가능 락(Reentrant Lock)**이다.
📢 섹션 요약 비유: 클럽 VIP 룸에 들어갈 때 경호원(뮤텍스)이 문을 막습니다. 일반 락은 내가 안에서 춤추다가 잠깐 화장실 갔다가 다시 들어가려 해도 "문 닫혔습니다" 하고 나를 못 들어가게 막습니다(셀프 데드락). 하지만 재진입 락은 경호원이 내 얼굴(스레드 ID)을 기억해 두고 있다가, "아까 들어가신 VIP시네요, 또 들어가십쇼!" 하고 통과시켜 주는 똑똑한 자물쇠입니다.
Ⅱ. 재진입 가능 락의 작동 원리 (카운팅 메커니즘)
재진입 락의 내부는 순수 뮤텍스보다 2개의 부품을 더 가지고 있다. **'소유자 스레드 ID'**와 **'홀드 카운트(Hold Count)'**다.
1. 락 획득 (Lock, 진입)
- 스레드 A가 락을 요청한다.
- 락이 풀려있으면(Count=0), 락을 잠그고 소유자=A, 카운트=1로 기록한다.
- 스레드 A가 안에서 한 번 더 락을 요청한다(재귀 호출 등).
- 락은 잠겨있지만, 소유자가 A임을 확인하고 "주인님이네!" 하며 카운트를 2로 올리고 통과시킨다.
- 스레드 B가 락을 요청하면? 소유자가 B가 아니므로 무자비하게 문 밖에서 재운다(Block).
2. 락 해제 (Unlock, 탈출)
- 스레드 A가
unlock()을 한 번 호출한다. - 다른 스레드에게 락을 넘겨주지 않고, 그저 카운트를 1로 내린다. (아직 1번 더 감싸져 있기 때문)
- 스레드 A가
unlock()을 한 번 더 호출해서 카운트가 0이 되는 순간! - 비로소 락의 문이 철컥 열리며 대기하던 스레드 B가 들어올 수 있게 된다.
┌──────────────────────────────────────────────────────────────────────────────┐
│ 재진입 락 (Reentrant Lock)의 재귀적 호출과 카운팅 시각화 │
├──────────────────────────────────────────────────────────────────────────────┤
│ │
│ public synchronized void 이체() { [ 락 상태 모니터 ] │
│ // 1. 여기서 스레드 A가 락 획득! -> 소유자: A, Count: 1 │
│ │
│ 출금(); // 2. 내부에서 다른 메서드 호출 │
│ } │
│ │
│ public synchronized void 출금() { │
│ // 3. 일반 락이면 여기서 셀프 데드락으로 뻗어버림! │
│ // 하지만 재진입 락이면? "나 A야!" -> 소유자: A, Count: 2 (통과!) │
│ │
│ 잔액 = 잔액 - 100; │
│ } // 4. 출금 끝 (1차 Unlock) -> 소유자: A, Count: 1 │
│ │
│ // 5. 이체 끝 (2차 Unlock) -> 소유자: 없음, Count: 0 │
│ // ★ 비로소 밖에서 기다리던 스레드 B가 락을 쥐고 진입 가능! │
└──────────────────────────────────────────────────────────────────────────────┘
[다이어그램 해설] 이 구조 덕분에 개발자는 메서드 안에서 다른 동기화된 메서드를 호출할 때 "이게 락이 걸려있나 안 걸려있나?"를 눈치 보며 설계할 필요가 없어진다. 코드를 아주 깔끔하고 모듈화(조립)하기 좋게 만들어주는 객체 지향 프로그래밍의 구원자다.
Ⅲ. 일반 뮤텍스 대비 단점과 오버헤드
세상에 공짜는 없다. 재진입을 지원하기 위해 자물쇠 안에 부품을 더 구겨 넣었기 때문에 필연적인 오버헤드가 발생한다.
- 상태 추적의 비용 (Performance Overhead)
- 락을 걸고 풀 때마다, "네가 주인이 맞냐?"라고 스레드 ID를 확인하고 숫자를 더하고 빼는 추가적인 메모리 연산(Overhead)이 발생한다.
- 극단적으로 성능을 쥐어짜야 하는 리눅스 커널의 최하위 단이나 임베디드 시스템에서는 이런 오버헤드조차 아까워서 '순수 뮤텍스(Non-Reentrant Lock)'나 '스핀 락(Spin Lock)'을 고집하는 경우가 많다.
- 락 해제 횟수 불일치 오류
- 개발자가 실수로 락을 3번 걸었는데(카운트 3), 풀기(
unlock)는 2번만 호출하고 빠져나갔다고 치자. - 카운트가 1로 남아있기 때문에, 다른 스레드들은 이 락이 풀리기를 영원히 기다리며 진짜 데드락이 터진다. (그래서 Java는 보통
try-finally블록 안에unlock을 쑤셔 넣어 이 휴먼 에러를 방지한다.)
- 개발자가 실수로 락을 3번 걸었는데(카운트 3), 풀기(
Ⅳ. 결론
"스레드가 자기 자신의 발에 걸려 넘어지는 어리석음을 방지하는 설계."
재진입 가능 락(Reentrant Lock)은 동시성 프로그래밍이 현실의 객체 지향 코드(메서드 연쇄 호출, 재귀 함수)와 결합할 때 발생하는 충돌을 가장 매끄럽게 봉합해 준 기술이다. 우리가 무심코 쓰는 자바의 synchronized나 C#의 lock 블록이 내부적으로 모두 이 '재진입 카운터'를 가지고 있는 덕분에, 수많은 초보 개발자들이 셀프 데드락의 수렁에 빠지지 않고 안전하게 멀티 스레드 서버를 돌릴 수 있는 것이다.
📌 관련 개념 맵
- 해결하는 문제: 셀프 데드락 (Self-Deadlock)
- 내부 구현체: 스레드 소유자 식별자(Thread ID), 홀드 카운터(Hold Count)
- 대표적인 실무 클래스: Java의
ReentrantLock클래스, C++의std::recursive_mutex - 비교 대상: 이진 세마포어 (소유권이 없으므로 재진입 불가능), 순수 뮤텍스
👶 어린이를 위한 3줄 비유 설명
- 내가 비밀의 방에 자물쇠를 잠그고 들어갔어요. 그런데 방 안에서 또 똑같은 자물쇠가 달린 작은 금고를 열어야 해요.
- 멍청한 자물쇠(일반 뮤텍스)는 내가 방금 잠그고 들어온 사람이라는 걸 모르고, "문이 잠겼으니 금고도 못 열어!" 하고 나를 안에서 갇히게 만들어요(셀프 데드락).
- 똑똑한 자물쇠(재진입 락)는 내 지문을 기억해 뒀다가, "아까 밖에서 문 잠그고 들어오신 분이네? 금고도 열어드릴게요!" 하고 내가 내 자물쇠를 여러 번 열 수 있게 통과시켜 준답니다!