본문 바로가기
Artificial Intelligence/60. Python

[PYTHON] 레이스 컨디션(Race Condition)을 방지하는 2가지 동기화 기법 : Lock과 RLock의 결정적 차이와 해결 방법

by Papa Martino V 2026. 3. 18.
728x90

Race Condition
Race Condition

 

멀티스레딩 환경에서 파이썬 프로그램을 개발하다 보면 예상치 못한 데이터 오염이나 프로그램 중단 현상을 겪게 됩니다. 그 중심에는 '레이스 컨디션(Race Condition)'이라는 고질적인 문제가 자리 잡고 있습니다. 여러 스레드가 동일한 자원에 동시에 접근하여 수정을 시도할 때 발생하는 이 문제는, 시스템의 신뢰성을 무너뜨리는 치명적인 버그의 원인이 됩니다. 이 글에서는 파이썬 threading 모듈이 제공하는 가장 기본적인 상호 배제(Mutual Exclusion) 도구인 LockRLock의 동작 원리를 심층 분석하고, 실무에서 발생하는 데드락(Deadlock) 문제를 해결하는 구체적인 방법을 제시합니다.


1. 레이스 컨디션이란 무엇이며 왜 발생하는가?

레이스 컨디션은 두 개 이상의 스레드가 공유 데이터에 접근하여 동시에 변경을 시도할 때, 실행 순서에 따라 결과가 달라지는 상태를 말합니다. 파이썬에는 GIL(Global Interpreter Lock)이 존재하지만, 이는 바이트코드 실행 수준에서의 안전을 보장할 뿐, 고수준의 비즈니스 로직(예: count += 1)에서 발생하는 원자성(Atomicity) 부족 문제까지 해결해주지는 않습니다.


2. Lock vs RLock 핵심 차이 분석

파이썬 개발자가 가장 흔히 혼동하는 것이 바로 일반적인 Lock과 재진입 가능한 RLock(Reentrant Lock)의 차이입니다. 아래 표를 통해 두 메커니즘의 차이를 명확히 구분할 수 있습니다.

구분 항목 threading.Lock (일반 락) threading.RLock (재진입 락)
기본 개념 한 번에 하나의 스레드만 소유 가능 동일 스레드가 중복해서 소유 가능
소유권 확인 누가 소유했는지 따지지 않음 소유한 스레드와 획득 횟수를 기록함
재획득 시 동작 동일 스레드라도 다시 호출하면 차단(Deadlock) 동일 스레드라면 즉시 획득 허용 (카운트 증가)
해제 규칙 어떤 스레드든 해제(release) 가능 획득한 횟수만큼 동일 스레드가 해제해야 함
주요 사용처 단순한 임계 영역 보호 재귀 함수나 중첩된 메서드 호출 시

3. Lock의 한계와 RLock을 통한 데드락 해결 방법

일반적인 Lock은 매우 효율적이지만, 복잡한 클래스 구조 내에서 메서드가 다른 메서드를 호출할 때 치명적인 단점이 드러납니다. 만약 method_A가 락을 잡은 상태에서 똑같은 락을 사용하는 method_B를 호출하면, 프로그램은 영원히 대기 상태에 빠지는 '자기 자신에 의한 데드락'에 빠지게 됩니다.

 

해결 방법: RLock은 내부적으로 '소유한 스레드 ID'와 '획득 횟수(Recursion Level)'를 관리합니다. 따라서 동일한 스레드가 이미 락을 보유하고 있다면 차단되지 않고 내부 카운트만 올리며 통과시켜 줍니다. 이는 객체지향 설계에서 캡슐화된 메서드 간의 안전한 호출을 보장합니다.


4. 실전 Sample Example

아래 예제는 RLock이 필요한 전형적인 상황인 재귀 구조에서의 활용법을 보여줍니다.


import threading

class SafeCounter:
    def __init__(self):
        # 일반 Lock을 쓰면 recursion() 호출 시 데드락 발생
        self.lock = threading.RLock()
        self.value = 0

    def decrement_recursive(self, n):
        with self.lock:
            if n > 0:
                print(f"현재 값: {n} - 락 획득 상태")
                self.value += 1
                # 동일 스레드에서 다시 락 영역으로 진입
                self.decrement_recursive(n - 1)

if __name__ == "__main__":
    counter = SafeCounter()
    t = threading.Thread(target=counter.decrement_recursive, args=(3,))
    t.start()
    t.join()
    print("최종 실행 완료")

5. 개발자를 위한 기술적 조언 (Best Practice)

  • 성능 고려: RLock은 일반 Lock보다 소유권 확인 로직이 추가되어 미세하게 더 느립니다. 중첩 호출이 없는 단순 루프라면 Lock이 유리합니다.
  • 컨텍스트 매니저 사용: acquire()release()를 직접 호출하기보다 with 문을 사용하세요. 예외 발생 시에도 안전하게 락이 해제됩니다.
  • 최소 범위 유지: 락을 잡고 있는 시간(Critical Section)은 최소화해야 병렬 처리 효율이 극대화됩니다.

내용 출처 및 참고 문헌

  • Python Software Foundation: "threading — Thread-based parallelism" (Official Documentation)
  • Modern Operating Systems (4th Edition) by Andrew S. Tanenbaum
  • Effective Python: 90 Specific Ways to Write Better Python by Brett Slatkin
728x90