메시지 큐를 언제 써야 하고 언제 쓰면 안 되나

2026. 10. 23. 13:00·IT
728x90
반응형

사용자가 회원가입 버튼을 눌렀는데 응답이 3초 걸린다. 확인해 보니 이메일 발송, 환영 알림, 추천 데이터 생성, 외부 CRM 등록을 모두 기다리고 있었다. 이럴 때 나오는 답이 메시지 큐다. 그런데 큐를 도입하면 시스템이 복잡해지고 새로운 실패 방식이 생긴다. 이 글은 큐가 실제로 푸는 문제와, 도입 전에 알아야 할 대가를 정리한다.

큐가 푸는 세 가지 문제

큐를 쓰는 이유는 대개 셋 중 하나다.

응답 시간 분리. 사용자가 기다릴 필요 없는 작업을 나중으로 미룬다. 회원가입은 계정만 만들고 응답한다. 이메일은 큐에 넣고 워커가 처리한다.

부하 평탄화. 순간적으로 몰리는 요청을 큐가 받아두고 워커가 감당 가능한 속도로 처리한다. 이벤트 시작 시각에 트래픽이 열 배로 뛰어도 데이터베이스는 평소 속도로 처리한다.

결합도 분리. 주문 서비스가 배송·정산·알림 서비스를 직접 호출하지 않는다. 이벤트를 발행하고 관심 있는 쪽이 구독한다. 새 기능을 붙일 때 주문 서비스를 고치지 않아도 된다.

대가: 최소 한 번 전달과 중복 처리

큐를 쓰면 반드시 마주치는 것이 메시지 중복이다.

대부분의 큐는 "최소 한 번(at-least-once)" 전달을 보장한다. 메시지를 처리한 뒤 확인 응답(ack)을 보내는데, 처리는 끝났지만 ack를 보내기 전에 워커가 죽으면 큐는 그 메시지를 다시 준다. 같은 메시지가 두 번 처리된다.

"정확히 한 번(exactly-once)"을 내세우는 시스템도 있지만, 분산 환경에서 완전한 보장은 어렵다. 컨슈머를 멱등하게 만드는 것이 현실적인 해법이다.

def handle(msg):
    if processed_ids.exists(msg.id):    # 이미 처리했으면 건너뛴다
        return
    with transaction():
        do_work(msg)
        processed_ids.add(msg.id)

메시지 ID를 기록하고 중복을 걸러내는 방식이다. 기록과 작업이 같은 트랜잭션에 있어야 안전하다.

대가: 순서가 보장되지 않는다

워커를 여러 개 띄우면 처리 순서가 뒤바뀔 수 있다. "주문 생성" 다음에 "주문 취소"가 왔는데, 취소가 먼저 처리될 수 있다.

대부분의 큐는 파티션이나 그룹 단위로만 순서를 보장한다. 카프카라면 같은 파티션 안에서만 순서가 지켜진다. 그래서 같은 주문의 이벤트가 같은 파티션에 가도록 주문 ID를 키로 쓴다.

순서에 의존하지 않는 설계가 더 견고하다. 메시지에 상태 자체를 싣거나(현재 상태가 무엇인지), 버전 번호를 붙여 오래된 메시지를 무시하는 방식이다.

실패한 메시지를 어디로 보낼 것인가

처리에 실패한 메시지를 계속 재시도하면 그 메시지가 큐를 막는다. 뒤에 있는 정상 메시지가 처리되지 않는다. 잘못된 데이터가 들어온 메시지는 몇 번을 다시 해도 실패한다.

그래서 데드 레터 큐(DLQ)가 필요하다. 재시도 횟수를 넘긴 메시지는 별도 큐로 보내고 본 큐는 계속 흐르게 한다.

DLQ를 만들어 두고 보지 않으면 의미가 없다. DLQ에 메시지가 쌓이면 알림이 가도록 설정해야 한다. 그리고 쌓인 메시지를 어떻게 처리할지(고쳐서 재투입할지, 버릴지) 절차를 정해둔다.

큐를 쓰지 말아야 할 때

여기서 균형을 잡고 싶다. 큐는 만능이 아니고, 잘못 쓰면 디버깅이 지옥이 된다.

결과를 바로 알려줘야 하는 작업은 큐에 넣으면 안 된다. 결제 승인을 비동기로 하면 사용자에게 무엇을 보여줄지가 곤란해진다. "처리 중입니다"를 보여주고 폴링하게 만들면 복잡도가 크게 는다.

작업이 아주 가벼우면 큐 자체의 오버헤드가 더 크다. 밀리초 단위로 끝나는 일을 큐에 넣으면 직렬화, 네트워크, 역직렬화 비용이 배보다 배꼽이다.

트랜잭션 경계를 넘어야 하는 경우도 조심해야 한다. 데이터베이스에 주문을 저장하고 큐에 메시지를 넣는데, 저장은 성공하고 큐 발행이 실패하면 어떻게 되나. 반대로 큐는 성공했는데 트랜잭션이 롤백되면 존재하지 않는 주문에 대한 메시지가 흐른다. 이 문제를 푸는 방법(트랜잭셔널 아웃박스 패턴 등)이 있지만 구현이 늘어난다.

그리고 무엇보다 관측이 어려워진다. 동기 호출은 스택 트레이스 하나로 흐름이 보인다. 큐를 거치면 요청이 어디까지 갔는지 추적하려면 분산 추적이 필요하다. 이 인프라 없이 큐를 도입하면 장애 때 원인을 못 찾는다.

도입 순서

권하는 순서는 이렇다.

