Contents

1. 왜 필요한가

2. 핵심 개념

3. Result Backend

4. 도구 비교


1. 왜 필요한가

1.1 동기 처리의 문제

블로킹으로 인해 성능, 확장성, 자원 효율 측면에서 한계가 있다.

  1. Blocking(대기 발생)
    • 작업이 끝날 때 까지 다음 작업 수행 불가
    • I/O 작업(DB, API)에서 심각
  2. 성능 저하
    • CP가 남아있으나 스레드는 대기
  3. 낮은 확장성
    • 요청 수↑ → 스레드 수↑
    • 스레드가 막히면 처리량 한계 도달
  4. Latency 증가
    • 순차 처리 구조
    • 하나의 느린 작업이 전체 지연 유발
  5. 자원 비효율
    • 대기 중인 스레드도 메모리 점유
    • 컨텍스트 스위칭 비용 증가
  6. 사용자 경험 저하
    • 응답 지연 → 서비스 멈춘 느낌
    • 사용자 이탈 가능성 증가

비동기로 백그라운드로 돌리면 되지않나?

1.2 인메모리 비동기

빠르지만, 데이터 유실과 분산 환경에서의 신뢰성 문제가 크다.

  1. 데이터 유실 위험
    • 메모리에만 존재(비영속성)
    • 서버 장애/재시작 시 데이터 유실
  2. 단일 서버 의존성
    • 서버 내부에서만 동작
    • 다른 서버와 작업 공유 불가
  3. 분산 환경에 부적합
    • 여러 서버 간 작업 동기화 어려움
    • 동일 작업 중복 실행 가능성 존재
  4. 장애 복구 어려움
    • 실패한 작업 재처리 불가
    • Retry / DLQ 같은 구조 없음
  5. 처리 보장 없음
    • At-least-once / Exactly-once 보장 불가
    • 작업 누락 가능
  6. 확장성 한계
    • 트래픽 증가 시 서버 메모리에만 의존
    • 수평 확장 구조와 맞지 않음

1.3 왜 Message Broker가 필요한가

인메모리

  • 메모리에만 존재하므로, 작업 유실
  • 각 서버가 자기 메모리에 존재하는 작업만 처리함
  • 작업이 폭주하면 메모리가 가득 차므로 크래시 발생
  • 웹 서버와 워커가 같은 프로세스이므로 분리가 불가능함

메시지 브로커

  • 브로커에 저장되므로 보존
  • 공유 큐 이므로 아무 워커나 처리할 수 있음
  • 디스크에 저장하고, 버퍼 역할을 하므로 안정적임
  • Producer / Broker / Worker가 분리되어 있으므로 독립적으로 확장 가능하다.

2. 핵심 개념

2.1 메시지 흐름과 용어

Producer

  • 작업을 생성해 Broker에 전달(요청) Broker(Queue)
  • 작업을 저장하고 Worker에 전달 Worker(Consumer)
  • Broker에서 작업을 가져와 실제 처리

2.2 큐 분리와 메시지 라우팅

  1. 큐 분리
    • 작업 종류 별로 큐를 분리하여 독립적으로 처리할 수 있다.
    • 이메일 컨슈머가 느려 이메일이 지연되더라도 알림, 인코딩은 빠르게 처리한다.
    • 프로듀서가 메시지를 보내면 브로커가 받아서 보관하고, 워커가 가져가서 처리한다.
  2. 메시지 라우팅
    • Producer가 메시지에 힌트(Routing Key)를 보낸다.
      • review.asset.generate
      • menu.poster.generate
    • Broker가 이를 보고 적절한 큐로 전달(Exchange)한다.
  3. 브로커가 필요한 이유
    • 컨슈머가 바쁘면 브로커에 쌓아둔다.
    • 컨슈머가 죽으면 브로커에 쌓여있는걸 다시 처리한다.
    • 시스템 간 결합도를 감소시켜 독립적인 확장이 가능하다.

2.3 신뢰성 보장 3요소

메시지 큐를 사용하더라도, 유실 / 중복 / 장애 상황을 고려하지 않으면 안정성을 보장할 수 없다.

