4주차 Unit 6.1 — 생산자-소비자 문제

Psj·2026년 5월 21일

F-lab

목록 보기
141/238

Unit 6.1 — 생산자-소비자 문제

F-LAB JAVA · 4주차 · Phase 6 · 스레드 간 협력
🚀 Phase 6 시작 — 스레드 협력 진입


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • 생산자-소비자 문제 (Producer-Consumer) 의 정의는?
  • 생산자와 소비자의 역할 은?
  • 버퍼 (큐) 가 꽉 차거나 비었을 때 의 처리는?
  • 실제 사례 (MQ, 로깅, 작업 큐) 는?
  • 일반 락만으로 처리하는 방법 의 한계는?
  • 바쁜 대기 (Busy Waiting) 의 문제는?
  • 이 문제가 멀티스레드의 핵심 문제 인 이유는?
  • BlockingQueue 가 해결책인 이유는?
  • wait/notify 가 필요한 이유는?

🎯 핵심 한 문장

생산자-소비자 문제 (Producer-Consumer Problem) 는 데이터를 생성하는 생산자 스레드와 소비하는 소비자 스레드가 공유 버퍼 (큐) 를 통해 협력하는 고전적인 동시성 문제다.
생산자 는 데이터를 버퍼에 넣고 (produce), 소비자 는 버퍼에서 꺼내 처리한다 (consume) — 둘은 서로의 속도를 모른 채 버퍼를 매개로 느슨하게 연결된다.
핵심 제약은 버퍼가 꽉 차면 생산자가 대기 (넣을 공간 없음), 버퍼가 비면 소비자가 대기 (꺼낼 것 없음) 해야 한다는 점이며, 이 대기·통지를 어떻게 구현하느냐가 문제의 본질이다.
실제 사례로 메시지 큐 (Kafka, RabbitMQ), 비동기 로깅, 작업 큐 (스레드 풀의 task queue) 등이 있다.
일반 락 (synchronized) 만으로는 "버퍼 빌 때까지 반복 확인" 하는 바쁜 대기 (busy waiting) 가 되어 CPU 를 낭비하므로, 효율적 대기·통지를 위해 wait/notify (다음 Unit) 또는 BlockingQueue 가 필요하다.

비유 — 빵집 진열대

생산자-소비자 = 빵집:

생산자 (제빵사):
  - 빵을 만들어 진열대에 놓음
  - 진열대 가득 차면 → 대기 (놓을 곳 없음)

소비자 (손님):
  - 진열대에서 빵을 가져감
  - 진열대 비면 → 대기 (가져갈 빵 없음)

버퍼 (진열대):
  - 빵을 임시 보관
  - 생산/소비 속도 차이 흡수

협력:
  - 제빵사와 손님은 서로 속도 모름
  - 진열대로 느슨하게 연결
  - 진열대 차면 제빵사 쉼
  - 진열대 비면 손님 기다림

나쁜 방법 (바쁜 대기):
  - 손님이 "빵 있나?" 1초에 1000번 확인
  - 에너지 낭비

좋은 방법 (wait/notify):
  - 손님은 자고 (wait)
  - 빵 나오면 제빵사가 깨움 (notify)

→ 생산자-소비자 = 버퍼 매개 협력, 차면/비면 대기, 효율적 통지 필요.


🧭 9개 섹션 로드맵

1. 생산자-소비자 문제의 정의
2. 생산자와 소비자의 역할
3. 버퍼가 꽉 차거나 빌 때
4. 실제 사례
5. 일반 락만으로 처리
6. 바쁜 대기의 문제
7. 멀티스레드 핵심 문제인 이유
8. BlockingQueue 해결책
9. 면접 + 자기 점검

1️⃣ 생산자-소비자 문제의 정의

1.1 문제 정의

생산자-소비자 문제:

  데이터를 생성하는 생산자와
  소비하는 소비자가
  공유 버퍼를 통해 협력하는 문제.

구성:
  - 생산자 (Producer)
  - 소비자 (Consumer)
  - 버퍼 (Buffer/Queue)