먼저 정말 비동기여야 하는지 확인한다. 응답이 느린 이유가 외부 호출이라면, 타임아웃을 줄이거나 병렬 호출로 해결되는 경우가 많다. 3초가 800밀리초가 되면 큐가 필요 없을 수도 있다.

작게 시작한다. 처음부터 카프카를 세우지 말고, 이미 쓰고 있는 것으로 시작한다. Redis 기반 큐나 데이터베이스 테이블을 큐로 쓰는 방식도 규모가 작으면 충분하다. 운영 부담이 훨씬 적다.

한 가지 용도로 시작한다. 이메일 발송 하나만 큐로 옮겨보고, 중복 처리와 DLQ 운영을 겪어본 뒤에 범위를 넓힌다. 여러 개를 한꺼번에 옮기면 문제가 생겼을 때 무엇 때문인지 알 수 없다.

추적을 먼저 붙인다. 메시지에 trace_id를 실어 보내고, 발행과 소비 양쪽에서 로그를 남긴다. 이게 없으면 비동기 시스템은 블랙박스가 된다.

참고 자료

  • Amazon SQS: Dead-letter queues — DLQ 의 동작과 운영 방법
  • Apache Kafka: Design — Message Delivery Semantics — at-least-once 와 exactly-once 의 실제 의미
  • Transactional Outbox — microservices.io — 데이터베이스 쓰기와 메시지 발행의 원자성 문제
728x90
반응형

'IT' 카테고리의 다른 글

알림 피로를 줄이는 SLO 기반 모니터링  (0) 2026.10.25
환경 변수와 시크릿 관리, 어디까지 해야 하나  (0) 2026.10.24
트랜잭션 격리 수준과 데드락 줄이기  (0) 2026.10.22
API 버저닝, 언제 올리고 언제 안 올리나  (0) 2026.10.21
타임아웃과 재시도, 서킷 브레이커까지  (0) 2026.10.20
'IT' 카테고리의 다른 글
  • 알림 피로를 줄이는 SLO 기반 모니터링
  • 환경 변수와 시크릿 관리, 어디까지 해야 하나
  • 트랜잭션 격리 수준과 데드락 줄이기
  • API 버저닝, 언제 올리고 언제 안 올리나
밍글링글링
밍글링글링
mingling - 밍글링, 밍글밍글링. 코드와 어우러지다. IT/ 프로그래밍/소스
    반응형
    250x250
  • 밍글링글링
    mingling
    밍글링글링
  • 전체
    오늘
    어제
    • 밍글링글링 (463)
      • Flutter (2)
      • 일상생활 (8)
        • 리뷰 (1)
        • 생활정보 (4)
        • 맛집 (0)
        • 여행 (0)
        • 모든정보 (3)
      • JAVA (126)
        • 개념 (6)
        • 예제 (115)
        • Exception (2)
      • C (1)
        • C (1)
        • C++ (0)
        • C# (0)
      • JS (29)
        • JavaScript (18)
        • JQuery (5)
        • AJax (0)
        • NODE.JS (6)
        • Angular.JS 2.0 (0)
      • WEB (87)
        • HTML (6)
        • CSS (61)
        • JSP (20)
        • JSTL (0)
      • FrameWork (8)
        • Spring (8)
        • BootStrap (0)
        • MyBATIS (0)
        • JUnit (0)
      • 외부 라이브러리 (5)
      • 공유 소스 관리 (5)
        • Git (5)
        • SVN (0)
      • 빅데이터 프로그래밍 (37)
        • Python (37)
        • R Programming (0)
      • DB (7)
        • ORACLE (0)
        • MySql (6)
      • Development Tools (7)
        • StarUML (0)
        • eXERD (0)
        • Eclipse (4)
      • SKILL (6)
        • Migration (0)
        • Security (6)
      • MicroSoft (0)
        • Excel (0)
        • Word (0)
      • Android (0)
      • Server (21)
        • Ubuntu (5)
        • Linux (15)
      • IOS (0)
      • XML (0)
      • 미디어 (0)
      • 공지사항 (3)
      • NETWORK (1)
      • 게임 (4)
        • 피파 (1)
        • 리니지M (0)
        • 배틀그라운드 (1)
        • 듀랑고 (2)
      • 세상 이슈 (6)
      • 일렉트론 (0)
      • 대회 소식 (4)
      • 업무 (2)
      • Express, Vue (6)
      • docker (11)
      • svelte (3)
      • 블록체인 (1)
      • IT (66)
      • Rust (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 미디어로그
    • 위치로그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    css perspective
    vscode
    rust linux
    VS Code 팁
    java casting
    spring java
    css transition
    자바 for문
    vue 설치
    jsp include
    티스토리 자동화
    제미나이
    클론코딩
    ubuntu
    nginx ssl 적용
    css float
    React Compiler
    jsp parameter
    servlet class
    Node
    자바 배열
    ssl 인증서 발급
    AI 코딩 에이전트
    lang rust
    자바 객체 지향
    gitlab 설치
    자바 생성자
    mysql db
    자바 exception
    css table
    Extension Bisect
    Java Array
    자바 클래스
    SSL 인증서
    nginx
    css tb
    nginx ssl 설정
    에디터 팁
    프런트엔드
    러스트
    오류
    svelte
    API 설계
    Rust lang
    브라우저 자동화
    docker
    proxy pass
    AI 반도체
    vue cli
    리눅스 설치
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
밍글링글링
메시지 큐를 언제 써야 하고 언제 쓰면 안 되나
상단으로

티스토리툴바