-
이코에코(Eco²) Message Queue #1: MQ 적용 가능 영역 도출이코에코(Eco²)/Message Queue 2025. 12. 21. 22:52
개요
RabbitMQ + Celery 아키텍처 설계에서는 Chat/Scan API의 AI 파이프라인을 비동기로 전환하는 계획을 수립했다. 그러나 MQ 도입의 효과는 AI 파이프라인에만 국한되지 않는다.
본 글에서는 이코에코 백엔드 전체를 분석하여 RabbitMQ 적용으로 개선할 수 있는 영역을 도출하고, 우선순위를 정한다.
분석 대상
도메인 역할 현재 부하 특성 chat 폐기물 분류 챗봇 GPT API 호출 (10~30초) scan 이미지 스캔 + 리워드 GPT API + gRPC 호출 character 캐릭터 발급 + 동기화 DB + gRPC 호출 image 이미지 업로드 S3 Presigned URL location 위치 검색 PostGIS 쿼리 my 사용자 프로필 CRUD auth 인증/인가 Redis 블랙리스트 ext-authz JWT 검증 Redis 조회 (1,100 RPS)
1. ext-authz: Local Cache + MQ Event Broadcasting
현재 상태
ext-authz 성능 튜닝에서 Redis Pool Size 튜닝과 HPA 수평 확장으로 성능을 개선했다.
[현재 튜닝된 구조] 모든 API 요청 → ext-authz → Redis EXISTS blacklist:{jti} ↓ Redis Pool (PoolSize: 200, MinIdleConns: 50)지표 튜닝 전 튜닝 후 비고 Redis PoolSize 20 200 10배 증가 MinIdleConns 0 50 Cold Start 방지 PoolTimeout 4s 2s 빠른 실패 (Backpressure) 달성 RPS ~450 1,100+ 단일 노드 기준 p99 Latency 500ms+ 250~350ms 부하 시 Success Rate 95% 99.8% 현재 아키텍처의 근본적 한계
Redis Pool 튜닝으로 단기적 병목은 해소했으나, Redis 싱글 스레드 특성으로 인한 구조적 한계가 존재한다:
┌────────────────────────────────────────────────────────────────┐ │ Redis 싱글 커맨드 큐 병목 │ ├────────────────────────────────────────────────────────────────┤ │ │ │ ext-authz-1 ──┐ │ │ ext-authz-2 ──┼──▶ Redis (싱글 스레드) ──▶ [커맨드 큐] ──▶ 순차 처리 │ │ ext-authz-3 ──┘ ↑ │ │ 여기가 병목! │ │ │ │ Pool Size를 늘려도, HPA로 파드를 확장해도 │ │ Redis 내부에서는 O(n) 순차 처리 │ │ │ └────────────────────────────────────────────────────────────────┘한계 설명 영향 Redis 싱글 스레드 모든 명령어가 순차 처리됨 연결 수 증가해도 처리량 한계 매 요청 Redis 호출 모든 JWT 검증 시 네트워크 I/O RTT 누적 (1-10ms/요청) 수평 확장 시 비용 Pod당 Redis 연결 200개 × Pod 수 Redis 연결 폭증 Redis 장애 전파 Redis 다운 시 전체 인증 실패 가용성 저하 해결책: Local Cache + MQ Event Broadcasting
핵심 아이디어: 읽기 경로에서 Redis를 완전히 제거한다.
┌─────────────────────────────────────────────────────────────────┐ │ 쓰기 경로 (로그아웃 시) │ └─────────────────────────────────────────────────────────────────┘ auth-api ──▶ Redis (저장) ──▶ MQ (fanout) ──┬──▶ ext-authz-1 (로컬 캐시) ├──▶ ext-authz-2 (로컬 캐시) └──▶ ext-authz-3 (로컬 캐시) ┌─────────────────────────────────────────────────────────────────┐ │ 읽기 경로 (매 API 요청) │ └─────────────────────────────────────────────────────────────────┘ Request ──▶ ext-authz-1 ──▶ 로컬 캐시 조회 (O(1), <0.1ms) ↓ Redis 조회 없음!동기화 방식: Polling이 아닌 Event-Driven Push
중요: 매번 Redis에서 동기화하는 게 아니다.
시점 Redis 조회 MQ 사용 파드 시작 ✅ 한 번 (초기 로딩) ❌ 로그아웃 발생 ❌ ✅ 이벤트 수신 토큰 검증 (매 요청) ❌ 없음! ❌ 