1.2 기본 구조

생산자-소비자 구조:

생산자 ──put──→ [버퍼] ──take──→ 소비자
                 (큐)

  - 생산자: 데이터 생성 → 버퍼
  - 소비자: 버퍼 → 데이터 처리
  - 버퍼: 중간 저장소

1.3 느슨한 결합

느슨한 결합 (Decoupling):

  생산자와 소비자가 직접 X.
  버퍼를 매개로 연결.

효과:
  - 속도 차이 흡수
  - 독립적 동작
  - 확장성 (생산자/소비자 수 조절)

1.4 코드 (개념)

// 생산자-소비자 (개념)
public class ProducerConsumer {
    private final Queue<Task> buffer = new LinkedList<>();
    private final int capacity = 10;
    
    // 생산자
    public void produce(Task task) {
        // 버퍼에 추가
        buffer.offer(task);
    }
    
    // 소비자
    public Task consume() {
        // 버퍼에서 꺼냄
        return buffer.poll();
    }
    // 동기화 필요 (다음 섹션)
}

1.5 ILIC 의 맥락

@Service
public class ShipmentProcessingPipeline {
    
    // 생산자: 주문 → 처리 큐
    // 소비자: 처리 큐 → 실제 처리
    
    private final Queue<Shipment> processingQueue = new LinkedList<>();
    
    // 생산자 (주문 접수)
    public void submitForProcessing(Shipment shipment) {
        processingQueue.offer(shipment);   // 큐에 추가
        // 동기화 필요
    }
    
    // 소비자 (백그라운드 처리)
    public Shipment takeForProcessing() {
        return processingQueue.poll();   // 큐에서 꺼냄
    }
    
    // 생산 (주문) 과 소비 (처리) 가 느슨하게 연결
    // 큐로 속도 차이 흡수
}

1.6 자기 점검 답변

생산자-소비자 문제의 정의는?

:
1. 정의:

  • 생산자 + 소비자 + 버퍼
  • 협력 문제
  1. 구조:

    • 생산자 → 버퍼 → 소비자
  2. 느슨한 결합:

    • 버퍼 매개
    • 속도 차이 흡수
  3. 효과:

    • 독립 동작
    • 확장성

2️⃣ 생산자와 소비자의 역할

2.1 생산자

생산자 (Producer):

  데이터를 생성하여 버퍼에 넣음.

동작:
  1. 데이터 생성
  2. 버퍼에 추가 (put/offer)
  3. (버퍼 가득 차면 대기)

예:
  - 주문 접수
  - 로그 생성
  - 이벤트 발생

2.2 소비자

소비자 (Consumer):

  버퍼에서 데이터를 꺼내 처리.

동작:
  1. 버퍼에서 꺼냄 (take/poll)
  2. 데이터 처리
  3. (버퍼 비면 대기)

예:
  - 주문 처리
  - 로그 기록
  - 이벤트 핸들링

2.3 여러 생산자/소비자

N 생산자 - M 소비자:

  - 여러 생산자가 동시 생성
  - 여러 소비자가 동시 처리
  - 버퍼는 공유

장점:
  - 병렬 처리
  - 부하 분산
  - 확장성

도전:
  - 버퍼 동기화 (경쟁)

2.4 시각화

N-M 생산자-소비자:

생산자1 ─┐
생산자2 ─┼─put──→ [버퍼] ──take─┬─→ 소비자1
생산자3 ─┘                      ├─→ 소비자2
                                └─→ 소비자3

  버퍼 동기화 필요 (여러 접근)

2.5 속도 불균형

속도 불균형:

생산 > 소비:
  - 버퍼 점점 참
  - 생산자 대기 증가
  - 소비자 증설 필요

생산 < 소비:
  - 버퍼 점점 빔
  - 소비자 대기 증가
  - 생산자 증설 또는 소비자 감소

버퍼:
  - 일시적 불균형 흡수

2.6 ILIC 의 맥락

@Service
public class ShipmentProducerConsumer {
    
