모니터로 관리하는 동시성 제어와 조건 동기화
모니터의 캡슐화와 자동 상호 배제, 조건 변수, 진입·조건·신호제공자 큐의 동작을 세마포어와 비교해 정리한다.
2026-08-14 · 최초 발행 2025-12-25
공유 데이터 접근을 모듈 안으로 묶는 방식
모니터(Monitor)는 공유 데이터와 critical section을 함께 감싼 프로그래밍 언어 수준의 동시성 제어 구조다. 세마포어에서 발생할 수 있는 타이밍 오류와 구현 복잡성을 줄이기 위해 쓰이며, 모니터 내부의 상호 배제는 자동으로 보장된다.
세마포어가 저수준 OS 제공 기능이라면 모니터는 언어 차원의 고수준 추상화다. Java의 synchronized, C++의 std::mutex + condition_variable, Python의 threading.Condition처럼 언어와 라이브러리에서 구현할 수 있다. 조건 변수는 특정 조건을 기다리거나 신호를 전달하는 수단이 된다.
모니터 안에 배치되는 데이터와 프로시저
공유 데이터는 모니터 내부에 두고, 외부 프로세스는 그 데이터를 조작하는 프로시저(메서드)를 통해 접근한다. 생성 시 실행되는 초기화 코드와 조건 변수 역시 모니터의 구성 요소다. 이 구조는 데이터 직접 접근을 제한하고 내부 구현 변경이 외부에 미치는 영향을 줄인다.
Java에서는 synchronized 메서드와 wait(), notifyAll()을 이용해 버퍼의 생산과 소비를 제어할 수 있다.
public class BoundedBuffer {
private int[] buffer;
private int count = 0;
public synchronized void produce(int item) {
while (count == buffer.length) {
wait(); // 버퍼가 가득 차면 대기
}
buffer[count++] = item;
notifyAll(); // 대기 중인 consumer 깨움
}
public synchronized int consume() {
while (count == 0) {
wait(); // 버퍼가 비어 있으면 대기
}
int item = buffer[--count];
notifyAll(); // 대기 중인 producer 깨움
return item;
}
}
대기 상태를 나누어 관리하는 큐
모니터는 최소 3개 이상의 큐를 유지한다. 각 큐는 모니터 진입을 기다리는 프로세스, 특정 조건을 기다리는 프로세스, signal()을 실행한 프로세스를 구분한다.
진입을 기다리는 프로세스
진입 큐(Entry Queue)는 모니터 내부 프로시저에 들어가기를 기다리는 프로세스를 관리한다. 모니터 내 프로시저 수만큼 존재하며, 상호 배제 규칙에 따라 한 번에 하나의 프로세스만 진입할 수 있다.
조건이 충족될 때까지의 대기
조건 큐(Condition Queue)는 조건 변수마다 존재하며, 특정 조건이 만족될 때까지 프로세스를 대기시킨다. wait()는 현재 프로세스를 조건 큐에 넣어 대기시키고, signal()은 조건 큐에 있는 한 프로세스를 깨운다.
signal() 이후의 순서
신호제공자 큐(Signaler Queue)는 모니터당 1개이며 항상 존재한다. signal()을 실행한 프로세스는 임시로 이 큐에서 대기한다. 모니터 안에는 한 번에 하나의 프로세스만 있을 수 있기 때문이다.
큐 선택 우선순위는 Signaler Queue, Condition Queue, Entry Queue 순서다.
P/V 호출을 직접 다루는 경우와의 차이
세마포어는 P()와 V() 호출의 순서를 개발자가 직접 관리한다. 해제를 잊거나 순서를 바꾸면 데드락 또는 상호 배제 실패로 이어질 수 있고, 동기화 코드가 여러 위치에 흩어진다.
// 잘못된 사용 예시
P(mutex);
// Critical Section
// V(mutex) 호출 잊음 → 데드락!
// 순서 잘못
V(mutex); // Unlock 먼저
P(mutex); // Lock 나중에 → 상호 배제 실패!
| 항목 | 세마포어 | 모니터 |
|---|---|---|
| 추상화 수준 | 저수준 (OS 제공) | 고수준 (언어 제공) |
| 상호 배제 | 프로그래머가 명시 (P/V) | 자동 보장 (컴파일러) |
| 타이밍 오류 | 발생 가능 | 불가능 (캡슐화) |
| 코드 분산 | 여러 곳에 분산 | 모니터 내 집중 |
| 정보 은폐 | 없음 | 공유 데이터 직접 접근 불가 |
| 사용 편의성 | 어려움 | 쉬움 |
세마포어에서는 임계 구역 앞뒤에 직접 잠금과 해제를 작성한다.
semaphore mutex = 1;
void critical_function() {
P(mutex);
// Critical Section
V(mutex);
}
모니터에서는 동기화 대상 메서드가 상호 배제를 자동으로 보장한다.
public synchronized void critical_function() {
// Critical Section
// 자동으로 상호 배제 보장
}
조건 변수로 생산과 소비를 맞추기
조건 변수는 조건 대기와 신호 전달에 쓰인다. wait()는 조건이 만족될 때까지 대기시키고, signal()은 조건이 만족됐을 때 대기 프로세스를 깨운다. broadcast()는 모든 대기 프로세스를 깨운다.
생산자-소비자 상황에서는 버퍼가 가득 찼을 때 생산자를, 비었을 때 소비자를 대기시킬 수 있다.
synchronized void produce() {
while (full) wait(); // 버퍼 가득 찰 때 대기
// 생산
notifyAll(); // 소비자 깨움
}
synchronized void consume() {
while (empty) wait(); // 버퍼 비었을 때 대기
// 소비
notifyAll(); // 생산자 깨움
}
모니터는 공유 데이터의 직접 접근을 막고, 프로시저를 유일한 접근 경로로 둔다. 이 캡슐화와 자동 상호 배제, 조건 변수의 조합이 동시성 제어의 안전성과 유지보수성을 뒷받침한다.