Producer–Consumer 패턴이란?

2026. 2. 13. 19:53·공부/CS

1. Producer–Consumer 패턴이란?

Producer–Consumer 패턴은

“데이터를 생산하는 주체(Producer)와 데이터를 처리하는 주체(Consumer)를 분리하고,
중간에 공유 버퍼(Queue)를 두어 두 주체가 서로 비동기로 동작하도록 만드는 디자인 패턴”
이다.

핵심 아이디어는

  • Producer는 자신의 속도대로 데이터를 생성해서 큐에 넣고,
  • Consumer는 자신의 속도대로 큐에서 꺼내 처리한다.
  • 큐가 속도 차이를 완화해 주기 때문에, 한쪽이 느려도 다른 쪽이 바로 막히지는 않는다.
  • 다만 큐가 bounded(유한 크기)라면, Consumer가 지속적으로 느리면 큐가 가득 차면서 back‑pressure가 발생할 수 있다.

2. 구성 요소

대표적인 구성은 다음과 같다.

구성 요소 역할
Producer 데이터/작업을 생성해서 큐에 넣는 주체. 예: 웹 요청을 처리하는 스레드, 로그 생성기 등.
Consumer 큐에서 데이터를 꺼내 실제 처리(저장, 계산, 전송 등)를 수행하는 주체.
Shared Buffer (Queue) Producer와 Consumer가 공유하는 큐. 보통 FIFO로 구현되며, 블로킹 큐(Bounded/Blocking Queue)를 많이 사용한다.

 

이 구조는

  • Producer와 Consumer가 서로를 몰라도 되고, 중간 큐만 알고 있으면 된다.
  • Producer는 Consumer의 처리 완료를 직접 기다리지는 않지만, 큐가 가득 차면 Producer도 block 될 수 있다.

3. 왜 쓰는가? (장점)

Producer–Consumer 패턴을 쓰는 주요 이유는 다음과 같다.

  • 부하 분리 및 비동기 처리
    • Producer와 Consumer가 각자의 속도로 동작할 수 있어서, 한쪽이 느려도 다른 쪽이 바로 막히지는 않는다.
  • 결합도 감소(Decoupling)
    • Producer와 Consumer는 서로를 몰라도 되고, 중간 큐만 알고 있으면 된다.
  • 버퍼링과 트래픽 버스트 흡수
    • 순간적으로 Producer가 많이 밀려도 큐가 버퍼 역할을 해서 Consumer가 차례대로 처리할 수 있다.
  • 확장성
    • Producer나 Consumer를 여러 개로 늘려도, 큐만 잘 설계하면 시스템을 쉽게 확장할 수 있다.

4. 동작 방식과 규칙


Producer–Consumer 패턴은 “큐가 가득 찼을 때”와 “큐가 비었을 때”를 어떻게 처리하느냐에 따라 동작이 결정된다.

 

4‑1. 기본 규칙

  • 큐가 가득 찼을 때
    • Producer는 더 이상 데이터를 넣을 수 없으므로, 대기(wait) 상태가 된다.
  • 큐가 비었을 때
    • Consumer는 가져올 데이터가 없으므로, 대기(wait) 상태가 된다.

이렇게 하면

  • Producer는 Consumer의 처리 완료를 직접 기다리지는 않지만, 큐가 가득 차면 Producer도 block 된다.
  • Consumer는 Producer를 기다리지 않고, 큐에 데이터가 쌓이면 바로 처리할 수 있다.

 

4‑2. 동기화와 블로킹 큐

실제 구현에서는 동기화가 핵심이다.

  • 큐는 공유 자원이므로, 여러 스레드가 동시에 접근하면
    • race condition, 데이터 꼬임이 발생할 수 있다.
  • 그래서 일반적으로
    • 큐에 접근할 때 락(뮤텍스)을 걸고,
    • Producer가 큐에 넣고 나면 Consumer에게 신호(notify)를 보내는 구조를 사용한다.

예를 들어 Java의 BlockingQueue를 쓰면,

  • put()은 큐가 가득 찼을 때 자동으로 대기하고,
  • take()는 큐가 비었을 때 자동으로 대기한다.

전통적인 wait/notify 기반 구현에서는 Producer가 큐에 데이터를 넣은 뒤 Consumer에게 notify를 보내지만,
현대적인 구현에서는 BlockingQueue, ConcurrentLinkedQueue, Semaphore, Lock + Condition 같은 고수준 동기화 자료구조가 내부적으로 이를 처리한다.

 

4-3. wait/notify vs BlockingQueue: 구현 방식 비교

두 방식은 동일한 문제를 해결하지만, 구현 난이도와 안정성에서 차이가 있다.

 

