Skip to content

[제안] Context→AI keyword 요청 경로에 메시징큐 도입 검토 (안 A/B/C 비교) #129

Description

@minyongP

요약

Record(Context) 저장 후 keyword·임베딩 생성을 위해 back이 FastAPI를 호출하는 경로에 메시징큐(MQ) 도입을 검토하자는 제안입니다. 세 가지 안을 비교했고, 추천은 안 C(back 내부에만 도입) → 팀 합의 후 안 B(완료 이벤트 역방향)를 후속으로입니다.

안 C는 back 혼자 완결할 수 있어 다른 파트 코드 변경이 없고, 안 B는 AI 파트와의 계약 변경이 필요해서 이 이슈에서 미리 공유하고 의견을 받으려 합니다.

배경 — 지금은 어떻게 동작하나

먼저 현재 구조입니다. 다른 파트 분들도 맥락을 잡을 수 있게 처음부터 설명합니다.

flowchart LR
    A["Record 저장 API<br/>(PENDING 기록 후 커밋)"] -->|"커밋 후 이벤트"| B["@Async 리스너<br/>(인메모리 큐)"]
    B -->|"HTTP 호출<br/>실패는 삼킴"| F["FastAPI 워커<br/>(202 = 접수만)"]
    A -->|"같은 트랜잭션에서 기록"| D[("ai.context_ai_state<br/>상태 테이블")]
    D -->|"오래된 PENDING 회수"| E["재스캔 스케줄러<br/>(주기 폴링)"]
    E -->|"HTTP 재요청"| F
Loading
  1. 사용자가 Record를 저장하면, back은 같은 트랜잭션 안에서 ai.context_ai_statePENDING 상태를 기록하고 커밋합니다.
  2. 커밋이 끝나면 리스너가 별도 스레드(인메모리 큐)에서 FastAPI의 POST /internal/v1/context/process를 호출합니다. 이 호출은 **"지금 처리해 주면 빨라지는 힌트"**일 뿐이라, 실패해도 예외를 올리지 않고 로그만 남깁니다. 202 응답도 "접수했다"는 뜻이지 "처리 끝났다"는 뜻이 아닙니다.
  3. 어떤 이유로든 처리가 안 되면(호출 실패, 인메모리 큐 포화로 버려짐, FastAPI가 접수 후 내부에서 실패) 상태 테이블에 PENDING이 남고, 재스캔 스케줄러가 주기적으로 오래된 PENDING을 집어 재요청합니다. 재시도 예산을 소진하면 FAILED로 종결합니다.

현재 구조도 이미 "비동기 + 실패 시 재시도"입니다. 큐 역할을 브로커가 아니라 DB(상태 테이블)가 하고 있을 뿐이고, 최소 한 번 처리(at-least-once)는 보장됩니다.

  • 관련 코드: back 레포 domain/ai/ 패키지 (ContextAiRequestedListener, AiProcessClient, AiRescanScheduler)
  • 관련 명세: docs/ai/spec/ai-integration.md, docs/ai/spec/ai-rescan-scheduler.md (AI 파트 소유)

왜 MQ를 검토하나

솔직하게 두 가지입니다.

  1. 기술 경험·포트폴리오: 프로듀서/컨슈머, ack, 재전달, DLQ(처리에 계속 실패한 메시지를 따로 모아두는 큐) 같은 MQ 핵심 패턴을 실제 프로젝트에서 다뤄보고 싶습니다.
  2. 현재 구조의 실제 약점 보완: 커밋 후 호출이 쓰는 인메모리 큐는 포화되면 요청을 그냥 버립니다(재스캔이 나중에 줍긴 하지만, 재스캔 주기만큼 지연됩니다). 또 FastAPI가 접수 후 내부에서 실패한 경우 back은 그 사실을 알 방법이 없어서, 폴링으로 "오래됐는데 아직 PENDING이네?"를 추측하는 게 유일한 복구 수단입니다.

반대로 말하면 유실 방지 자체는 이미 해결돼 있어서, "신뢰성 때문에 MQ가 필수"라는 주장은 하지 않습니다. 어디에 어떻게 넣느냐에 따라 얻는 게 달라져서 세 가지 안을 나눴습니다.

안 A — 요청 방향을 MQ로 전환 (비추천)

back이 HTTP 대신 브로커에 발행하고, FastAPI가 컨슈머로 소비합니다.

flowchart LR
    A["Record 저장 API<br/>(HTTP 대신 발행)"] --> Q[["메시지 브로커<br/>(재전달·DLQ)"]]
    Q --> F["FastAPI 컨슈머<br/>(신규 구현 필요)"]
    N["DB 상태 + 재스캔<br/>그대로 필요"]:::note
    F -.-> N
    classDef note stroke-dasharray: 5 5