메시지 큐 + ACK + 멱등성 + 영속성을 만족해야 안정적인 비동기 시스템이 된다.

  1. ACK(처리 보장)
    • Consumer가 처리 완료 후 ACK를 전송한다.
    • Broker는 ACK를 받은 후에만 메시지를 삭제한다.
    • ACK가 오지 않으면 다른 컨슈머에게 다시 전달한다.
    • 따라서 컨슈머가 죽어도 메시지가 유실되지 않음
  2. 멱등성(중복 처리 방지)
    • 같은 메시지를 여러 번 처리해도 결과가 동일하다.
    • 중복 메시지를 수신할 수 있지만, 처리 전에 이미 처리했는지를 확인 한다.
  3. 영속성(데이터 유실 방지)
    • 브로커가 메시지를 디스크에 저장한다.
    • 브로커가 재시작되더라도 메시지가 살아있음

2.4 멱등성

이미 처리한 요청인지 먼저 확인한다.

  1. 적용 방법
    • Redis에 처리 완료한 ID를 저장한다.
    • DB에 unique key / 상태값 체크한다.
    • 멱등키(idempotency key)를 활용한다.
  2. 효과
    • 중복 방지

2.5 실패 처리: 재시도와 DLQ

메시지는 실패할 수 있기 때문에,

재시도 전략과 최종 실패 처리(DLQ)가 필요하다.

  1. 재시도
    • 바로 재시도 하지 않고, 지수 백오프 방식으로 간격을 늘려간다.
    • 외부 시스템 장애(API, DB), 일시적인 오류(네트워크, 타임아웃) 발생 시 빠르게 재시도 하면 실패할 확률이 높음
    • 간격을 늘려 회복 시간을 확보한다.
  2. DLQ
    • 재시도 한도 초과 → 별도 큐로 이동
    • 자동 처리 중단
    • 실패 메시지 보관
    • 개발자가 로그 확인하고 분석 후 재처리

Redis Streams를 브로커로 사용했고, AbstractRedisStreamMessage 메시지에

  • retyrCount: 재시도 횟수
  • nextRetryAt: 다음 재시도 시각
  • expireAt: TTL(만료 시각)
  • retyrFailReason: 실패 사유 정보를 추가하여 지수 백오프 재시도 로직과 DLQ를 애플리케이션 레벨에서 직접 구현해 신뢰성을 확보 했다.(작업 별로 TTL, MAXLEN, retryCount를 다르게 가져감 / 구현 복잡도, 운영 부답 증가)

3. Result Backend

3.1 Result Backend

비동기 작업은 요청과 처리가 분리되기 때문에,

작업의 상태와 결과를 별도로 저장해야 한다.

  1. 비동기 구조의 한계
    • 요청 시 바로 결과를 알 수 없음
    • 작업 완료 여부 확인이 필요함
  2. 상태 추적
    • PENDING: 작업 큐에 들어감
    • STARTED: 워커가 작업을 처리 중
    • SUCCESS: 작업 완료(성공)
    • FAILURE: 작업 실패
  3. 동작 흐름
    • Client: 작업 요청
    • Producer: 메시지 큐 전달
    • Worker: 작업 수행
    • Result Backend: 상태/결과 저장
    • Client: 상태 조회

3.3 Task 상태 조회 - AsyncResult

  • taksID 기반 조회
    • 현재 상태 + 결과 반환
  • Polling 방식으로 결과 확인

3.4 결과 저장과 TTL

Result Backend는 작업의 상태와 결과를 저장하지만,

모든 결과를 영구적으로 보관할 필요는 없다.

  1. Redis
    • 빠른 조회(Low Latency)
      • 상태 조회는 빈번하게 발생
      • ms 단위 응답 필요
    • 결과 재조회 필요 없음
      • key 단위 만료 자동 처리(TTL)
      • 별도 정리 로직 불필요
  2. DB
    • 결과를 장기 보관해야 할 때
    • 영상, 주문, 결제 등 중요한 데이터

3.5 Celery 설정 - Broker + Backend + TTL

각 역할과 데이터 특성에 따라 TTL 전략을 다르게 가져간다.

