큐로 처리 흐름을 분리하는 방법

FIFO 큐의 구조와 동시성 제어, 백프레셔, 재시도·DLQ 정책을 통해 안정적인 비동기 처리 흐름을 설계하는 방법

2026-08-14 · 최초 발행 2024-04-29

요청이 몰릴 때 큐가 맡는 역할

큐는 생산자와 소비자의 처리 속도가 다를 때 그 간극을 흡수하는 선입선출(FIFO) 자료구조다. 단일 프로세스의 자료구조로 사용할 수도 있고, 분산 메시징 인프라까지 확장할 수도 있다.

시스템 설계와 동시성 프로그래밍에서는 처리량 확장, 지연 완화, 컴포넌트 간 결합도 감소를 위해 큐를 둔다. 기본 큐의 삽입(Enqueue)과 삭제(Dequeue)는 평균적으로 O(1) 연산을 보장한다.

큐와 함께 고려할 수 있는 구조는 다음과 같다.

  • 우선순위 큐는 키를 기준으로 우선순위를 정렬하며, 힙 기반으로 O(log n) 연산 특성을 가진다.
  • 지연 큐 또는 타이머 큐는 정해진 시점에 이른 항목만 소비할 수 있게 한다.
  • 원형 버퍼(링 버퍼)는 고정 크기 배열과 순환 포인터를 사용해 재사용하는 고성능 큐 구조다.
  • 블로킹 큐와 논블로킹 큐는 각각 생산자·소비자 대기 제어, CAS 기반 락-프리 진행에 초점을 둔다.

데크(Deque)는 양쪽에서 삽입과 삭제를 지원한다. 반면 큐는 보통 한쪽에 넣고 반대쪽에서 꺼내는 흐름을 전제로 한다.

메모리 배치와 동시성 모델이 바꾸는 특성

배열 기반이나 링 버퍼 기반 구현은 캐시 친화적인 접근과 상수 시간 연산에 유리하다. 연결 리스트 기반 큐는 동적으로 늘리기 쉽지만, 메모리 단편화와 캐시 미스가 늘어날 수 있다.

동시성 제어 방식도 선택 기준이다. 락 기반 큐는 뮤텍스와 조건변수를 사용하므로 구현이 비교적 단순하지만, 경쟁 구간이 커질 수 있다. 락-프리 큐는 CAS와 메모리 배리어를 사용해 낮은 지연과 높은 처리량을 노릴 수 있는 대신 ABA 문제와 구현 복잡도를 함께 안는다.

과부하를 제어하려면 유한 버퍼 크기와 생산자 대기·거부 정책을 정해야 한다. 소비 속도에 맞춰 대역폭 제한이나 배치 크기를 조절하는 방식도 적용할 수 있다.

메시지 큐 관점에서는 디스크 퍼시스턴스, ACK/NACK, 재시도 정책으로 최소 1회 전달(at-least-once) 또는 정확히 1회 전달(exactly-once, 상황 의존)을 달성한다. 순서 보장 역시 파티션 단위인지, 글로벌 순서를 보장하지 않는지 명확히 해야 한다.

운영 중에는 대기열 길이, 처리율(Throughput), 소비 지연(Latency), 재시도·사망 큐(DLQ) 비율을 본다. 버퍼 포화율, 처리 지연 백분위(P95/P99), NACK 급증, 컨슈머 랙 증가는 경보 기준이 될 수 있다.

생산부터 재시도 격리까지의 흐름

생산자는 요청을 받아 메시지를 직렬화·검증하고, 파티션 또는 키를 기준으로 라우팅한다. 버퍼에 여유가 있으면 Enqueue를 수행하고, 포화 상태에서는 백프레셔나 거부·드랍 정책이 작동한다.

소비자는 Dequeue한 메시지를 비즈니스 로직으로 처리한 뒤 성공하면 ACK를 보낸다. 실패한 작업은 재시도하거나 DLQ로 옮긴다. 일시 오류에는 지수 백오프 재시도를 적용하고, 재시도 횟수를 넘기면 DLQ로 격리한다. 치명 오류는 즉시 DLQ로 보내고 알림을 발행한다.

동일 저장소 안에서는 Outbox 패턴을, 메시지 브로커에서는 트랜잭션을 사용해 원자성을 보강할 수 있다. 컨슈머 단위의 오프셋 커밋과 락 점유 시간은 최소화하고, 아이들포텐시(멱등성) 키를 설계해 중복 처리에도 안전하게 만든다.

ProducersenqueueenqueuedequeuedequeueACKNACK/Retryexceeded retriesif fullthrottlethrottleProducer AProducer BQueue BufferConsumer 1Consumer 2Dead Letter QueueBackpressure Signal

구현 방식별 선택 지점