Loading
  • 장점: 가장 교과서적인 그림. FastAPI가 죽어 있는 동안에도 요청이 브로커에 쌓입니다.
  • 단점: 브로커는 "전달"까지만 보장하고 "처리 완료"는 보장하지 못합니다. FastAPI가 메시지를 받아놓고 내부에서 실패하는 경우는 지금과 똑같이 남아서, DB 상태 추적과 재스캔을 없앨 수 없습니다. 결과적으로 재시도 장치가 두 겹(브로커 재전달 + DB 재스캔)이 되어 원인 추적만 복잡해집니다.
  • 파트 영향: AI 파트가 FastAPI에 컨슈머를 새로 구현해야 하고(변경량 최대), 연동 명세도 전면 개정이 필요합니다.

비용 대비 효과가 가장 낮아 접는 것을 제안합니다.

안 B — 완료 이벤트 역방향 MQ (후속 제안)

요청은 지금처럼 HTTP로 두고, FastAPI가 처리 완료/실패 이벤트를 브로커에 발행하면 back의 신규 컨슈머가 소비해 상태 테이블을 직접 갱신합니다.

flowchart LR
    A["back: Record 저장 API<br/>(HTTP 호출 기존 유지)"] --> F["FastAPI 워커<br/>(처리 후 이벤트 발행)"]
    F --> Q[["메시지 브로커<br/>(완료·실패 이벤트)"]]
    Q --> C["back 컨슈머 (신규)<br/>상태 직접 갱신"]
Loading
  • 장점: 현재 구조에서 완전히 비어 있는 "완료 통보" 방향을 새로 만드는 안입니다. back이 처리 결과를 즉시 알게 되므로 재스캔 폴링은 주 메커니즘에서 최종 안전망으로 내려가고, keyword 반영 지연도 줄어듭니다. 아키텍처 개선 효과가 가장 큽니다.
  • 단점: 발행 측 코드가 FastAPI(AI 파트 소유)라서 AI 파트의 구현과 명세 합의가 선행돼야 합니다. 이벤트 스키마(무슨 필드를 실어 보낼지), 발행 실패 시 동작 등을 함께 정해야 합니다.
  • 파트 영향: AI — FastAPI에 이벤트 발행 코드 추가. INFRA — 브로커 운영. BE — 컨슈머 구현과 상태 갱신 로직.

안 C — back 내부에만 MQ 도입 (추천 시작점)

커밋 후 호출이 쓰는 인메모리 큐를 브로커로 교체합니다. back 안의 컨슈머가 큐에서 꺼내 지금과 똑같은 HTTP 호출을 보내므로, FastAPI와의 계약은 한 글자도 바뀌지 않습니다.

flowchart LR
    A["Record 저장 API<br/>(커밋 후 발행)"] --> Q[["메시지 브로커<br/>(재시도·백오프·DLQ)"]]
    Q --> C["back 컨슈머<br/>(HTTP 호출 수행)"]
    C -->|"기존 계약 그대로"| F["FastAPI 워커"]
    E["재스캔 스케줄러"] -.->|"최종 안전망 유지"| F
Loading
  • 장점:
    • 다른 파트 코드 변경이 없어 back 혼자 완결할 수 있습니다.
    • 포화 시 요청을 버리는 인메모리 큐가 내구성 있는 큐로 바뀌고, 브로커 수준의 재시도·백오프·DLQ가 생겨 실패가 재스캔 주기를 기다리지 않고 복구됩니다.
    • 프로듀서/컨슈머, ack, 같은 메시지가 두 번 와도 안전하게 처리하는 설계(멱등성) 등 MQ 학습 요소가 전부 들어갑니다.
  • 단점: 아키텍처 관점에서는 기존 재스캔과 역할이 일부 겹칩니다(재스캔은 안전망으로 유지). "202 이후 FastAPI 내부 실패"는 이 안으로는 해결되지 않습니다 — 그건 안 B의 몫입니다.
  • 파트 영향: BE — 발행·컨슈머 구현. INFRA — 브로커 하나 추가(아래 참고). AI — 없음.

브로커 선택 — Redis Streams vs RabbitMQ vs Kafka (INFRA 협의 필요)

세 후보의 성격이 서로 달라서, 먼저 우리 워크로드가 뭔지부터 짚습니다. 이 경로는 "작업 하나를 컨슈머 하나가 집어 처리하고, 실패하면 나중에 다시 시도하는" 작업 큐(task queue) 패턴입니다. 처리량은 사용자의 Record 저장 빈도만큼이라 초당 수십 건 수준을 넘지 않고, 같은 메시지를 여러 소비자가 각자 읽을 일도(현재로서는) 없습니다.

