세마포어란 무엇인가
세마포어는 여러 프로세스나 스레드가 공유 자원에 접근할 때, 동시에 접근 가능한 개수를 제어하기 위한 동기화 도구입니다. 내부적으로 정수 카운터를 하나 가지고 있으며, 자원을 사용하려는 스레드는 wait()(또는 acquire()) 호출로 카운터를 감소시키고, 사용이 끝나면 signal()(또는 release())로 카운터를 증가시킵니다. 카운터가 0이면 다른 스레드는 대기하게 됩니다.
비유하자면 주차장을 떠올리면 됩니다. 주차 공간이 3개인 주차장이 있다면, 동시에 3대까지만 들어올 수 있고 4번째 차는 자리가 빌 때까지 기다려야 합니다. 세마포어의 카운터가 바로 이 ‘남은 주차 자리 수’와 같은 역할을 합니다.
동작 원리와 예시
세마포어는 크게 두 가지로 나뉩니다. 카운터 값이 1인 바이너리 세마포어와, 1보다 큰 값을 가질 수 있는 카운팅 세마포어입니다. 예를 들어 DB 커넥션 풀이 최대 5개의 연결만 허용한다면, 초기값 5인 세마포어를 두고 커넥션을 사용할 때마다 acquire(), 반납할 때 release()를 호출하는 식으로 동시 접속 수를 제한할 수 있습니다.
- 카운터가 0보다 크면: 즉시 자원 획득 후 카운터 감소
- 카운터가 0이면: 다른 스레드가 release할 때까지 대기
중요한 점은 세마포어는 소유 개념이 없다는 것입니다. A 스레드가 acquire하고 B 스레드가 release해도 문제가 없습니다. 이 덕분에 생산자-소비자 패턴처럼 서로 다른 스레드끼리 신호를 주고받는 용도로도 널리 쓰입니다.
뮤텍스와의 차이 및 실무 활용
뮤텍스(Mutex)는 세마포어의 특수한 형태처럼 보이지만 핵심 차이가 있습니다. 뮤텍스는 소유권(ownership)이 있어서, 잠금을 건 스레드만 해제할 수 있습니다. 반면 세마포어는 앞서 말했듯 누구나 release할 수 있어 더 유연하지만, 그만큼 실수로 자원을 놓치거나 잘못 반납할 위험도 큽니다.
또한 뮤텍스는 ‘한 번에 하나의 스레드만 접근’이라는 상호배제(mutual exclusion)에 특화되어 있고, 세마포어는 ‘동시에 N개까지 허용’이라는 자원 개수 제한에 특화되어 있습니다. 그래서 임계 구역을 보호할 때는 뮤텍스를, 제한된 개수의 자원(스레드 풀, 커넥션 풀, API 호출 제한 등)을 관리할 때는 세마포어를 선택하는 경우가 많습니다.
실무에서는 자바의 java.util.concurrent.Semaphore나 파이썬의 threading.Semaphore처럼 언어 표준 라이브러리에서 바로 제공하므로, 두 개념의 차이를 이해하고 상황에 맞게 골라 쓰는 것이 동시성 버그를 줄이는 첫걸음입니다.