    private final BlockingQueue<Shipment> queue = new LinkedBlockingQueue<>(100);
    
    // 생산자 (여러 API 핸들러)
    public void produce(Shipment shipment) throws InterruptedException {
        queue.put(shipment);   // 큐 가득 차면 대기
        log.info("생산: {}", shipment.getId());
    }
    
    // 소비자 (여러 워커 스레드)
    public void startConsumers(int count) {
        for (int i = 0; i < count; i++) {
            Thread consumer = new Thread(() -> {
                while (!Thread.currentThread().isInterrupted()) {
                    try {
                        Shipment shipment = queue.take();   // 비면 대기
                        process(shipment);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        break;
                    }
                }
            }, "consumer-" + i);
            consumer.start();
        }
    }
    
    private void process(Shipment s) { }
}

2.7 자기 점검 답변

생산자와 소비자의 역할은?

:
1. 생산자:

  • 데이터 생성
  • 버퍼에 추가
  • 차면 대기
  1. 소비자:

    • 버퍼에서 꺼냄
    • 처리
    • 비면 대기
  2. N-M:

    • 여러 생산자/소비자
    • 병렬, 확장
  3. 속도 불균형:

    • 버퍼가 흡수

3️⃣ 버퍼가 꽉 차거나 빌 때

3.1 두 가지 경계 조건

버퍼 경계 조건:

1. 버퍼 가득 참 (Full):
   - 생산자가 넣을 수 없음
   - → 생산자 대기

2. 버퍼 빔 (Empty):
   - 소비자가 꺼낼 수 없음
   - → 소비자 대기

3.2 버퍼 가득 참

버퍼 가득 참:

생산자:
  - 버퍼 크기 한계 (예: 100)
  - 가득 차면 더 못 넣음

처리:
  - 생산자 대기 (소비자가 비울 때까지)
  - 또는 버린다 (drop)
  - 또는 블록

3.3 버퍼 빔

버퍼 빔:

소비자:
  - 꺼낼 데이터 없음

처리:
  - 소비자 대기 (생산자가 채울 때까지)
  - 또는 null 반환
  - 또는 블록

3.4 대기와 통지

대기와 통지:

생산자가 채움:
  → 소비자에게 통지 ("데이터 있어!")
  → 대기 중 소비자 깨움

소비자가 비움:
  → 생산자에게 통지 ("공간 있어!")
  → 대기 중 생산자 깨움

핵심:
  - 조건 충족 시 통지
  - 효율적 협력

3.5 시각화

버퍼 경계:

가득 참:
  생산자: [대기 (공간 없음)]
  소비자: 꺼냄 → 공간 생김 → 생산자 깨움

빔:
  소비자: [대기 (데이터 없음)]
  생산자: 넣음 → 데이터 생김 → 소비자 깨움

3.6 ILIC 의 맥락

@Service
public class BufferBoundaryExample {
    
    // 유한 버퍼 (크기 100)
    private final BlockingQueue<Shipment> queue = new ArrayBlockingQueue<>(100);
    
    // 생산자 — 가득 차면 대기
    public void produce(Shipment shipment) throws InterruptedException {
        queue.put(shipment);   // 가득 차면 자동 대기
        // 소비자가 비우면 자동 진행
    }
    
    // 소비자 — 비면 대기
    public Shipment consume() throws InterruptedException {
        return queue.take();   // 비면 자동 대기
        // 생산자가 채우면 자동 진행
    }
    
    // 비대기 버전 (offer/poll)
    public boolean tryProduce(Shipment shipment) {
        return queue.offer(shipment);   // 가득 차면 false (대기 X)
    }
    
    public Shipment tryConsume() {
        return queue.poll();   // 비면 null (대기 X)
    }
}

3.7 자기 점검 답변

버퍼가 꽉 차거나 빌 때의 처리는?

:
1. 가득 참:

  • 생산자 대기
  • (또는 drop/블록)
  1. :

    • 소비자 대기
    • (또는 null)
  2. 통지:

    • 채우면 소비자 깨움
    • 비우면 생산자 깨움
  3. 효율:

    • 조건 시 통지

4️⃣ 실제 사례

4.1 메시지 큐

메시지 큐 (Message Queue):

  Kafka, RabbitMQ, SQS 등.

생산자:
  - 메시지 발행 (publish)

소비자:
  - 메시지 구독/소비 (subscribe/consume)

큐:
  - 메시지 저장
  - 비동기 통신

4.2 비동기 로깅

비동기 로깅:

  Logback AsyncAppender 등.

생산자 (앱 스레드):
  - 로그 이벤트 생성
  - 큐에 추가 (빠름)

소비자 (로깅 스레드):
  - 큐에서 꺼냄
  - 파일/DB 기록 (느림)

효과:
  - 앱 스레드 블록 X
  - 로깅 비동기

4.3 작업 큐 (스레드 풀)

작업 큐 (Thread Pool):

  ExecutorService 의 내부.

생산자 (submit):
  - 작업 제출
  - 작업 큐에 추가

소비자 (워커 스레드):
  - 큐에서 작업 꺼냄
  - 실행

→ 스레드 풀의 핵심 (Phase 7)

4.4 기타 사례

기타 사례:

  - 이벤트 루프 (이벤트 큐)
  - 배치 처리 (입력 큐)
  - 스트림 처리
  - 다운로드 매니저 (작업 큐)
  - 프린터 스풀러

4.5 공통 패턴

공통 패턴:

  생산 속도 ≠ 소비 속도:
    - 버퍼로 흡수

  비동기:
    - 생산자 블록 X
    - 소비자 독립

  확장:
    - 소비자 증설 (병렬)

4.6 ILIC 의 맥락

@Service
public class ShipmentRealWorldExamples {
    
    // 1. 비동기 알림 (메시지 큐 패턴)
    private final BlockingQueue<Notification> notificationQueue = 
        new LinkedBlockingQueue<>();
    
    public void sendNotificationAsync(Notification notification) throws InterruptedException {
        notificationQueue.put(notification);   // 생산 (빠름)
        // 별도 스레드가 소비 (실제 발송, 느림)
    }
    
    // 2. 비동기 로깅 (감사 로그)
    private final BlockingQueue<AuditLog> auditQueue = new LinkedBlockingQueue<>();
    
    public void logAudit(AuditLog log) {
        auditQueue.offer(log);   // 비동기 (앱 블록 X)
    }
    
    // 3. 작업 큐 (배송 처리)
    private final ExecutorService processingPool = 
        Executors.newFixedThreadPool(4);   // 내부 작업 큐
    
    public void processShipment(Shipment shipment) {
        processingPool.submit(() -> doProcess(shipment));   // 생산
        // 워커 스레드가 소비 (Phase 7)
    }
    
    private void doProcess(Shipment s) { }
    
    record Notification(String to, String message) {}
    record AuditLog(String action, long timestamp) {}
}

4.7 자기 점검 답변

실제 사례는?

:
1. 메시지 큐:

  • Kafka, RabbitMQ
  • publish/consume
  1. 비동기 로깅:

    • AsyncAppender
    • 앱 블록 X
  2. 작업 큐:

    • 스레드 풀
    • submit/워커
  3. 공통:

    • 속도 차이 흡수
    • 비동기, 확장

5️⃣ 일반 락만으로 처리

5.1 synchronized 만으로

// 일반 락만으로 (한계 있음)
public class SimpleBuffer {
    private final Queue<Task> buffer = new LinkedList<>();
    private final int capacity = 10;
    private final Object lock = new Object();
    
    public void produce(Task task) {
        synchronized (lock) {
            while (buffer.size() >= capacity) {
                // 가득 참 → 어떻게 대기?
                // 그냥 반복? (바쁜 대기)
            }
            buffer.offer(task);
        }
    }
    
    public Task consume() {
        synchronized (lock) {
            while (buffer.isEmpty()) {
                // 빔 → 어떻게 대기?
            }
            return buffer.poll();
        }
    }
}