Redis Streams RabbitMQ Kafka
본질 인메모리 저장소의 스트림 자료구조 메시지 브로커 (작업 큐 특화) 분산 커밋 로그 (이벤트 스트리밍 특화)
인프라 추가 없음 (운영 중인 Redis 사용) 컨테이너 1개 브로커 컨테이너 + 상대적으로 큰 메모리·디스크
개별 메시지 ack / 재전달 있음 (pending 목록 + XAUTOCLAIM) 있음, 가장 자연스러움 (ack/nack + requeue) 없음 — offset 커밋 방식이라 "이 메시지만 나중에 다시" 가 기본기로 안 됨
재시도 백오프·DLQ 직접 구현 기본 제공에 가까움 (DLX + TTL 조합) 직접 구현 (재시도 토픽 체인 패턴)
메시지 보존·재생(replay) 제한적 (maxlen 관리, 영속성은 Redis 설정 의존) 소비하면 사라짐 (큐 의미론) 강함 — 보존 기간 내 언제든 처음부터 다시 읽기 가능
다수 소비자가 같은 데이터를 각자 읽기 가능하지만 관리 번거로움 익스체인지 바인딩으로 가능 가장 자연스러움 (컨슈머 그룹별 독립 offset)
운영 난이도 낮음 (이미 운영 중) 낮음~중간 (관리 UI 제공) 높음 — 파티션·offset·보존 정책 등 개념 부담과 리소스 요구가 가장 큼

각 도구의 트레이드오프를 우리 상황에 대입하면:

  • Redis Streams — 비용이 가장 쌉니다. 인프라 추가가 0이고 안 C에 필요한 기능(컨슈머 그룹, ack, 죽은 컨슈머의 메시지 회수)은 다 됩니다. 대신 백오프·DLQ를 손으로 짜야 해서 "MQ를 배웠다"기보다 "Redis로 MQ를 흉내 냈다"에 가깝고, 브로커 운영 경험은 남지 않습니다.
  • RabbitMQ워크로드와 도구의 성격이 가장 일치합니다. 우리가 원하는 게 정확히 작업 큐이고, 개별 ack·재전달·DLQ·지연 재시도가 도구의 기본 문법이라 설계가 단순해집니다. 컨테이너 하나가 추가되지만 운영 부담은 크지 않습니다.
  • Kafka — 강점(대용량 처리량, 재생, 다수 독립 소비자)이 전부 지금 우리에게 없는 요구입니다. 반대로 우리가 필요한 것(개별 메시지 재시도·DLQ)은 Kafka의 약점이라 재시도 토픽 체인 같은 우회 패턴을 직접 짜야 하고, 운영 부담도 셋 중 가장 큽니다. 즉 순수하게 안 C만 보면 명백한 과투자입니다. Kafka가 정당해지는 조건은 안 B 이후의 그림입니다 — 처리 완료 이벤트를 back뿐 아니라 다른 소비자(예: 통계, 알림)도 각자 읽는 "이벤트 백본"으로 키울 계획이 있다면, 그때는 재생과 다중 소비자가 실제 요구가 되므로 Kafka가 맞는 도구가 됩니다. 이력서·포트폴리오 가치가 가장 큰 것도 사실이라, "학습 목적으로 과투자를 감수한다"는 선택지 자체는 열어 둡니다. 다만 그 경우에도 감수하는 것이 무엇인지(운영 복잡도, 재시도 우회 구현)는 위 표대로입니다.

정리하면: 안 C만 한다면 RabbitMQ(패턴 일치) 또는 Redis Streams(최소 비용) 중 택일, 안 B까지 가서 이벤트를 여러 소비자가 쓰는 그림이면 그때 Kafka를 다시 올려놓고 논의하는 것을 제안합니다. 어느 쪽이든 배포·운영은 INFRA와 협의가 필요합니다.

제안 순서

  1. 안 C를 먼저 진행합니다 — back 내부에서 완결되고, 실패 복구 지연과 인메모리 큐 유실 문제를 바로 개선합니다.
  2. 운영해 보고 팀 합의가 되면 안 B를 후속으로 — 완료 통보가 생기면서 재스캔이 안전망으로 내려갑니다.
  3. 안 A는 진행하지 않습니다.

논의하고 싶은 것

  • AI 파트: 안 B의 방향성에 대한 의견 (FastAPI에서 처리 완료/실패 이벤트를 발행하는 것이 부담스럽지 않은지, 언제쯤 가능할지). 안 C는 AI 파트 변경이 없지만 연동 경로가 바뀌는 것이라 공유드립니다.
  • INFRA 파트: 브로커 선택(Redis Streams vs RabbitMQ vs Kafka)과 배포·운영 관점 의견. 특히 Kafka는 서버 리소스 여유가 판단 기준이 될 것 같습니다.
  • 공통: 안 C 착수 자체에 대한 이견 여부.

의견 주시면 반영해서 설계를 확정하고, Jira 티켓을 만들어 진행하겠습니다.

Metadata

Metadata

Labels

enhancement새 기능·개선 (feat)

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions