Post

[Thread] 생산자-소비자 패턴

[Thread] 생산자-소비자 패턴

DevBid 프로젝트에서 입찰 동시성 제어를 Redis 분산락으로 구현했지만 자바 기본 동시성 메커니즘을 제대로 이해하고 싶어 김영한 선생님의 자바 고급편 강의를 듣고 있다. 그 중 생산자-소비자 문제를 BlockingQueue 로 이해하고 정리한 포스팅이다.

락의 2단계 대기 상태

synchronized, ReentrantLock 모두 2가지 대기 상태가 존재한다.

synchronized 대기

  1. 모니터 락 획득 대기
  2. wait() 대기

ReentrantLock 대기

  1. ReentrantLock 락 획득 대기
  2. await() 대기

임계 영역 안에서는 항상 락을 가진 하나의 스레드만 실행될 수 있다. 락을 획득하지 못한 스레드는 WAITING 상태로 대기 큐에서 기다린다.


생산자-소비자 문제

멀티스레드 환경에서 가장 흔히 마주치는 패턴이다.

1
2
생산자(Producer): 데이터를 만들어 큐에 넣음
소비자(Consumer): 큐에서 데이터를 꺼내 처리

문제는 큐가 가득 찼을 때 생산자가 어떻게 해야 하는지, 큐가 비었을 때 소비자가 어떻게 해야 하는지다. 직접 구현하려면 락, 조건변수, 대기 로직을 모두 작성해야한다..


BlockingQueue

자바는 이 문제를 해결하기 위해 Doug Lea 라는 교수님이 만든 java.util.concurrent.BlockingQueue 라는 멀티스레드 자료 구조를 제공한다. 이름 그대로 스레드를 차단(Block) 할 수 있는 큐다.

주요 구현체

ArrayBlockingQueue

  • 배열 기반, 버퍼 크기 고정
  • 생성 시 크기를 지정해야 함

LinkedBlockingQueue

  • 링크 기반, 크기를 고정하거나 무한으로 설정 가능
  • 기본값은 Integer.MAX_VALUE (사실상 무제한)

메서드 선택 가이드

큐가 가득 찼거나 비었을 때 어떻게 동작할지에 따라 4가지 선택지가 있다.

데이터 추가 (Insert)

메서드 큐가 가득 찼을 때
add(e) IllegalStateException 발생
offer(e) 즉시 false 반환
put(e) 공간이 생길 때까지 대기
offer(e, time, unit) 지정 시간만큼 대기 후 false

데이터 획득 (Remove)

메서드 큐가 비었을 때
remove() NoSuchElementException 발생
poll() 즉시 null 반환
take() 요소가 들어올 때까지 대기
poll(time, unit) 지정 시간만큼 대기 후 null

실무에서의 선택

실무에서 멀티스레드를 사용할 때는 응답성이 중요하다. 상황에 따라 적절한 메서드를 선택해야 한다.

예외를 던지는 방식 (add, remove)

1
2
3
4
5
try {
    queue.add(item);
} catch (IllegalStateException e) {
    log.error("큐가 가득 참, 재시도 또는 에러 처리");
}
  • 큐가 가득 찬 상황 자체가 비정상일 때 사용
  • 예외를 잡아서 알림을 보내거나 로깅할 때 유용

즉시 반환 방식 (offer, poll)

1
2
3
4
if (!queue.offer(item)) {
    log.warn("큐가 가득 참, 아이템 버림");
    // 또는 다른 처리
}
  • 빠른 응답이 필요할 때
  • 데이터 유실이 허용되는 경우

무한 대기 방식 (put, take)

1
2
queue.put(item);  // 공간이 생길 때까지 블록
Item item = queue.take();  // 데이터가 들어올 때까지 블록
  • 데이터 유실이 절대 안 되는 경우
  • 단, 응답성이 떨어질 수 있음

타임아웃 방식 (offer/poll with timeout)

1
2
3
4
if (!queue.offer(item, 5, TimeUnit.SECONDS)) {
    log.error("5초 대기 후에도 큐에 공간 없음");
    // 재시도 또는 에러 처리
}
  • 실무에서 가장 많이 사용하는 방식
  • 무한 대기의 위험을 피하면서 어느 정도 여유를 줌

DevBid에서의 선택

DevBid 경매 프로젝트에서는 단일 JVM의 BlockingQueue 대신 Redis 분산락을 선택

이유

  • 다중 서버 환경에서 동작해야 함
  • 입찰은 데이터 정합성이 최우선 (돈이 오가는 거래)
  • 서버가 죽어도 락 상태가 유지되어야 함

BlockingQueue는 단일 JVM 내에서만 동작한다. 서버를 스케일 아웃하면 각 서버가 별도의 큐를 갖게 되어 동시성 제어가 불가능해진다. 분산 환경에서는 Redis, Zookeeper 같은 외부 저장소를 통한 분산락이 필요하다.

1
2
단일 서버: synchronized, ReentrantLock, BlockingQueue
다중 서버: Redis 분산락, DB Lock, Zookeeper

정리

BlockingQueue는 생산자-소비자 패턴을 깔끔하게 구현할 수 있는 도구다. 다만 단일 JVM에서만 동작하므로, 분산 환경에서는 Redis 분산락 같은 외부 저장소 기반 솔루션이 필요하다. 동시성 제어는 결국 “어디서 락을 잡을 것인가”의 문제라는 걸 다시 한번 느꼈다.


출처: 김영한의 실전 자바 - 고급편

This post is licensed under CC BY 4.0 by the author.