Semaphore(세마포)란: 스레드 동기화에서의 작동 원리와 활용

저자: IT Sectr 게시일: 2026-03-19 읽는 시간: 8 분

Semaphore(세마포)는 카운터와 대기 중인 스레드 큐를 통해 공유 리소스에 대한 액세스를 제어하는 동기화 프리미티브입니다. Wikipedia, 2024에 따르면, 세마포는 1965년 Edsger Dijkstra가 멀티스레드 상호작용 문제를 해결하기 위해 제안했습니다. 이 도구는 임계 구역과 동시에 작업하는 스레드 수를 제한할 수 있습니다.

핵심 사항

  • Semaphore는 허가 카운터를 통해 액세스를 제어하는 동기화 프리미티브입니다.
  • 이진 세마포는 값 0과 1을 가지며, 차단 플래그 역할을 합니다.
  • 카운팅 세마포는 지정된 수의 스레드에 동시 액세스를 허용합니다.
  • 뮤텍스와 달리 세마포는 소유자 스레드에 묶여 있지 않습니다.
  • Deadlock은 세마포를 잘못 사용할 때의 주요 위험 중 하나입니다.

Semaphore란?

Semaphore는 카운터를 사용하여 공유 리소스에 대한 액세스를 제어하는 동기화 프리미티브입니다. 이 개념은 1965년 Edsger Dijkstra에 의해 제안되었으며 운영 체제의 모든 현대 동기화 메커니즘의 기초가 되었습니다.

정의 및 목적

세마포는 두 개의 원자 연산을 가진 정수 변수입니다: wait(acquire)와 signal(release). wait 연산은 카운터를 감소시키고, signal은 카운터를 증가시킵니다. 카운터가 0에 도달하면 wait를 호출한 스레드는 다른 스레드가 signal을 실행할 때까지 차단됩니다.

세마포의 주요 목적은 여러 스레드의 동시 액세스로부터 임계 구역을 보호하는 것입니다. 뮤텍스와 달리 세마포는 소유자 스레드에 대한 바인딩이 필요하지 않으므로 더 넓은 범위의 조정 작업에 적합합니다.

역사 및 이론적 기초

세마포의 개념은 Technische Hogeschool Eindhoven에서 개발된 THE 운영 체제의 맥락에서 생겨났습니다. Dijkstra는 세마포를 수학적 추상화로 공식화하여 모든 동기화 프리미티브를 구현하기에 충분함을 증명했습니다.

세마포는 어떻게 작동하나요?

세마포 메커니즘은 두 개의 원자 연산과 내부 대기 큐를 기반으로 합니다. acquire가 호출되면 스레드는 카운터 값을 확인하고 실행을 계속하거나 리소스가 해제될 때까지 차단됩니다.

카운터 및 원자 연산

세마포를 생성할 때 허가 카운터의 초기값이 설정됩니다. 각 acquire 호출은 카운터를 1씩 감소시킵니다. 이후 카운터가 음수가 되면 스레드가 차단됩니다. release 연산은 카운터를 증가시키고 대기 중인 스레드 중 하나를 깨웁니다.

kotlin
import java.util.concurrent.Semaphore

val semaphore = Semaphore(3)

fun accessResource() {
    semaphore.acquire()
    try {
        println("${Thread.currentThread().name} 작업 중")
    } finally {
        semaphore.release()
    }
}

대기 큐 및 스케줄링

스레드가 0 카운터로 acquire를 호출하면 OS는 해당 스레드를 세마포의 FIFO 큐에 배치합니다. 스레드는 BLOCKED 상태로 전환되어 CPU 시간을 소비하지 않습니다. release 호출 후 큐의 첫 번째 스레드는 RUNNABLE 상태로 전환되어 리소스에 액세스할 수 있습니다.

세마포의 유형

동기화 이론에서는 세마포의 두 가지 주요 유형인 이진(binary)과 카운팅(counting)을 구분합니다. 유형 선택은 리소스 액세스 관리의 특정 작업에 따라 달라집니다.

이진 세마포 (Binary Semaphore)

이진 세마포는 값 0과 1만 취합니다. 동작 방식은 뮤텍스와 유사하지만 소유권 요구사항이 없습니다—모든 스레드가 release를 실행할 수 있습니다. 이러한 세마포는 스레드 간 준비 플래그 및 이벤트 구현에 편리합니다.

kotlin
val ready = Semaphore(0)

fun producer() {
    Thread.sleep(1000)
    ready.release()
}

fun consumer() {
    ready.acquire()
    println("데이터 준비 완료")
}

카운팅 세마포 (Counting Semaphore)