구현 방식 성능(처리량/지연) 확장성 일관성(순서/중복) 안정성(오류 복원) 운영 편의
배열 기반 큐 높음/낮음, 캐시 친화적 특성 중간, 리사이즈 비용 존재 단일 파티션 순서 보장 용이 중간, 포화 시 드랍 위험 단순, 메모리 관리 용이
연결 리스트 기반 큐 중간/중간, 캐시 미스 증가 높음, 동적 확장 용이 순서 보장 용이, 오버헤드 존재 중간, GC 영향 가능 구현 용이, 튜닝 범위 넓음
링 버퍼(원형 버퍼) 매우 높음/매우 낮음, 상수 시간 제한적, 고정 크기 가정 단일 샤드 순서 매우 안정 높음, 예측 가능 자원 사용 초기 설계 요구 높음
락-프리 큐 매우 높음/낮음, CAS 기반 높음, 멀티코어 효율 순서 보장 제한(경쟁 시) 중간, ABA·메모리 순서 이슈 디버깅 난이도 높음

비동기 처리 경계로 사용하는 경우

이미지 변환, 메일/SMS 발송, 배치 ETL은 큐로 넘겨 피크 부하를 흡수하고 평균화할 수 있다. 주문 이벤트를 재고·결제·알림 서비스로 비동기 분리하는 이벤트 주도 아키텍처도 대표적인 사용 방식이다.

토큰 버킷과 큐를 함께 두면 API 호출을 평활화하는 레이트 리미팅·스무딩 구성이 가능하다. 트랜잭션 아웃박스 패턴에서는 DB 상태 변경과 메시지 발행의 원자성을 보장하며, 재처리와 멱등 설계가 전제가 된다.

Python에서 생산자와 소비자 연결하기

표준 라이브러리만 사용해 생산자-소비자 패턴, 유한 큐 기반 백프레셔, 오류 재시도를 간단히 구현한 예시다.

import threading, time, random
from queue import Queue, Full, Empty

q = Queue(maxsize=100)  # 유한 버퍼로 백프레셔 적용

def producer(pid: int, n: int):
    for i in range(n):
        item = (pid, i)
        while True:
            try:
                q.put(item, timeout=0.1)  # 포화 시 대기
                break
            except Full:
                time.sleep(0.01)

def consumer(cid: int, stop_event: threading.Event):
    while not stop_event.is_set() or not q.empty():
        try:
            pid, i = q.get(timeout=0.1)
        except Empty:
            continue
        try:
            # 가끔 실패 시뮬레이션
            if random.random() < 0.02:
                raise RuntimeError("transient error")
            # 처리 로직
            time.sleep(0.001)
        except Exception:
            # 단순 재시도: 최대 3회
            retries = 0
            while retries < 3:
                try:
                    time.sleep(0.01 * (2 ** retries))
                    # 재시도 처리
                    break
                except Exception:
                    retries += 1
            # 실패 시 DLQ로 보낸다고 가정(여기서는 로그로 대체)
        finally:
            q.task_done()

if __name__ == "__main__":
    stop = threading.Event()
    producers = [threading.Thread(target=producer, args=(p, 5000)) for p in range(4)]
    consumers = [threading.Thread(target=consumer, args=(c, stop)) for c in range(4)]
    [t.start() for t in producers + consumers]
    [t.join() for t in producers]
    q.join()  # 큐 비워질 때까지 대기
    stop.set()
    [t.join() for t in consumers]
    print("done")

이 구조에서는 maxsize 튜닝, 컨슈머 수 증가, DLQ 분기 구현, 멱등 키 적용을 확장 포인트로 둔다.

부하를 흡수하면서 얻는 변화

피크를 평균화하면 P95 지연이 30~70% 감소할 수 있으며, 락 경합 구간을 제외하면 컨슈머 확장에 따라 선형에 근접한 처리량 증가를 기대할 수 있다. 서비스 간 결합도는 30% 이상 감소하고, 장애 전파를 막아 부분적 성능 저하 상태에서도 복원탄력성을 높인다.

고부하 시간대에 스로틀링을 적용하면 인프라 초과 프로비저닝을 줄이고 유휴 시간의 처리 여력을 활용할 수 있다.

정책과 관측을 함께 설계하기

서비스별 SLO를 기준으로 큐 깊이 상한, 재시도 횟수, 백오프 곡선을 정한다. 비즈니스 키를 이용한 중복 감지 테이블이나 분산 락 최소화 전략으로 멱등성을 보장한다.

대기열 길이, 처리량, 오류율, DLQ 유입률은 대시보드로 묶고 포화율 알림을 설정한다. 키 기반 파티션으로 순서 보장 범위를 분명히 하며, 해싱·셔플링으로 핫 파티션을 완화한다. 컨슈머 일시 중단, 느린 소비, 브로커 장애, 네트워크 분할 상황은 카오스 테스트로 점검한다.

자료구조동시성백프레셔메시지 큐