구분 wait/notify (Low-level) BlockingQueue (High-level)
구현 난이도 높음 (직접 동기화 로직 작성) 낮음 (내부 구현 숨김)
버그 가능성 높음 (spurious wakeup 등) 낮음 (검증된 라이브러리)
유연성 높음 (세밀한 제어 가능) 중간 (정해진 인터페이스)
유지보수 어려움 (디버깅 힘들 수 있음) 쉬움 (표준 API 사용)

 

wait/notify의 주요 함정

잘못된 예시 - spurious wakeup 미처리

synchronized(queue) {
    if (queue.isEmpty()) {  // ❌ if 사용
        queue.wait();
    }
    return queue.remove();
}

올바른 예시

synchronized(queue) {
    while (queue.isEmpty()) {  // ✅ while 사용
        queue.wait();
    }
    return queue.remove();
}

wait/notify는 다음 문제를 직접 처리해야 한다:

  • Spurious wakeup: wait()가 이유 없이 깨어날 수 있어 while로 재확인 필수
  • Lost notify: notify() 전에 wait()하지 않으면 신호를 놓침
  • 잘못된 락 범위: 락을 너무 오래 잡으면 성능 저하, 너무 짧으면 race condition

 

wait/notify 완전한 구현 예시

class ProducerConsumerWithWaitNotify {
    private final Queue<Integer> queue = new LinkedList<>();
    private final int MAX_SIZE = 10;
    
    // Producer 메서드
    public void produce(int value) throws InterruptedException {
        synchronized(queue) {
            while (queue.size() == MAX_SIZE) {
                queue.wait();  // 큐가 가득 차면 대기
            }
            queue.add(value);
            queue.notifyAll();  // Consumer에게 알림
        }
    }
    
    // Consumer 메서드
    public int consume() throws InterruptedException {
        synchronized(queue) {
            while (queue.isEmpty()) {
                queue.wait();  // 큐가 비면 대기
            }
            int value = queue.poll();
            queue.notifyAll();  // Producer에게 알림
            return value;
        }
    }
}

BlockingQueue 구현 예시

class ProducerConsumerWithBlockingQueue {
    private final BlockingQueue<Integer> queue = 
        new ArrayBlockingQueue<>(10);
    
    // Producer 메서드
    public void produce(int value) throws InterruptedException {
        queue.put(value);  // 내부적으로 대기 처리
    }
    
    // Consumer 메서드
    public int consume() throws InterruptedException {
        return queue.take();  // 내부적으로 대기 처리
    }
}

언제 어떤 것을 쓸까?

  • 대부분의 경우: BlockingQueue 사용 (안전하고 명확함)
  • 극한의 성능 최적화가 필요한 경우: wait/notify로 직접 구현 (예: HFT 시스템)
  • 특수한 동기화 로직: Lock + Condition (wait/notify보다 유연하면서도 안전)

 

Lock + Condition 예시 (중간 방식)

class ProducerConsumerWithLock {
    private final Queue<Integer> queue = new LinkedList<>();
    private final int MAX_SIZE = 10;
    private final Lock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();
    
    public void produce(int value) throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() == MAX_SIZE) {
                notFull.await();  // 큐가 가득 차면 대기
            }
            queue.add(value);
            notEmpty.signal();  // Consumer에게 알림
        } finally {
            lock.unlock();
        }
    }
    
    public int consume() throws InterruptedException {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                notEmpty.await();  // 큐가 비면 대기
            }
            int value = queue.poll();
            notFull.signal();  // Producer에게 알림
            return value;
        } finally {
            lock.unlock();
        }
    }
}

5. Producer–Consumer 패턴의 변형

Producer–Consumer 패턴은 스레드 수에 따라 여러 변형이 있다.

유형 설명
Single Producer – Single Consumer 하나의 Producer와 하나의 Consumer만 존재. 구현이 가장 단순.
Multiple Producer – Single Consumer 여러 Producer가 하나의 Consumer에게 데이터를 보냄. 예: 여러 워커 스레드가 로그를 큐에 넣고, 하나의 로거 스레드가 파일에 기록.
Single Producer – Multiple Consumer 하나의 Producer가 여러 Consumer에게 데이터를 분배. 예: 하나의 큐에서 여러 스레드가 작업을 가져와 병렬 처리.
Multiple Producer – Multiple Consumer 가장 현실적인 구조. Producer와 Consumer가 여러 개이고, 큐를 공유해서 작업을 분산 처리.

 

이때는

  • 락 경쟁, 대기 시간, 잘못된 동기화 구현 시 데드락 가능성 등이 커지므로,
  • 락 구조(스핀락/뮤텍스/세마포어)와 큐 구현(배열/링버퍼/우선순위 큐)을 잘 설계해야 한다.