카운팅 세마포는 음수가 아닌 모든 값을 취할 수 있습니다. 여러 인스턴스가 있는 유사한 리소스 풀을 관리하는 데 사용됩니다. 예를 들어, 5개의 네트워크 연결 풀: 각 acquire는 하나의 연결을 가져오고 release는 이를 풀에 반환합니다.

카운팅 세마포는 외부 서비스에 대한 액세스 속도를 제한하고 스레드 풀을 구현하는 데 필수적입니다. 수동 스레드 관리 없이 병렬 처리 수준을 정확하게 제어할 수 있습니다.

매개변수이진 세마포카운팅 세마포
범위0 또는 10에서 N까지
동시 스레드1최대 N
용도시그널링, 플래그리소스 풀, 속도 제한

Semaphore vs Mutex

개발자들은 종종 세마포와 뮤텍스를 혼동하지만, 이들 사이에는 근본적인 차이점이 있습니다. 이러한 차이점을 이해하는 것은 프로젝트에서 올바른 동기화 메커니즘을 선택하는 데 매우 중요합니다.

소유권 원칙

핵심 차이점은 소유권 개념입니다. 뮤텍스는 항상 어떤 스레드가 이를 획득했는지 알고 있으며, 해당 스레드만이 해제할 수 있습니다. 세마포에는 소유자가 없습니다: acquire를 호출하지 않고도 모든 스레드가 release를 호출할 수 있습니다. 이는 뮤텍스를 데이터 보호에 더 안전하게 만들고 세마포를 조정에 더 유연하게 만듭니다.

성능 및 사용 사례

실제로 뮤텍스는 일반적인 시나리오에 대한 최적화 덕분에 단순한 상호 배제에 더 빠릅니다. 세마포는 카운터 유지를 위한 추가 오버헤드가 필요합니다. 그러나 병렬 처리를 제한하거나 생산자-소비자 패턴을 구현하려면 세마포가 필수적입니다.

특성SemaphoreMutex
소유권소유자 없음소유자 있음
해제모든 스레드소유자 스레드만
카운터0에서 N이진
사용 사례병렬 처리 제한 및 시그널링임계 구역 보호
재귀아니요예 (재진입 가능)

모바일 개발에서의 Semaphore

모바일 애플리케이션 개발에서 Semaphore는 제한된 리소스(네트워크 연결, 파일, 데이터베이스, 하드웨어 구성 요소)에 대한 액세스를 관리하는 데 사용됩니다. 현대 플랫폼은 편리한 내장 구현을 제공합니다.

서버 연결 풀

일반적인 사용 사례 중 하나는 HTTP 연결 풀입니다. 제공자의 API가 병렬 처리를 제한하기 때문에 애플리케이션은 서버에 동시에 최대 4개의 요청만 보낼 수 있습니다. 초기값 4의 세마포는 모든 부하에서 동시 요청 수가 제한을 초과하지 않도록 보장하며, 다른 스레드는 큐에서 대기합니다.

세마포가 없으면 사용자 활동의 급격한 증가로 인해 서버 인프라에 갑작스러운 과부하가 발생하여 시간 초과 및 429 Too Many Requests 오류가 발생할 수 있습니다. Semaphore는 퓨즈 역할을 하여 활성 스레드 수와 관계없이 엄격하게 지정된 수의 동시 호출을 허용합니다.

Android용 Kotlin의 Semaphore

Android는 java.util.concurrent 패키지에서 Semaphore 클래스를 제공합니다. 서버 과부하를 방지하기 위해 동시 네트워크 요청을 2개의 스레드로 제한하는 예를 살펴보겠습니다.

kotlin
class ApiClient {
    private val throttle = Semaphore(2)

    suspend fun fetch(url: String): Result {
        throttle.acquire()
        return try {
            httpGet(url)
        } finally {
            throttle.release()
        }
    }
}

iOS용 Swift의 DispatchSemaphore

iOS에서는 GCD의 DispatchSemaphore가 동일한 작업을 해결합니다. 개발자는 기본 스레드를 차단하지 않고 비동기 코드에서 리소스에 대한 액세스를 동기화하는 데 이를 사용합니다.

swift
let semaphore = DispatchSemaphore(value: 3)

func processBatch(_ items: [UIImage]) {
    for img in items {
        semaphore.wait()
        DispatchQueue.global().async {
            applyFilter(to: img)
            semaphore.signal()
        }
    }
}

세마포 작업 시 일반적인 오류

가장 흔한 오류는 예외 발생 시 release 누락입니다. release를 호출하기 전에 스레드가 오류로 종료되면 세마포는 다른 스레드에 대해 영구적으로 차단된 상태로 유지됩니다. 안전한 해제를 위해 try/finally 또는 defer를 사용하세요. 두 번째 문제는 다른 스레드가 다른 순서로 여러 세마포를 획득할 때 발생하는 교착 상태입니다.

세마포 사용 패턴