5.2 문제 1 — 바쁜 대기

// ❌ 바쁜 대기 (busy waiting)
public Task consume() {
    while (true) {
        synchronized (lock) {
            if (!buffer.isEmpty()) {
                return buffer.poll();
            }
        }
        // 비어있으면 반복 확인 (CPU 낭비)
    }
}
// 계속 확인 → CPU 100%

5.3 문제 2 — 데드락

// ❌ 락 안에서 무한 루프 (데드락)
public Task consumeBroken() {
    synchronized (lock) {
        while (buffer.isEmpty()) {
            // 락 잡은 채 대기
            // → 생산자가 락 못 얻음
            // → 영원히 빔 (데드락)
        }
        return buffer.poll();
    }
}
// 락 잡고 대기 → 생산자 진입 불가

5.4 sleep 으로 완화 (여전히 한계)

// sleep 으로 바쁜 대기 완화 (불완전)
public Task consumeWithSleep() throws InterruptedException {
    while (true) {
        synchronized (lock) {
            if (!buffer.isEmpty()) {
                return buffer.poll();
            }
        }
        Thread.sleep(10);   // 잠시 대기 (CPU 절약)
        // 하지만:
        // - 지연 (10ms)
        // - 여전히 폴링
        // - 효율 X
    }
}

5.5 근본 해결 필요

일반 락의 한계:

  - 바쁜 대기 (CPU 낭비)
  - 또는 sleep (지연, 폴링)
  - 락 안 대기 (데드락)

근본 해결:
  - wait/notify (다음 Unit)
    - 락 반납 + 대기
    - 통지로 깨움
  - BlockingQueue
    - 이미 구현됨

5.6 ILIC 의 맥락

@Service
public class LockOnlyLimitation {
    
    private final Queue<Shipment> buffer = new LinkedList<>();
    private final int capacity = 100;
    private final Object lock = new Object();
    
    // ❌ 바쁜 대기 (CPU 낭비)
    public Shipment consumeBusy() {
        while (true) {
            synchronized (lock) {
                if (!buffer.isEmpty()) {
                    return buffer.poll();
                }
            }
            // 비어있으면 계속 반복 (CPU 100%)
        }
    }
    
    // △ sleep (완화, 여전히 폴링)
    public Shipment consumeSleep() throws InterruptedException {
        while (true) {
            synchronized (lock) {
                if (!buffer.isEmpty()) {
                    return buffer.poll();
                }
            }
            Thread.sleep(10);   // 지연 + 폴링
        }
    }
    
    // ✓ 근본 해결은 wait/notify 또는 BlockingQueue (다음)
}

5.7 자기 점검 답변

일반 락만으로 처리의 한계는?

:
1. synchronized 만:

  • 대기 방법 문제
  1. 바쁜 대기:

    • 반복 확인
    • CPU 낭비
  2. 데드락:

    • 락 안 대기
    • 생산자 진입 불가
  3. sleep:

    • 완화 (지연, 폴링)
    • 근본 해결 X → wait/notify

6️⃣ 바쁜 대기의 문제

6.1 바쁜 대기

바쁜 대기 (Busy Waiting / Spin Waiting):

  조건을 만족할 때까지
  계속 반복 확인하며 CPU 점유.

  while (!condition) { }   // 계속 확인

6.2 CPU 낭비

CPU 낭비:

  바쁜 대기:
    - CPU 100% 사용
    - 의미 없는 반복
    - 다른 작업 방해
    - 전력 낭비

예:
  while (buffer.isEmpty()) { }
  → 비어있는 동안 CPU 풀가동

6.3 시각화

바쁜 대기 vs 효율적 대기:

바쁜 대기:
  소비자: [확인][확인][확인]...[확인][꺼냄]
          CPU 100% (의미 없음)

효율적 대기 (wait):
  소비자: [wait (CPU 0%)]......[깨어남][꺼냄]
          CPU 0% (대기)
          생산자 notify 로 깨움

6.4 언제 바쁜 대기가 OK

바쁜 대기가 OK인 경우:

  - 매우 짧은 대기 (마이크로초)
  - 컨텍스트 스위칭이 더 비쌀 때
  - spin lock (짧은 임계 영역)

대부분:
  - 바쁜 대기 피해야
  - wait/notify, BlockingQueue

6.5 효율적 대기

효율적 대기:

wait/notify:
  - 조건 안 되면 wait (CPU 0%)
  - 조건 되면 notify (깨움)

BlockingQueue:
  - take() 가 자동 대기
  - put() 이 자동 통지

LockSupport:
  - park/unpark

→ CPU 낭비 X

6.6 ILIC 의 맥락

@Service
public class BusyWaitingProblem {
    
    private final Queue<Shipment> buffer = new ConcurrentLinkedQueue<>();
    
    // ❌ 바쁜 대기 (CPU 낭비)
    public Shipment consumeBusy() {
        Shipment shipment;
        while ((shipment = buffer.poll()) == null) {
            // 계속 확인 (CPU 100%)
        }
        return shipment;
    }
    
    // ✓ 효율적 — BlockingQueue
    private final BlockingQueue<Shipment> blockingBuffer = 
        new LinkedBlockingQueue<>();
    
    public Shipment consumeEfficient() throws InterruptedException {
        return blockingBuffer.take();   // 비면 효율적 대기 (CPU 0%)
        // 내부적으로 wait/notify (또는 park/unpark)
    }
}

6.7 자기 점검 답변

바쁜 대기의 문제는?

:
1. 바쁜 대기:

  • 반복 확인
  • CPU 점유
  1. 문제:

    • CPU 100% 낭비
    • 다른 작업 방해
    • 전력
  2. OK 경우:

    • 매우 짧은 대기
    • spin lock
  3. 효율적:

    • wait/notify
    • BlockingQueue (CPU 0%)

7️⃣ 멀티스레드 핵심 문제인 이유

7.1 핵심 문제

생산자-소비자가 핵심인 이유:

  멀티스레드 협력의 모든 요소 포함:
    - 공유 자원 (버퍼)
    - 동기화 (경쟁)
    - 조건 대기 (차/빔)
    - 통지 (협력)
    - 효율 (바쁜 대기 회피)

7.2 포함된 개념들

포함된 동시성 개념:

1. 상호 배제
   - 버퍼 동기화

2. 조건 동기화
   - 차면/비면 대기

3. 협력
   - 생산자-소비자 통지

4. 효율
   - 바쁜 대기 회피

7.3 다른 문제와 연결

다른 동시성 문제와 연결:

  - 스레드 풀 (작업 큐)
  - 이벤트 루프 (이벤트 큐)
  - 파이프라인 (단계별 큐)
  - 백프레셔 (버퍼 제어)

→ 생산자-소비자가 기반

7.4 실무의 보편성

실무 보편성:

  거의 모든 비동기 시스템:
    - 메시지 큐
    - 작업 큐
    - 로깅
    - 스트림

  → 생산자-소비자 패턴
  → 이해 필수

7.5 학습 가치

학습 가치:

  생산자-소비자 이해 =
    - wait/notify 이해
    - BlockingQueue 이해
    - 스레드 풀 이해
    - 비동기 시스템 이해

→ 동시성의 핵심

7.6 ILIC 의 맥락

@Service
public class ProducerConsumerCore {
    
    // ILIC 의 여러 곳에 생산자-소비자
    
    // 1. 배송 처리 파이프라인
    private final BlockingQueue<Shipment> processingQueue = 
        new LinkedBlockingQueue<>();
    
    // 2. 알림 발송
    private final BlockingQueue<Notification> notificationQueue = 
        new LinkedBlockingQueue<>();
    
    // 3. 감사 로그
    private final BlockingQueue<AuditEvent> auditQueue = 
        new LinkedBlockingQueue<>();
    
    // 4. 외부 API 호출 (재시도 큐)
    private final BlockingQueue<ApiCall> retryQueue = 
        new LinkedBlockingQueue<>();
    