6. 시스템 안정성을 위한 백프레셔(Back-pressure) 전략

 

Producer-Consumer 패턴에서 가장 경계해야 할 상황은 생산 속도가 소비 속도를 압도할 때이다. 이를 제어하지 못하면 시스템은 다음과 같은 임계점에 도달하게 된다.

  • 자원 고갈: 큐에 데이터가 무한정 쌓이면서 메모리 사용량이 급증하고, 결국 OOM(Out Of Memory)으로 프로세스가 강제 종료될 수 있다.
  • 성능 저하: 메모리 압박으로 인해 GC(Garbage Collection)가 빈번하게 발생하며 시스템 전체의 처리 성능(Throughput)이 급격히 떨어진다.

이러한 과부하 상태를 생산자에게 알려 데이터 흐름을 제어하는 메커니즘을 Back-pressure라고 한다. 단순히 큐의 크기를 늘리는 것은 임시방편일 뿐이며, 시스템의 성격에 따라 다음과 같은 명확한 거절 전략(Rejection Policy)을 설계해야 한다.

  1. Blocking(차단): 큐가 가득 차면 생산자를 대기 상태로 만든다. 데이터 유실이 없어야 하는 시스템에서 가장 기본적으로 사용하는 전략이다.
  2. Drop(유실): 큐가 포화 상태일 때 새로 들어오는 데이터(Drop New) 혹은 가장 오래된 데이터(Drop Old)를 과감히 버린다. 실시간성이 중요한 로그 수집이나 센서 데이터 처리에 적합하다.
  3. Rate Limiting(속도 제한): 생산자가 생성하는 데이터 양 자체를 시간당 일정 수치 이하로 제한하여 과부하의 원인을 사전 차단한다.
  4. Circuit Breaker(회로 차단): 소비 측의 장애가 감지되면 생산자와의 연결을 즉시 차단하여 시스템 전체로 장애가 전파되는 것을 방지한다.

7. Producer–Consumer 패턴과 메시지 큐의 관계


Producer–Consumer 패턴은 “멀티스레드 환경에서의 큐 기반 비동기 처리”이고,
메시지 큐는 그걸 네트워크/분산 환경으로 확장한 구조라고 보면 된다.

항목 Producer–Consumer (OS/스레드) 메시지 큐 (분산)
위치 같은 프로세스/서버 내 여러 서버 간
통신 공유 메모리/큐 네트워크 + 브로커
동기화 락/세마포어 ack, commit, 리플리카
장애 프로세스 종료 시 큐 유실 가능 디스크/리플리카로 내구성 제공

 

즉, Producer–Consumer 패턴은 “동기화 + 버퍼 + 비동기 처리”라는 CS 개념을 이해하는 핵심이고,

메시지 큐는 그걸 분산 시스템에서 네트워크와 내구성을 붙여서 확장한 형태다.

'공부 > CS' 카테고리의 다른 글

REST, REST API, 그리고 RESTful API  (0) 2025.11.21
[Python] 파이썬으로 TTS 구현하기: gTTS (Google Text-to-Speech)  (0) 2025.11.13
[Celery] 파이썬 비동기 태스크 큐 Celery란 무엇인가  (0) 2025.11.11
'공부/CS' 카테고리의 다른 글
  • REST, REST API, 그리고 RESTful API
  • [Python] 파이썬으로 TTS 구현하기: gTTS (Google Text-to-Speech)
  • [Celery] 파이썬 비동기 태스크 큐 Celery란 무엇인가
gepetton
gepetton
공부하며 얻은것들을 공유합니다!
  • gepetton
    gepetton의 블로그
    gepetton
  • 전체
    오늘
    어제
    • 분류 전체보기 (16)
      • 개발 기록 (3)
        • 프로젝트 - BUKAE (2)
        • 프로젝트 - GULON (1)
      • 공부 (10)
        • DB (1)
        • JAVA (5)
        • CS (4)
      • 자격증 (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • github
  • 공지사항

  • 인기 글

  • 태그

    인덱스 성능
    리눅스마스터 2502
    Python
    Celery
    큐기반비동기
    정보처리기사필기후기
    이벤트 기반 처리
    자바
    자격증
    java
    PK 설계
    API제한
    정보처리기사실기후기
    SQLD 2025
    Python Worker
    FailFast
    springboot
    spring boot
    토큰버킷알고리즘
    태크스큐
    58회 SQLD
    2025년3회정보처리기사
    리눅스마스터 1급 후기
    WebSocket 인증
    비동기 아키텍처
    파이썬 TTS
    리눅스마스터1급 2502
    정보처리기사2025
    ratelimit
    java record
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
gepetton
Producer–Consumer 패턴이란?
상단으로

티스토리툴바