[Thread] 생산자-소비자 패턴
DevBid 프로젝트에서 입찰 동시성 제어를 Redis 분산락으로 구현했지만 자바 기본 동시성 메커니즘을 제대로 이해하고 싶어 김영한 선생님의 자바 고급편 강의를 듣고 있다. 그 중 생산자-소비자 문제를 BlockingQueue 로 이해하고 정리한 포스팅이다.
락의 2단계 대기 상태
synchronized, ReentrantLock 모두 2가지 대기 상태가 존재한다.
synchronized 대기
- 모니터 락 획득 대기
wait()대기
ReentrantLock 대기
ReentrantLock락 획득 대기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 분산락 같은 외부 저장소 기반 솔루션이 필요하다. 동시성 제어는 결국 “어디서 락을 잡을 것인가”의 문제라는 걸 다시 한번 느꼈다.
출처: 김영한의 실전 자바 - 고급편