세마포는 데이터 보호뿐만 아니라 복잡한 멀티스레드 시나리오에서 스레드 조정에도 사용됩니다. 일반적인 패턴을 알면 개발 속도가 빨라지고 동기화 오류 가능성이 줄어듭니다.

실제 프로젝트에서 세마포 사용에는 여러 입증된 패턴이 있습니다. 이를 알면 일반적인 실수를 피하고 안정적인 멀티스레드 시스템을 구축하는 데 도움이 됩니다.

속도 제한기 (Rate Limiter)

초기값 N과 타이머를 통한 주기적 release가 있는 세마포는 API 요청의 속도 제한을 구현합니다. 예를 들어, 서비스가 초당 10개의 요청을 허용하는 경우: 세마포는 10에서 시작하고, 각 요청은 카운터를 감소시키며, 별도의 TimerTask가 매초 카운터를 초기값으로 되돌립니다. 이는 애플리케이션과 서버 모두를 과부하로부터 보호합니다.

세마포를 이용한 생산자-소비자

고전적인 생산자-소비자 문제에서 두 개의 세마포가 버퍼를 관리합니다: empty(쓰기 허가)와 full(읽기 허가). 생산자는 empty에서 acquire를, full에서 release를 호출하고, 소비자는 그 반대를 수행합니다. 이 방식은 소비자가 빈 버퍼를 읽지 않고 생산자가 버퍼를 오버플로우하지 않도록 보장합니다.

이 동일한 방식은 운영 체제의 바운디드 버퍼—고정 크기의 링 버퍼의 기초가 됩니다. 모바일 애플리케이션에서는 이미지, 비디오 파일 및 분석 이벤트의 큐를 처리하는 데 사용됩니다.

네트워크 요청 스로틀링

세마포는 백그라운드 서비스에서 네트워크 호출 스로틀링에 성공적으로 사용됩니다. 예를 들어, 분석 애플리케이션이 서버에 이벤트 패킷을 보냅니다. 피크 부하(앱 실행, 오프라인 후 동기화) 시 동시 스레드를 제한하지 않으면 동시 요청 수가 서버 제한을 초과할 수 있습니다. 초기값 3의 세마포는 원활한 전송을 보장하고 서버 측 차단을 방지합니다.

자주 묻는 질문

Semaphore와 일반 카운터의 차이점은 무엇인가요?

Semaphore는 단순한 카운터가 아니라 원자 연산과 대기 큐를 가진 동기화 프리미티브입니다. 일반 카운터는 스레드를 차단하지 않으며 여러 스레드가 동시에 액세스할 때 원자적 증가를 보장하지 않습니다.

세마포가 교착 상태를 유발할 수 있나요?

네, 다른 스레드가 다른 순서로 여러 세마포를 획득할 때 교착 상태가 발생할 수 있습니다. 예를 들어, 스레드 A가 S1을 획득한 다음 S2를 획득하고, 스레드 B가 S2를 획득한 다음 S1을 획득하는 경우입니다. 프로젝트의 모든 세마포에 대해 단일 획득 순서를 고정하세요.

카운터가 0일 때 acquire를 호출하면 어떻게 되나요?

스레드가 차단되고 대기 상태로 전환됩니다. 다른 스레드가 release를 호출할 때까지 CPU 시간을 소비하지 않습니다. Java에서는 BLOCKED 상태이고, Swift에서는 GCD에 의해 스레드가 일시 중단됩니다.

Binary Semaphore는 Mutex와 근본적으로 어떻게 다른가요?

주요 차이점은 소유권입니다. Mutex는 소유자 스레드에 의해서만 해제될 수 있습니다. Binary Semaphore는 모든 스레드에 의해 해제될 수 있으며, 이는 스레드 간 시그널링에 편리하지만 데이터 무결성 보호에는 덜 안전합니다.

카운터의 초기값은 어떻게 선택해야 하나요?

초기값은 시나리오에 따라 다릅니다. 단일 리소스 보호 — 1. N개의 연결 풀 — N. 스레드 간 시그널링의 경우 소비자 스레드가 생산자의 신호를 기다리도록 0을 사용하세요.

요약

  • Semaphore는 1965년 Dijkstra가 제안한 카운터 기반 동기화 프리미티브입니다.
  • 이진 세마포는 값 0과 1을 가지며, 카운팅 세마포는 음수가 아닌 모든 값을 가집니다.
  • Acquire 및 release 연산은 원자적이고 스레드 안전합니다.
  • 뮤텍스와 달리 세마포는 소유자 스레드에 묶여 있지 않습니다.
  • 카운팅 세마포는 리소스 풀 관리 및 병렬 처리 제한에 사용됩니다.
  • release 누락은 스레드 중단을 초래하는 가장 흔한 오류입니다.

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기