    // 모두 생산자-소비자 패턴
    // 이 패턴 이해 = ILIC 비동기 시스템 이해
    
    record Notification(String message) {}
    record AuditEvent(String action) {}
    record ApiCall(String endpoint) {}
}

7.7 자기 점검 답변

멀티스레드 핵심 문제인 이유는?

:
1. 모든 요소 포함:

  • 공유 자원
  • 동기화
  • 조건 대기
  • 통지, 효율
  1. 개념:

    • 상호 배제
    • 조건 동기화
    • 협력
  2. 연결:

    • 스레드 풀, 이벤트 루프
    • 파이프라인
  3. 보편성:

    • 거의 모든 비동기 시스템

8️⃣ BlockingQueue 해결책

8.1 BlockingQueue

BlockingQueue:

  생산자-소비자를 위한 동시성 큐.
  java.util.concurrent

특징:
  - put: 가득 차면 대기
  - take: 비면 대기
  - 내부 동기화 (자동)

8.2 주요 메서드

BlockingQueue<Task> queue = new LinkedBlockingQueue<>();

// 블로킹 (대기)
queue.put(task);          // 가득 차면 대기
Task task = queue.take(); // 비면 대기

// 논블로킹
queue.offer(task);        // 가득 차면 false
Task t = queue.poll();    // 비면 null

// 타임아웃
queue.offer(task, 1, SECONDS);   // 1초 시도
queue.poll(1, SECONDS);          // 1초 대기

8.3 구현체

BlockingQueue 구현체:

ArrayBlockingQueue:
  - 고정 크기 배열
  - 유한 버퍼

LinkedBlockingQueue:
  - 링크드 노드
  - 선택적 크기

PriorityBlockingQueue:
  - 우선순위

SynchronousQueue:
  - 크기 0 (직접 전달)

DelayQueue:
  - 지연 후 꺼냄

8.4 생산자-소비자 구현

// BlockingQueue 로 생산자-소비자 (간단)
public class ProducerConsumerWithBQ {
    private final BlockingQueue<Task> queue = new LinkedBlockingQueue<>(100);
    
    // 생산자
    public void producer() throws InterruptedException {
        while (true) {
            Task task = createTask();
            queue.put(task);   // 가득 차면 자동 대기
        }
    }
    
    // 소비자
    public void consumer() throws InterruptedException {
        while (true) {
            Task task = queue.take();   // 비면 자동 대기
            process(task);
        }
    }
    
    // wait/notify 직접 안 써도 됨 (내부 구현)
    private Task createTask() { return null; }
    private void process(Task t) { }
}

8.5 왜 권장

BlockingQueue 권장 이유:

  - 검증된 구현 (버그 없음)
  - wait/notify 직접 X
  - 효율적 (바쁜 대기 X)
  - 다양한 구현체
  - 타임아웃, 논블로킹

→ 직접 wait/notify 보다 권장

8.6 ILIC 의 맥락

@Service
public class ShipmentBlockingQueue {
    
    // 유한 버퍼 (백프레셔)
    private final BlockingQueue<Shipment> queue = new ArrayBlockingQueue<>(1000);
    
    // 생산자 (API 핸들러)
    public void submit(Shipment shipment) throws InterruptedException {
        queue.put(shipment);   // 1000 가득 차면 대기 (백프레셔)
    }
    
    // 논블로킹 버전 (거부)
    public boolean trySubmit(Shipment shipment) {
        boolean accepted = queue.offer(shipment);
        if (!accepted) {
            log.warn("큐 가득 참, 거부: {}", shipment.getId());
        }
        return accepted;
    }
    
    // 소비자 (워커 풀)
    @PostConstruct
    public void startWorkers() {
        for (int i = 0; i < 4; i++) {
            Thread worker = new Thread(() -> {
                while (!Thread.currentThread().isInterrupted()) {
                    try {
                        Shipment shipment = queue.take();   // 효율적 대기
                        process(shipment);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        break;
                    }
                }
            }, "worker-" + i);
            worker.start();
        }
    }
    