구현 예시 (Go)
// 로컬 캐시 구조체 type LocalBlacklistCache struct { mu sync.RWMutex items map[string]int64 // jti -> expireAt unix timestamp } func (c *LocalBlacklistCache) Contains(jti string) bool { c.mu.RLock() defer c.mu.RUnlock() expireAt, exists := c.items[jti] return exists && time.Now().Unix() < expireAt } func (c *LocalBlacklistCache) Add(jti string, expireAt int64) { c.mu.Lock() c.items[jti] = expireAt c.mu.Unlock() }// 검증 로직 변경 // Before: 매번 Redis 조회 func (s *AuthzService) IsBlacklisted(jti string) bool { return s.redis.Exists(ctx, "blacklist:"+jti).Val() > 0 // 네트워크 I/O } // After: 로컬 메모리 조회 func (s *AuthzService) IsBlacklisted(jti string) bool { return s.localCache.Contains(jti) // O(1), <0.1ms }// 파드 시작 시 초기화 func (s *AuthzService) Initialize() error { // 1. Redis에서 전체 블랙리스트 로드 (1회성) keys, _ := s.redis.Keys(ctx, "blacklist:*").Result() for _, key := range keys { ttl, _ := s.redis.TTL(ctx, key).Result() jti := strings.TrimPrefix(key, "blacklist:") s.localCache.Add(jti, time.Now().Add(ttl).Unix()) } // 2. MQ 구독 시작 (이후 이벤트 기반 동기화) go s.subscribeBlacklistEvents() return nil } // MQ 이벤트 수신 func (s *AuthzService) subscribeBlacklistEvents() { ch, _ := s.mq.Consume("blacklist.events") for msg := range ch { var event BlacklistEvent json.Unmarshal(msg.Body, &event) s.localCache.Add(event.JTI, event.ExpireAt) } }RabbitMQ 토폴로지
Exchange: name: auth.blacklist.events type: fanout # 모든 ext-authz 파드에 브로드캐스트 Queues: - name: blacklist.sync.{pod-id} # 각 파드별 exclusive queue autoDelete: true # 파드 종료 시 자동 삭제 exclusive: true # 단일 consumer만 허용⚠️ Celery Task Queue와의 차이
중요: 이 패턴은 Celery Task Queue가 아닌 RabbitMQ Fanout Exchange를 사용한다.
┌─────────────────────────────────────────────────────────────────────────────┐ │ Celery Task Queue (AI 파이프라인용) │ │ ───────────────────────────────── │ │ │ │ scan-api ──▶ Exchange (direct) ──▶ scan.vision Queue ──▶ Worker-AI-1 │ │ (혼자 처리!) │ │ │ │ • "이 이미지 분류해줘" (Command) │ │ • Competing Consumers: 하나의 Worker만 처리 │ │ • 처리 완료 후 메시지 삭제 │ │ │ ├─────────────────────────────────────────────────────────────────────────────┤ │ Event Broadcasting (ext-authz 동기화용) │ │ ─────────────────────────────────────── │ │ │ │ auth-api ──▶ Exchange (fanout) ──┬──▶ Queue-1 ──▶ ext-authz-1 (로컬캐시) │ │ ├──▶ Queue-2 ──▶ ext-authz-2 (로컬캐시) │ │ └──▶ Queue-3 ──▶ ext-authz-3 (로컬캐시) │ │ (모두 처리!) │ │ │ │ • "JTI xxx가 블랙리스트에 추가됐다" (Event) │ │ • Fanout: 모든 구독자가 동일 이벤트 수신 │ │ • 각자 로컬 캐시 업데이트 │ │ │ └─────────────────────────────────────────────────────────────────────────────┘구분 Celery Task Queue Event Broadcasting 목적 작업 분산 처리 상태 동기화 Exchange 타입 Direct/Topic Fanout 처리 주체 하나의 Worker 모든 인스턴스 메시지 의미 "해라" (Command) "일어났다" (Event) 라이브러리 Celery (Python) amqp (Go 네이티브) 같은 RabbitMQ 클러스터를 사용하지만 다른 패턴이다. Celery로 구현하면 하나의 Worker만 이벤트를 수신하여 나머지 Pod들의 로컬 캐시가 동기화되지 않는다.
왜 이게 가능한가?
블랙리스트는 이벤트 소스가 단일하다:
동작 소스 빈도 추가 auth-api 로그아웃만 드묾 (사용자 행동) 삭제 TTL 만료 (자동) - 조회 모든 API 요청 매우 빈번 읽기:쓰기 비율 = 99.99%:0.01% → 쓰기 이벤트만 전파하면 완벽한 동기화.
Eventual Consistency 허용 가능한 이유
- 보안 위험 낮음: 토큰 탈취 후 1-2초 내에 사용하는 공격은 극히 드묾
- TTL 기반: 만료된 토큰은 JWT 서명 검증에서 이미 거부됨
- 이중 방어: Access Token 만료 시간 단축 (15분)
메모리 사용량 예측
항목 값 계산 동시 활성 세션 ~10,000 JTI 크기 ~36 bytes UUID expireAt 크기 8 bytes int64 총 메모리 ~440KB 10,000 × 44 bytes → 무시할 수 있는 수준. 파드당 메모리 제한(128Mi) 대비 0.3%.
예상 개선 효과
지표 현재 (Redis 직접) MQ + 로컬 캐시 개선율 읽기 latency 1-10ms (네트워크) <0.1ms (메모리) 100x Redis RPS = API 전체 RPS ≈ 0 (쓰기만) ∞ Redis 조회 비율 100% 0% 100% 예상 RPS 한계 1,100 (Redis 제한) 10,000+ (CPU 제한) 10x 수평 확장성 Redis가 SPOF 파드별 독립 무한 Fallback 전략
MQ 장애 시에도 서비스 지속:
func (s *AuthzService) IsBlacklisted(jti string) bool { // 1. 로컬 캐시 우선 if s.localCache.Contains(jti) { return true } // 2. MQ 연결 끊김 시 Redis fallback if s.mqDisconnected.Load() { return s.redis.Exists(ctx, "blacklist:"+jti).Val() > 0 } return false }우선순위: P1
- 변경 이유: Redis 싱글 커맨드 큐 병목 해소가 RPS 증가의 핵심
- 의존성: RabbitMQ 인프라 구축 후 바로 적용 가능
- 트리거: 5,000+ RPS 목표 시 필수
- 리스크: 낮음 (Redis를 fallback으로 유지)
2. gRPC 통신 전체 맵 및 MQ 전환 분석
현재 gRPC 통신 흐름
┌─────────────────────────────────────────────────────────────────────────────┐ │ gRPC 내부 통신 전체 맵 │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────┐ GetCharacterReward ┌───────────┐ GrantCharacter │ │ │ scan │ ─────────────────────────▶│ character │ ────────────────────▶ │ │ └─────────┘ (동기, 응답 필요) └───────────┘ (best-effort) │ │ │ │ │ │ │ │ ▼ │ │ ┌────────┐ │ │ │ my │ │ │ └────────┘ │ │ │ │ │ │ GetDefaultCharacter │ │ │ (신규 사용자 시) │ │ ▼ │ │ ┌───────────┐ │ │ │ character │ │ │ └───────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘호출 방향 트리거 현재 상태 MQ 전환 GetCharacterReward scan → character 스캔 완료 후 동기 gRPC ❌ 응답 필요 GrantCharacter character → my 캐릭터 발급 시 best-effort + CB ⚠️ 현재 유지 GetDefaultCharacter my → character 신규 사용자 gRPC + CB ✅ P3 (아래) scan → character (GetCharacterReward): 유지
# scan/services/scan.py async def _call_character_reward_api(self, reward_request): stub = await get_character_stub() response = await stub.GetCharacterReward(grpc_req, timeout=...) return CharacterRewardResponse(...) # ← 사용자 응답에 포함유지 이유:
- 사용자에게 리워드 결과를 즉시 반환해야 함
- MQ로 전환하면 Polling/WebSocket 필요 → 복잡도 증가
대안 (향후):
- Scan 전체가 MQ 비동기 전환 시, Celery 체인 내에서 처리
scan_task.apply_async() → classify → evaluate_reward → save일괄 처리
my → character (GetDefaultCharacter): 로컬 캐시 가능
# my/services/characters.py async def _ensure_default_character(self, user_id: UUID) -> bool: client = get_character_client() default_char = await client.get_default_character() # ← 매번 gRPC 호출 ...문제점:
- 신규 사용자마다 기본 캐릭터 정보를 gRPC로 조회
- Circuit Breaker로 장애 격리되지만, 불필요한 네트워크 호출
개선안: 기본 캐릭터 정보(1개)를 로컬 캐시에 보관
# my/core/local_cache.py _default_character: DefaultCharacterInfo | None = None async def get_default_character() -> DefaultCharacterInfo | None: global _default_character if _default_character is not None: return _default_character # 로컬 캐시 hit # Fallback: gRPC 호출 client = get_character_client() char = await client.get_default_character() _default_character = char return char # MQ 이벤트 수신 시 갱신 async def on_default_character_updated(event): global _default_character _default_character = DefaultCharacterInfo(**event.payload)우선순위: P3 (현재 Circuit Breaker로 충분히 안정적)
3. Character 카탈로그: Local Cache + MQ 동기화
현재 상태
character 서비스는 캐릭터 카탈로그를 Redis에 캐싱하고 있다 (Cache-aside 패턴).
# character/services/character.py async def catalog(self) -> list[CharacterProfile]: # 1. 캐시 조회 cached = await get_cached(CATALOG_KEY) if cached is not None: return [CharacterProfile(**item) for item in cached] # Cache hit # 2. DB 조회 (Cache miss) characters = await self.character_repo.list_all() profiles = [self._to_profile(character) for character in characters] # 3. 캐시 저장 (TTL 5분) await set_cached(CATALOG_KEY, [p.model_dump() for p in profiles]) return profilesext-authz와의 유사점
특성 ext-authz 블랙리스트 character 카탈로그 읽기:쓰기 비율 99.99%:0.01% 99.9%:0.1% 쓰기 빈도 로그아웃 시 캐릭터 추가/수정 (admin) 데이터 크기 ~10KB (10,000 JTI) ~5KB (20개 캐릭터) 현재 캐싱 Redis 직접 조회 Redis Cache-aside TTL 토큰 만료 시간 5분 → 동일한 패턴 적용 가능: 로컬 캐시 + MQ 이벤트 브로드캐스트
현재 아키텍처의 한계
┌────────────────────────────────────────────────────────────────┐ │ 현재: TTL 기반 캐시 갱신 │ ├────────────────────────────────────────────────────────────────┤ │ │ │ character-1 ──┬──▶ Redis GET character:catalog │ │ character-2 ──┤ (TTL 5분마다 Cache miss → DB 조회) │ │ character-3 ──┘ │ │ │ │ 문제점: │ │ 1. TTL 만료 시 동시 요청 → Thundering Herd (DB 과부하) │ │ 2. 캐릭터 수정 후 최대 5분간 구버전 노출 │ │ 3. Redis 장애 시 모든 파드가 DB 직접 조회 │ │ │ └────────────────────────────────────────────────────────────────┘MQ 기반 개선

구현 예시 (Python)
# character/core/local_cache.py from threading import RLock from typing import Optional import asyncio class LocalCatalogCache: def __init__(self): self._lock = RLock() self._catalog: list[dict] | None = None def get(self) -> list[dict] | None: with self._lock: return self._catalog def set(self, catalog: list[dict]) -> None: with self._lock: self._catalog = catalog _local_cache = LocalCatalogCache() # character/services/character.py 수정 async def catalog(self) -> list[CharacterProfile]: # 1. 로컬 캐시 우선 (Redis 조회 없음) cached = _local_cache.get() if cached is not None: return [CharacterProfile(**item) for item in cached] # 2. Fallback: DB 조회 (초기 로딩 또는 캐시 미스) characters = await self.character_repo.list_all() profiles = [self._to_profile(character) for character in characters] _local_cache.set([p.model_dump() for p in profiles]) return profiles # MQ Consumer (lifespan에서 시작) async def subscribe_catalog_events(mq_channel): async for message in mq_channel.consume("catalog.updates"): catalog_data = json.loads(message.body) _local_cache.set(catalog_data) message.ack()예상 효과
지표 현재 (Redis Cache-aside) MQ + 로컬 캐시 조회 latency 1-5ms (Redis RTT) <0.1ms Redis 부하 TTL 만료마다 조회 0 캐시 일관성 TTL 기반 (최대 5분 지연) 즉시 동기화 Thundering Herd 발생 가능 완전 제거 우선순위: P3 (Low)
- 이유: 현재 Redis 캐시로 충분히 동작, 카탈로그 변경 빈도 낮음
- 트리거: 캐릭터 추가/수정 빈도 증가 시, 또는 Redis 부하 이슈 시
- 의존성: ext-authz MQ 구축 후 동일 인프라 재활용 가능
4. Image 후처리 파이프라인 (향후, P3)
현재 상태
현재 이미지 서비스는 Presigned URL 발급만 담당하며, 후처리는 없다.
# image/services/image.py async def create_upload_url(self, channel, request, uploader_id): key = self._build_object_key(channel.value, request.filename) upload_url = self._s3_client.generate_presigned_url(...) # ~50ms return ImageUploadResponse(upload_url=upload_url, cdn_url=cdn_url)향후 확장 시 MQ 필요
현재 CDN(CloudFront)에서 이미지 캐싱을 수행 중이다. 서비스 고도화 시 다음 후처리 작업이 추가될 수 있다:
작업 예상 시간 MQ 적용 비고 썸네일 생성 1~3초 ✅ PIL/Pillow 이미지 리사이즈 1~2초 ✅ 다양한 해상도 EXIF 메타데이터 추출 <1초 ⚠️ 선택적 동기 처리 가능 AI 이미지 분석 5~15초 ✅ 중복 이미지 탐지 등 CDN 캐시 Invalidation <1초 ✅ 이미지 업데이트 시 우선순위: P3 (Low)
- 이유: 현재 후처리 요구사항 없음
- 트리거: 서비스 고도화 시 재검토
5. Location 데이터 색인 갱신 (향후, P3)
현재 상태
위치 데이터는 정적 데이터셋으로, 초기 Job으로 색인된다.
# location/jobs/import_*.py # 초기 구동 시 CSV → PostGIS 색인향후 확장 시 MQ 필요
시나리오 MQ 필요성 비고 사용자 위치 제보 ✅ 비동기 검증 + 색인 스팸/악성 필터링 외부 API 동기화 ✅ 배치 처리 Rate Limit 대응 우선순위: P3 (Low)
- 이유: 현재 정적 데이터로 운영
- 트리거: 실시간 데이터 요구사항 발생 시
적용 우선순위 종합
의사결정 프레임워크
MQ 적용 우선순위는 다음 기준으로 결정했다:
기준 가중치 설명 사용자 영향 40% 직접적인 UX 개선 여부 시스템 안정성 30% 장애 격리, 재시도, 정합성 구현 복잡도 20% 기존 코드 변경량, 리스크 의존성 10% 선행 작업 필요 여부 우선순위 매트릭스 (RabbitMQ 중심)
┌─────────────────────────────────────────────────────────────────┐ │ RabbitMQ 기반 우선순위 매트릭스 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 영향도(Impact) │ │ ▲ │ │ │ ┌─────────────────────────────────────────────────┐ │ │ 높음│ │ P0: Chat/Scan AI 파이프라인 [RabbitMQ-Command] │ │ │ │ │ "이 이미지를 분류해" → 한 번 처리하고 끝, Command Queue │ │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ │ │ ┌─────────────────┐ │ │ 중간│ │ P1: ext-authz │ [RabbitMQ-Fanout] │ │ │ │ Local Cache + │ 실시간 브로드캐스트 │ │ │ │ MQ Broadcasting │ (Redis 기반) │ │ │ └─────────────────┘ │ │ │ │ │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ 낮음│ │ P3: 카탈로그 │ │ P3: Image/Loc │ │ │ │ │ Local Cache │ │ 향후 요구사항 │ │ │ │ └─────────────────┘ └─────────────────┘ │ │ └───────────────────────────────────────────────────▶ │ │ 구현 복잡도 시간축 │ └─────────────────────────────────────────────────────────────────┘RabbitMQ 활용 패턴 요약
작업 메시지 유형 Exchange 타입 우선순위 이유 AI 파이프라인 ClassifyImage(Command)Direct P0 한 번 처리, 응답 필요 리워드 지급 GrantReward(Command)Direct P0 Fire-and-forget, DLQ ext-authz 동기화 BlacklistAdded(Broadcast)Fanout P1 모든 인스턴스에 전파 카탈로그 동기화 CatalogUpdated(Broadcast)Fanout P3 관리자 수정 시 gRPC 통신 전환 전략
호출 방향 현재 전환 우선순위 이유 GetCharacterReward scan → character gRPC 유지 - 응답 필요 GrantCharacter character → my gRPC (best-effort) 현재 유지 - CB로 안정적 GetDefaultCharacter my → character gRPC 로컬캐시 P3 변경 드묾 Local Cache + MQ 패턴 적용 대상
공통 조건: 읽기:쓰기 비율이 극단적으로 비대칭 (99%+:1%-)
대상 데이터 Exchange 타입 우선순위 이유 ext-authz JWT 블랙리스트 Fanout P1 RPS 병목 해소 character 캐릭터 카탈로그 Fanout P3 현재 안정적 my 기본 캐릭터 정보 Fanout P3 변경 드묾 로드맵
순서 작업 예상 기간 예상 효과 1 RabbitMQ 인프라 구축 (Operator) 1주 기반 인프라 ✅ 완료 2 AI 파이프라인 비동기화 2주 응답 10초→100ms 3 ext-authz Local Cache + Fanout 1주 RPS 10x 향상 향후 카탈로그/기본캐릭터 로컬캐시 - 요구사항 발생 시
사례 및 베스트 프랙티스
1. Netflix: 마이크로서비스 간 비동기 통신
Netflix는 1,000개 이상의 마이크로서비스를 운영하며, 서비스 간 통신을 이벤트 기반으로 처리한다. 핵심 원칙:
- 서비스 독립성: 각 서비스는 자체 데이터 저장소를 보유
- 이벤트 발행/구독: 상태 변경 시 이벤트 발행, 관심 있는 서비스만 구독
- Eventually Consistent: 즉각적 정합성보다 가용성 우선
2. Uber: Domain-Oriented Microservices Architecture (DOMA)
Uber는 수천 개의 마이크로서비스를 도메인 기반으로 그룹화하고, 도메인 간 통신을 MQ로 처리한다:
"Microservices without proper boundaries become a distributed monolith."
- 도메인 경계: 강한 결합은 도메인 내부로 제한
- 비동기 통신: 도메인 간 통신은 이벤트 기반
- Saga Pattern: 분산 트랜잭션 대신 보상 트랜잭션
3. 올리브영: RabbitMQ 기반 쿠폰 발급 시스템
올리브영은 기존 Redis Worker에서 RabbitMQ로 전환하여 쿠폰 발급 시스템을 개선했다:
- 문제: 트래픽 급증 시 Redis Worker 병목
- 해결: RabbitMQ + Celery로 비동기 처리
- 효과: 안정적인 메시지 보장, 확장성 확보
참고: 올리브영 테크 블로그
4. Amazon MQ: Celery + RabbitMQ 최적화 가이드
AWS에서 권장하는 RabbitMQ + Celery 최적화 설정:
# Celery 5.5+ 권장 설정 app = Celery('tasks') app.conf.update( # Quorum Queue 사용 (Classic 대신) task_default_queue_type='quorum', # Broker 전송 옵션 broker_transport_options={ 'confirm_publish': True, # 발행 확인 }, # Worker 설정 worker_prefetch_multiplier=1, # 공정한 분배 task_acks_late=True, # 작업 완료 후 ACK task_reject_on_worker_lost=True, )
핵심 패턴: Local Cache + MQ Event Broadcasting
읽기:쓰기 비율이 극단적으로 비대칭인 데이터에 대해:
┌─────────────────────────────────────────────────────────────────┐ │ Before: 매번 Redis/DB 조회 │ │ ────────────────────────── │ │ Request → Service → Redis/DB → Response │ │ ↑ │ │ 네트워크 I/O, 순차 처리 병목 │ ├─────────────────────────────────────────────────────────────────┤ │ After: 로컬 캐시 + MQ 이벤트 동기화 │ │ ──────────────────────────────── │ │ [쓰기] Source → Redis/DB → MQ (fanout) → 모든 파드 로컬 캐시 │ │ [읽기] Request → 로컬 캐시 조회 (O(1), <0.1ms) → Response │ │ ↑ │ │ 네트워크 I/O 제거, 무한 확장성 │ └─────────────────────────────────────────────────────────────────┘RabbitMQ 활용 패턴
┌─────────────────────────────────────────────────────────────────┐ │ RabbitMQ Exchange 패턴 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ Direct Exchange = Command (일감) Fanout Exchange = 브로드캐스트│ │ ───────────────────────────────── ───────────────────────────│ │ │ │ • "이 이미지를 분류해" • "블랙리스트가 추가됐다" │ │ • "리워드를 지급해" • "카탈로그가 수정됐다" │ │ • 하나의 Worker가 처리 • 모든 인스턴스에 전파 │ │ • 처리 후 삭제 • 로컬 캐시 갱신 │ │ │ │ AI 파이프라인 (P0) ext-authz 동기화 (P1) │ │ │ └─────────────────────────────────────────────────────────────────┘핵심 우선순위
우선순위 작업 Exchange 타입 P0 AI 파이프라인 비동기화 Direct P1 ext-authz Local Cache + Broadcasting Fanout P3 카탈로그/기본캐릭터 로컬캐시 Fanout ext-authz 개선 핵심 (RabbitMQ Fanout)
문제 원인 해결책 Redis 싱글 스레드 병목 모든 요청이 Redis 커맨드 큐에 적재 읽기 경로에서 Redis 완전 제거 Pool Size 늘려도 한계 Redis 내부는 여전히 순차 처리 로컬 메모리 조회 (O(1)) 이벤트 동기화 필요 블랙리스트 추가는 드묾 RabbitMQ Fanout으로 브로드캐스트 단계별로 적용하여 시스템 복잡도를 관리하며, RabbitMQ 기반의 비동기 아키텍처로 확장성과 안정성을 확보하는 것이 목표다.
Reference
Foundation 문서 (이론적 기반)
- 10. DDD Aggregate - 트랜잭션 경계
- 06. Life Beyond Distributed Transactions - Eventual Consistency
References
- Netflix Tech Blog - Microservices
- Uber Engineering - DOMA
- 올리브영 테크 블로그 - RabbitMQ 도입
- Amazon MQ - Celery Best Practices
GitHub
Service
'이코에코(Eco²) > Message Queue' 카테고리의 다른 글
이코에코(Eco²) Message Queue #5: Celery Chain + Celery Events (1) (0) 2025.12.23 이코에코(Eco²) Message Queue #4: SSE vs Webhook vs Websocket (0) 2025.12.22 이코에코(Eco²) Message Queue #3: Scan 비동기 파이프라인 로드맵 (0) 2025.12.22 이코에코(Eco²) Message Queue #2: RabbitMQ 구축 (0) 2025.12.22 이코에코(Eco²) Message Queue #0: RabbitMQ + Celery 아키텍처 설계 (1) 2025.12.21