
파이썬 멀티스레딩 환경에서 공유 자원에 대한 접근을 제한하는 것은 안정적인 애플리케이션 구축의 필수 요소입니다. 단순히 Lock이나 RLock을 사용하는 것을 넘어, 동시에 접근 가능한 스레드의 수를 정밀하게 제어해야 할 때 우리는 세마포어(Semaphore)를 떠올리게 됩니다. 하지만 파이썬 threading 모듈이 제공하는 두 가지 클래스, Semaphore와 BoundedSemaphore 사이에서 어떤 것을 선택해야 할지 고민하는 개발자가 많습니다. 본 포스팅에서는 단순한 정의를 넘어, 실무에서 발생할 수 있는 'Release 버그'를 방지하고 시스템 리소스를 안전하게 보호하기 위한 두 클래스의 결정적인 차이점과 해결 방법을 전문적인 관점에서 심도 있게 분석합니다.
1. 세마포어(Semaphore)란 무엇인가?
세마포어는 에츠허르 데이크스트라(Edsger Dijkstra)가 고안한 개념으로, 공유 자원에 접근할 수 있는 스레드나 프로세스의 수를 나타내는 정수 카운터를 기반으로 동작합니다. 카운터가 0보다 크면 자원에 접근할 수 있고, 0이면 자원이 해제될 때까지 대기합니다.
- acquire(): 카운터를 1 감소시킵니다. 카운터가 0이면 블로킹(대기) 상태가 됩니다.
- release(): 카운터를 1 증가시킵니다. 대기 중인 다른 스레드가 있다면 깨웁니다.
2. Semaphore vs BoundedSemaphore: 2가지 결정적 차이
두 클래스의 가장 큰 차이는 초기값 초과 허용 여부에 있습니다. 아래 표를 통해 한눈에 비교해 보겠습니다.
| 비교 항목 | Semaphore | BoundedSemaphore |
|---|---|---|
| 초기 카운트 준수 | 강제하지 않음 (초과 가능) | 엄격하게 강제함 (에러 발생) |
| release() 동작 | 호출될 때마다 카운터가 무한히 증가 가능 | 초기값보다 커지면 ValueError 발생 |
| 주요 용도 | 단순한 자원 개수 제한 | 프로그래밍 실수 방지 및 자원 무결성 보장 |
| 안정성 수준 | 보통 (논리적 오류에 취약) | 매우 높음 (디버깅 용이) |
3. 왜 실무에서는 BoundedSemaphore를 권장하는가? (해결 방법)
일반적인 Semaphore의 치명적인 문제점은 개발자의 실수로 release()가 acquire() 횟수보다 더 많이 호출되었을 때 발생합니다. 예를 들어, 초기값이 5인 세마포어에서 실수로 release()를 10번 호출하면 카운터는 10이 되어버립니다. 이는 우리가 의도했던 '최대 5개 스레드 제한'이라는 설계 원칙을 완전히 무너뜨립니다. BoundedSemaphore는 이러한 문제를 즉각적으로 해결합니다. 초기 설정된 허용 범위를 넘어서는 순간 ValueError를 던지기 때문에, 개발자는 코드가 어디서 잘못되었는지 즉시 파악하고 수정할 수 있습니다. 즉, "자원의 상한선을 보장"한다는 측면에서 훨씬 안전한 선택입니다.
4. Sample Example: 안전한 리소스 관리 구현
데이터베이스 커넥션 풀을 모방하여, 한정된 자원을 안전하게 관리하는 코드를 살펴보겠습니다.
import threading
import time
import random
# 최대 3개의 연결만 허용하는 BoundedSemaphore 설정
connection_pool = threading.BoundedSemaphore(3)
def access_database(thread_name):
print(f"[{thread_name}] 접속 대기 중...")
# context manager (with) 사용을 권장 (자동 release)
with connection_pool:
print(f"[{thread_name}] DB 연결 성공! 작업 시작")
time.sleep(random.uniform(1, 2))
print(f"[{thread_name}] 작업 완료 및 연결 반환")
threads = []
for i in range(5):
t = threading.Thread(target=access_database, args=(f"Thread-{i+1}",))
threads.append(t)
t.start()
for t in threads:
t.join()
# 만약 실수로 release()를 직접 호출하여 범위를 초과하려 한다면?
try:
print("\n[위험 테스트] 추가 release 시도...")
connection_pool.release()
except ValueError as e:
print(f"[방어 성공] 에러 발생: {e} - 상한선을 넘을 수 없습니다.")
5. 성능과 설계를 위한 전문가의 팁
동시성 제어 도구를 선택할 때 반드시 고려해야 할 3가지 원칙입니다.
- With 구문 활용: 직접
acquire와release를 호출하는 것보다with문을 사용하는 것이 예외 발생 시 자원 점유 해제를 보장하는 가장 좋은 방법입니다. - 적절한 카운트 산정: 세마포어의 크기는 시스템의 메모리와 CPU 처리 능력을 고려하여 정해야 합니다. 너무 크면 컨텍스트 스위칭 비용이 증가하고, 너무 작으면 시스템 처리량이 저하됩니다.
- 디버깅 우선: 개발 및 테스트 단계에서는 반드시
BoundedSemaphore를 사용하여 로직상의 오류(허용되지 않은 release)를 잡아내십시오.
참고 문헌 및 출처
- Python Standard Library. "threading — Thread-based parallelism". Python Documentation.
- Dijkstra, E. W. "Cooperating Sequential Processes". Technological University Eindhoven.
- Luciano Ramalho. "Fluent Python: Clear, Concise, and Effective Programming". O'Reilly Media.
- Brett Slatkin. "Effective Python: 90 Specific Ways to Write Better Python". Pearson Education.