같은 Redis, 다른 DB 번호

  • DB 0 → Broker
  • DB 1 → Result Backend

TTL 전략

  1. 불필요한 결과
    • 짧은 TTL 설정
    • 자동 삭제(TTL)
  2. 중요한 결과
    • TTL을 길게 설정
    • 필요 시 장기 조회 가능
  3. 반영구(진짜 중요한) 데이터
    • 별도 DB에 저장
    • Redis X
  • 필요 없는 결과는 TTL로 지운다.
  • 중요한 결과는 TTL을 길게 가져간다.
    • 진짜 중요하면 별도 DB에 저장함
  • Result Backend는 조회용으로만 사용

3.6 결과 조회 패턴

비동기 작업은 즉시 결과를 알 수 없기 때문에,

작업 상태를 조회하는 별도의 방식이 필요하다.

  1. Polling(대부분 이 방식 사용)
    • 구현이 단순하지만, 불필요한 요청과 지연이 발생하는 방식
    • 리소스 낭비(계속 요청 → Long Polling)
      • Polling 간격 만큼 응답 지연
    • 트래픽 증가
      • 클라이언트 수 많아질수록 요청 폭증
  2. Callback(Webhook)
    • 작업 완료 시 서버 → 클라이언트로 요청 전송
    • 불필요한 요청 없음
    • 즉시 결과 전달 가능
    • 구현 복잡
      • 클라이언트가 API 서버를 가져야 함
      • 보안/인증 처리 필요
  3. WebSocket
    • 클라이언트와 서버가 연결을 유지
    • 작업 완료시 서버가 실시간으로 결과 ㅔush
    • 연결 유지 비용 발생
    • 서버 구현 복잡

3.7 EatDa에서의 Result Backend

실제 프로젝트에서는 다음과 같은 방식으로 비동기 결과를 처리했다.

  • FastAPI → 영상의 임시 URL 반환
  • Spring 서버 → 해당 URL에 접근하여 영상 다운로드
  • EC2 내부에 영상 저장
  • MySQL → 영상 접근 URL(public IP 기반) 저장

Result Backend로 MySQL를 선택한 이유:

  • 영상 결과는 반영구적으로 보관해야 하는 데이터
    • 영상은 한번 생성되면 지속적으로 조회됨
    • TTL 기반으로 삭제되는 구조는 부적합함
    • 따라서 Redis 보다 영구 저장이 가능한 DB가 적절하다고 판단함

한계:

  1. API 서버 부하 증가
    • 영상 다운로드는 네트워크 I/O가 큰 작업
    • Spring 서버가 요청 처리 + 파일 처리 모두 수행
  2. 확장성 한계
    • 트래픽 증가 시 서버가 모든 작업을 직접 처리
    • 수평 확장 시에도 동일한 문제 반복
  3. 저장 구조의 한계(영상 데이터 Ec2 내부 저장)
    • 서버에 종속적인 구조
    • 일반적으로는 S3와 같은 외부 스토리지 사용

개선 방향:

  • 무거운 I/O 작업을 lambda로 분리
  • Fast API → 임시 URL 반환
  • Lambda(영상은 15초 내외 임)
    • 영상 다운로드(Streaming 방식)
    • S3 업로드
  • MySQL → 최종 URL 저장

효과:

  1. 서버 부하 분리
    • API 서버는 요청 처리에 집중
    • 무거운 작업은 별도 실행 환경에서 처리
  2. 자동 확장
    • 요청 증가 시 Lambda가 자동으로 확장
    • 별도 인프라 관리 필요 없음

3.8 워크플로우: Chain & Chord

메시지 큐 기반 비동기 처리에서는

작업을 흐름(Workflow)으로 구성할 수 있다.

  1. Chain(순차 실행)
    • 앞 작업의 결과를 다음 작업이 이어받아 실행
    • 작업이 순서대로 실행됨
    • 결과가 다음 단계의 입력으로 전달됨
    • 데이터 가공 파이프라인 / 단계별 처리(검증 → 변환 → 저장)에 사용
  2. Chord(병렬 실행 + 집계)
    • 여러 작업을 병렬로 실행하고, 모두 끝난 후 다음 단계 실행
    • 여러 작업을 동시에 실행
    • 하나라도 실패/지연 → 다음 단계 대기
    • 여러 API 호출 결과 집계 / 이미지·영상 병렬 처리 후 결과 합치기에 사용