    private void process(Shipment s) { }
}

8.7 자기 점검 답변

BlockingQueue가 해결책인 이유는?

:
1. BlockingQueue:

  • 생산자-소비자 큐
  • put/take (자동 대기)
  1. 메서드:

    • put/take (블로킹)
    • offer/poll (논블로킹)
  2. 구현체:

    • Array/Linked/Priority
    • Synchronous
  3. 권장:

    • 검증된 구현
    • wait/notify 직접 X

9️⃣ 면접 + 자기 점검

9.1 면접 단골 질문 매핑

Q핵심 답변
생산자-소비자?버퍼 매개 협력
생산자 역할?데이터 생성 → 버퍼
소비자 역할?버퍼 → 처리
버퍼 가득?생산자 대기
버퍼 빔?소비자 대기
실제 사례?MQ, 로깅, 작업 큐
일반 락 한계?바쁜 대기, 데드락
바쁜 대기?CPU 낭비
핵심 문제 이유?모든 협력 요소
해결책?wait/notify, BlockingQueue

9.2 자기 점검 체크리스트

정의

  • 생산자-소비자
  • 버퍼
  • 느슨한 결합

역할

  • 생산자
  • 소비자
  • N-M

경계

  • 가득 참
  • 통지

사례

  • MQ
  • 로깅
  • 작업 큐

일반 락

  • 한계
  • 바쁜 대기

핵심 문제

  • 이유
  • 연결

해결

  • BlockingQueue
  • wait/notify

9.3 추가 심화 질문

Q1: 백프레셔 (Backpressure)?

답:

  • 소비자가 못 따라갈 때
  • 생산자 속도 조절
  • 유한 버퍼 (put 대기)
  • 시스템 보호

Q2: SynchronousQueue?

답:

  • 크기 0 버퍼
  • 생산자-소비자 직접 전달
  • put 이 take 기다림
  • 핸드오프 (handoff)

Q3: 무한 큐의 위험?

답:

  • LinkedBlockingQueue (크기 무제한)
  • 소비 < 생산 시 메모리 폭발
  • OOM 위험
  • 유한 큐 권장

Q4: 우선순위 큐?

답:

  • PriorityBlockingQueue
  • 우선순위 순서로 take
  • 긴급 작업 먼저
  • Comparator

Q5: Disruptor?

답:

  • 고성능 생산자-소비자
  • 링 버퍼 (lock-free)
  • LMAX
  • 매우 빠름

🎯 핵심 요약 — 3줄 정리

1. 생산자-소비자

  • 생산자 → 버퍼 → 소비자 (협력)
  • 가득 참: 생산자 대기, 빔: 소비자 대기

2. 일반 락 한계

  • 바쁜 대기 (CPU 낭비)
  • 또는 데드락 (락 안 대기)

3. 해결

  • wait/notify (다음 Unit)
  • BlockingQueue (권장, 검증됨)
  • 사례: MQ, 로깅, 작업 큐

📚 다음으로...

Unit 6.2 — wait()과 notify()

이번 Unit에서 생산자-소비자 문제를 봤다면, 다음은 wait/notify (효율적 대기/통지).

  • wait() 의 동작 (락 반납 + 대기)
  • notify / notifyAll
  • while vs if (spurious wakeup)
  • wait vs sleep

Phase 6 진행 상황

🚀 Phase 6 — 스레드 간 협력
  ✅ Unit 6.1 생산자-소비자 문제 ← 여기
  ⏭ Unit 6.2 wait()과 notify()
  ⏭ Unit 6.3 인터럽트 메커니즘
  ⏭ Unit 6.4 yield()

4주차 누적 진행

✅ Phase 1~5 (21 Unit, 1차 정점 완료)
🚀 Phase 6 — 스레드 협력 (1/4 진행)

총: 22/35 Unit

F-LAB JAVA · 4주차 · Phase 6 · Unit 6.1 · 끝
🚀 Phase 6 시작 — 스레드 협력 진입

profile
Software Developer

0개의 댓글