지난 포스팅에서는 서버의 상태를 엑스레이처럼 들여다볼 수 있는 ‘스레드 덤프(Thread Dump)’의 생성 방법과 주요 상태(State)에 대해 알아보았습니다. 이번 글에서는 멀티 스레드 프로그래밍 환경에서 개발자들이 겪을 수 있는 최악의 악몽이자, 스레드 덤프 분석을 통해 반드시 찾아내야 하는 치명적인 오류인 교착 상태(Deadlock, 데드락)에 대해 완벽하게 해부해 보겠습니다.
서버가 다운된 것도 아니고 CPU 사용량이 치솟지도 않았는데 애플리케이션이 먹통이 되어 사용자의 요청을 전혀 처리하지 못한다면, 가장 먼저 데드락을 의심해 보아야 합니다.
1. 교착 상태(Deadlock)란 무엇인가?
데드락은 두 개 이상의 스레드가 서로 상대방이 점유하고 있는 자원(Lock)을 차지하기 위해 무한정 기다리는 상태를 말합니다.
쉽게 비유하자면, 좁은 외나무다리에서 마주 친 두 대의 자동차와 같습니다. A 자동차는 B 자동차가 비켜주기를 기다리고, B 자동차는 A 자동차가 비켜주기를 기다립니다. 양보할 방법(통제권)이 없는 두 자동차는 결국 영원히 다리 위에서 움직이지 못하게 됩니다. Java 애플리케이션에서도 잘못된 동기화(synchronized) 처리로 인해 이러한 무한 대기 상태가 빈번하게 발생합니다.
2. 데드락 발생의 4가지 필수 조건
컴퓨터 과학에서 데드락이 발생하려면 다음의 4가지 조건(코프만 조건)이 모두 동시에 충족되어야 합니다. 역으로 말하면, 이 중 하나라도 끊어내면 데드락을 예방할 수 있습니다.
- 상호 배제 (Mutual Exclusion): 자원은 한 번에 하나의 스레드만 사용할 수 있어야 합니다. (Java의 락(Lock) 메커니즘 자체가 이에 해당합니다.)
- 점유 대기 (Hold and Wait): 최소한 하나의 자원을 점유한 상태에서, 다른 스레드가 점유하고 있는 자원을 추가로 얻기 위해 대기해야 합니다.
- 비선점 (No Preemption): 다른 스레드가 점유한 자원을 강제로 빼앗을 수 없어야 합니다.
- 순환 대기 (Circular Wait): 스레드들이 서로 꼬리를 물고 자원을 대기하는 형태(A는 B의 자원을, B는 A의 자원을 대기)가 만들어져야 합니다.
3. 스레드 덤프(Thread Dump)를 통한 데드락 진단
운영 서버에서 데드락이 발생했을 때 가장 확실한 진단 방법은 이전 포스팅에서 배운 스레드 덤프를 추출하는 것입니다.
JVM은 스레드 덤프를 생성할 때 자체적으로 데드락 발생 여부를 검사하는 훌륭한 기능을 가지고 있습니다. 덤프 파일의 최하단을 살펴보면, 데드락이 발생한 경우 다음과 같은 명확한 메시지와 함께 원인이 된 스레드들의 정보가 출력됩니다.
Found one Java-level deadlock:Thread-A waiting to lock <0x12345> (held by Thread-B)Thread-B waiting to lock <0x67890> (held by Thread-A)
이 메시지를 발견했다면, 두 스레드의 상태는 필연적으로 모두 BLOCKED 상태로 멈춰 있을 것입니다. 이를 통해 어떤 클래스의 어느 라인(Call Stack)에서 서로 락(Lock)을 잡고 놓아주지 않는지 정확히 추적할 수 있습니다.
4. 데드락 해결 및 실전 예방 전략
데드락은 발생 후 조치(서버 재시작)보다 발생하지 않도록 코드를 설계하는 것이 훨씬 중요합니다.
- 락(Lock) 획득 순서 고정: 가장 확실하고 고전적인 예방 방법입니다. 모든 스레드가 자원을 요청할 때 항상 같은 순서(예: A를 먼저 얻고 B를 얻는다)로만 락을 획득하도록 강제하면 순환 대기(Circular Wait) 조건이 깨져 데드락이 발생하지 않습니다.
- Lock Timeout 적용:
synchronized블록 대신java.util.concurrent.locks.ReentrantLock클래스의tryLock(time)메서드를 사용합니다. 일정 시간 동안 락을 얻지 못하면 포기하고 다른 작업을 수행하거나 에러를 반환하게 하여 무한 대기(Hold and Wait)를 방지합니다. - 동기화 블록 최소화: 꼭 필요한 최소한의 영역(Critical Section)에만 락을 걸어야 합니다. 메서드 전체에 무분별하게
synchronized를 남발하면 데드락 확률이 기하급수적으로 올라갑니다.
결론: 동시성 프로그래밍의 책임
멀티 스레드 환경은 강력한 퍼포먼스를 제공하지만, 그에 따른 ‘동기화 관리’라는 무거운 책임을 개발자에게 요구합니다. 데드락은 개발자의 논리적 설계 오류에서 기인하는 경우가 대부분이므로, 락을 사용할 때는 항상 ‘순서’와 ‘범위’를 고민하는 방어적 프로그래밍 습관을 길러야 합니다.
데드락을 피하는 가장 좋은 방법 중 하나는 스레드를 무한정 생성하지 않고 체계적으로 관리하는 것입니다. 다음 포스팅에서는 시스템 자원 낭비를 막고 서버의 안정성을 지켜주는 방파제, ‘시스템 자원 낭비를 막는 스레드 풀(Thread Pool) 적정 사이즈 산정법’에 대해 알아보겠습니다.