작업 상태를 추적해야 할 때 Result Backend를 사용한다.


4. 도구 비교

4.1 Broker, 어떤 도구를 쓸까

메시지 큐는 안전성, 라우팅, 확장성 요구사항에 따라 선택한다.

  1. Redis(List 기반 Queue)
    • 캐시/데이터 저장소로 이미 사용 중이면 쉽게 도입할 수 있음
    • Broker 방식: List(LPUSH / BPROP)
    • 빠르고 단순한 구조
    • 별도 인프라 없이 사용 가능
    • 메시지 안전성 낮음
      • 소비 시 즉시 제거 → 장애 시 유실 가능
    • 라우팅 기능 부족
    • 복잡한 워크플로우 처리 어려움
  2. Redis Streams
    • Redis 기반 로그형 메시지 큐
    • Broker 방식: Stream + Consumer Group
    • 메시지가 삭제되지 않고 로그 형태로 저장
    • Consumer Group 기반 병렬 처리
    • ACK 기반 처리 및 재처리 가능
      • List보다 높은 신뢰성 제공
    • 라우팅 기능 제한적
    • PENDING 메시지 관리 필요(운영 복잡도 증가)
    • RabbitMQ/Kafka 수준의 메시징 기능은 아님
  3. RabbitMQ
    • 메시지 큐 전용 시스템
    • Broker 방식: Exchange + Queue 구조
    • 메시지 안전성 높음
      • ACK 기반 처리
    • 유연한 라우팅 지원
    • 다양한 메시징 패턴 제공
    • 별도 운영 필요
    • Result Backend 별도 구성 필요

4.2 Exchange 타입 비교

메시지를 어떻게 분배할 것인가

  1. Direct
    • 라우팅 키 정확히 일치
      • review.asset.generate
      • menu.poster.generate
    • 1:1 매칭
  2. Topic
    • 패턴별로 매칭
      • user.*
      • order.*
    • 유연한 라우팅
  3. Fanout
    • 모든 큐에 전달
      • 브로드캐스트

4.3 시나리오별 추천

4.4 Redis 안전성 설정 - 3가지 핵심

Redis는 기본적으로 메시지 큐 기능이 제한적이기 때문에,

ACK, Retry, Durability를 직접 보완해야 한다.

  1. ACK(처리 보장)
    • RPOPLPUSH 패턴으로 구현
      • 메시지를 처리 전에 작업 중 큐로 이동
      • 처리 완료 후 삭제
    • 워커가 죽어도 메시지 복구 가능
  2. Retry
    • 애플리케이션 레벨에서 구현
      • 실패 시 재시도
      • 일정 횟수 초과 시 DLQ로 이동
  3. Durabillity
    • AOF / RDB 설정
      • Redis 데이터를 디스크에 저장
      • 재시작 시 데이터 복구

4.5 메시지 큐 vs Kafka

Kafka는 메시지 큐가 아니라

이벤트 스트리밍 플랫폼(Event Streaming Platform) 이다.

  1. 메시지 큐(작업 처리 중심)
    • 메시지는 한 번 처리되면 사라짐
    • Consumer가 처리하면 → 삭제
    • 하나이 작업을 하나의 Consumer가 처리
    • 비동기 작업 처리 및 시스템 간 결합도 감소가 목적임
    • 이메일 발송 / 영상 인코딩 / 주문 처리
  2. 이벤트 스트리밍(데이터 흐름 저장 + 재사용)
    • 메시지가 로그 형태로 계속 저장됨
    • Consumer가 읽어도 데이터 유지
    • 여러 Consumer가 독립적으로 소비 가능
    • 데이터 수짐 및 분석, 이벤트 기반 아키텍처, 데이터 파이프라인 구축이 목적임
    • 로그 수집 / 실시간 데이터 분석 / 추천 시스템