ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 이코에코(Eco²) Event Driven Architecture 전환 로드맵
    이코에코(Eco²) 2025. 12. 17. 12:15

    개요

    지난 포스팅에서 Observability 보강과 Message Queue 도입 계획을 언급했었다.
    이코에코(Eco²) 백엔드는 현재 Istio Service Mesh 기반의 Kubernetes 클러스터 위에서 8개 도메인(auth, character, chat, scan, my, location, image, ext-authz)으로 분리되어 운영 중이다.
    서비스 간 통신은 gRPC와 Envoy Proxy를 통해 이루어지며, 기본적인 메트릭 수집과 대시보드는 구축되어 있다.
    현재 아키텍처의 상태에서 EDA로의 안정적인 전환을 위한 로드맵을 서술한다. (이거 하려고 여기까지 진행했다.)


    현재 아키텍처 상태 진단

    현재 이코에코 시스템 구조도. 외부 엔드포인트는 HTTP 1.1, 서버 간 내부 통신은 gRPC로 구성됐다.

    현재 클러스터 노드 현황

    노드 인스턴스 타입 vCPU Memory Storage 용도
    k8s-master t3.xlarge 4 16GB 80GB Control Plane + Prometheus
    k8s-api-auth t3.small 2 2GB 20GB 인증/인가
    k8s-api-my t3.small 2 2GB 20GB 마이페이지
    k8s-api-scan t3.medium 2 4GB 30GB 스캔 (GPT 5.1, Vision Classification)
    k8s-api-character t3.small 2 2GB 20GB 캐릭터/미션
    k8s-api-location t3.small 2 2GB 20GB 위치/지도
    k8s-api-image t3.small 2 2GB 20GB 이미지
    k8s-api-chat t3.medium 2 4GB 30GB 챗봇 (GPT 5.1, Chatot)
    k8s-worker-storage (Pending) t3.medium 2 4GB 40GB I/O 작업
    k8s-worker-ai (Pending) t3.medium 2 4GB 40GB AI 처리
    k8s-rabbitmq (Pending) t3.medium 2 4GB 40GB 메시지 큐
    k8s-postgresql t3.large 2 8GB 80GB 데이터베이스
    k8s-redis t3.medium 2 4GB 30GB 캐시
    k8s-monitoring t3.large 2 8GB 60GB Prometheus + Grafana
    k8s-logging t3.large 2 8GB 100GB EFK Stack (작성일 기준 개발 중, 현재는 개발 완료)
    k8s-ingress-gateway t3.medium 2 4GB 20GB Istio Bridge Gateway

    EDA 기반 구축 완료

    사안 진척도 상세
    도메인 분리 ✅ 완료 7개 서비스 + 1개 JWT 검증 엔진 분리를 통해 이벤트 발행/구독의 경계(Bounded Context) 명확화
    gRPC 마이그레이션 ✅ 완료 내부 통신 프로토콜 표준화 및 클라이언트 통신용 엔드포인트(HTTP 1.1)를 서버 로직과 이원화
    Istio Service Mesh ✅ 완료 mTLS 보안 통신, 트래픽 제어, Envoy Sidecar를 통한 메트릭 수집 기반을 도메인 서버에서 분리
    Observability (Metric) ✅ 완료 Prometheus + Grafana를 통한 리소스 및 애플리케이션 상태 모니터링

    ⏳ 추가 필요 구성

    RabbitMQ 노드        → 미구성 (k8s-rabbitmq 노드 존재)
    Kafka/CDC 노드       → 신규 프로비저닝 필요
    중앙 로깅 (EFK -> ELK (w.Fluent bit))      → 프로비저닝 완료, 로깅 파이프라인 개발 중
    물리적 DB 분리       → 현재 통합 DB 사용 중, 도메인별 분리 필요
    Event Sourcing       → 도메인별 이벤트 저장소 설계 필요

    위 요소를 일시에 도입할 경우 서버가 다운될 위험이 있다.
    따라서 의존성을 고려한 단계별 도입 전략을 수립하였다.


    전환 로드맵

    안정적인 전환을 위해 의존성에 따라 다음 순서로 진행한다.

    Observability 강화  →  MQ (Async)  →  CDC (Consistency)  → EDA (Architecture)

    단계별 수립 근거

    1. Observability 선행: 분산 환경에서 비동기 메시지 흐름을 추적(Tracing)할 수 없는 상태에서의 MQ 도입은 디버깅을 불가능하게 만든다.
    2. MQ 도입: 장기 실행 작업(Long-running Task)을 비동기로 분리하여 API 응답 속도를 개선한다.
    3. CDC 도입: 비동기 처리에 따른 데이터 정합성(이중 쓰기 문제)을 해결하기 위해 Transaction Log 기반의 이벤트 발행을 보장한다.
    4. EDA 완성: Saga Pattern과 CQRS를 적용하여 도메인 간 결합도를 낮추고 유연한 아키텍처를 완성한다.

    1단계: Observability 강화 (Logging + Tracing)

    분산 트랜잭션의 가시성 확보

    현재 메트릭 수집은 가능하나, 중앙화된 로깅 시스템의 부재로 인해 요청의 구체적인 흐름을 파악하기 어렵다.
    특히 MSA 환경에서는 단일 요청이 여러 서비스와 Pod를 거치기 때문에, 로그 파편화 문제는 치명적이다.
    (이제까지는 kubectl logs로 파드를 직접 조회하거나, describe, ArgoCD UI에 의존했다.)
    Trace Context(Trace ID) 기반의 분산 추적 환경 구축이 필수적이다.
    핵심 목표

    • gRPC 요청, Celery Task, CDC 이벤트 등 모든 트랜잭션 단계에 동일한 trace_id를 전파한다.
    • 단일 ID 검색만으로 HTTP 요청부터 비동기 작업 처리 결과까지의 전체 흐름을 역추적할 수 있어야 한다.

    2단계: MQ (RabbitMQ + Celery)

    AI Pipeline 구조

    현재 AI 파이프라인은 GPT API 호출 기반으로 동작한다.

    [AI Scan Pipeline]
    Image → CDN_url → Vision Model (GPT) → Rule-based Retrieval → Answer Model (GPT)
                            │                                            │
                            ▼                                            ▼
                      classification                              disposal rules
                            │                                            │
                            └──────────────► 보상 여부 판단 ◄────────────┘
                                                  │
                                        YES ──────┼────── NO
                                                  ▼
                                          2레벨 분류라벨 매칭
                                                  │
                                        YES ──────┼────── NO
                                                  ▼
                                          캐릭터 발급 요청
    
    [AI Chat Pipeline]
    Text → Text Model (GPT) → Rule-based Retrieval → Answer Model (GPT) → 응답
                  │                                          │
                  ▼                                          ▼
            classification                            disposal rules

    RabbitMQ는 AI 파이프라인의 단계별 비동기 처리를 담당한다.

    Exchange/Queue 전략

    Exchange Queue 작업 내용 예상 시간 Worker
    scan.commands scan.vision Vision Model (GPT) 분류 5~15초 AI
    scan.commands scan.answer Answer Model (GPT) 응답 생성 3~10초 AI
    scan.commands scan.reward 보상 판단 + 캐릭터 발급 요청 1~3초 Storage
    chat.commands chat.classify Text Model (GPT) 분류 3~8초 AI
    chat.commands chat.answer Answer Model (GPT) 응답 생성 3~10초 AI
    my.commands my.profile 프로필 대량 업데이트 1~5초 Storage

    Pipeline 실행 흐름

    [Scan Pipeline - Chained Tasks]
    1. scan.vision   → Vision Model API 호출 → ScanClassified 이벤트
    2. scan.answer   → Rule-based Retrieval (동기) + Answer Model API 호출 → ScanAnswered 이벤트
    3. scan.reward   → 보상 여부 판단 → 2레벨 매칭 → 캐릭터 발급 요청 → RewardGranted 이벤트
    
    [Chat Pipeline - Chained Tasks]
    1. chat.classify → Text Model API 호출 → ChatClassified 이벤트
    2. chat.answer   → Rule-based Retrieval (동기) + Answer Model API 호출 → ChatAnswered 이벤트
    • Rule-based Retrieval: 로컬 JSON 기반 검색으로 지연 시간이 짧아 별도 Queue 없이 Answer Task 내에서 동기 처리.
    • Chained Tasks: Celery의 chain() 또는 이벤트 기반으로 단계별 순차 실행.

    안정성 확보 (DLQ)

    • GPT API 실패: Rate Limit, Timeout 등 외부 API 오류 시 Exponential Backoff로 3회 재시도.
    • 최종 실패: Dead Letter Queue(DLQ)로 격리하여 메시지 유실 방지 및 수동 재처리 지원.
    • 리소스: t3.small (2GB) 노드에 Quorum Queue를 구성하여 내구성 확보.

    3단계: CDC (Debezium → Kafka)]

    데이터 정합성 및 이중 쓰기 문제 해결

    MQ 도입 시 Dual Write Problem(DB 커밋은 성공했으나 메시지 발행은 실패하는 현상)이 발생할 수 있다. 이를 해결하기 위해 Outbox PatternCDC(Change Data Capture)를 도입한다.

    Outbox Pattern & CDC Flow

    각 AI Pipeline 단계에서 발생하는 이벤트를 Outbox 테이블에 기록한다.

    # Scan Pipeline - 단계별 이벤트 발행
    async with db.transaction():
        # Vision Model 완료 시
        await outbox_repo.insert({
            "event_type": "ScanClassified",
            "aggregate_id": scan_id,
            "payload": {"category": "plastic", "situation": "recyclable"}
        })
    
    async with db.transaction():
        # Answer Model 완료 시
        await outbox_repo.insert({
            "event_type": "ScanAnswered", 
            "aggregate_id": scan_id,
            "payload": {"disposal_rules": [...], "answer": "..."}
        })
    
    async with db.transaction():
        # 보상 지급 완료 시
        await outbox_repo.insert({
            "event_type": "RewardGranted",
            "aggregate_id": scan_id,
            "payload": {"character_id": "char_001", "reward_type": "character"}
        })

    CDC 이벤트 타입

    Domain Event Type 발생 시점 Saga Trigger
    Scan ScanClassified Vision Model 분류 완료 -
    Scan ScanAnswered Answer Model 응답 완료 -
    Scan RewardGranted 보상 판단 + 캐릭터 발급 완료 Character 서비스
    Chat ChatClassified Text Model 분류 완료 -
    Chat ChatAnswered Answer Model 응답 완료 -
    Character CharacterAcquired 캐릭터 획득 My 서비스
    My ProfileUpdated 프로필 업데이트 Notification
    1. 각 Pipeline 단계 완료 시 Outbox 테이블에 이벤트 기록 (트랜잭션 내).
    2. Debezium Connector가 WAL을 감지하여 Kafka로 발행.
    3. At-least-once 전송을 보장하며, Consumer는 Idempotency로 중복 처리.

    4단계: EDA (Saga + CQRS + Event Sourcing)

    성숙한 Event-Driven Architecture

    CDC를 통해 신뢰할 수 있는 이벤트 스트림이 확보되면, 본격적으로 EDA 패턴을 적용한다.

    Saga Choreography

    중앙 오케스트레이터 없이 이벤트 구독만으로 도메인 간 비즈니스 프로세스를 연결한다.

    [Scan Pipeline Event Chain]
    1. ScanClassified    → Scan Projector (ES 인덱싱)
    2. ScanAnswered      → Scan Projector (ES 인덱싱)
    3. RewardGranted     → Character 서비스 (캐릭터 발급)
    4. CharacterAcquired → My 서비스 (프로필 업데이트)
    5. ProfileUpdated    → Notification (푸시 알림)
    
    [Chat Pipeline Event Chain]
    1. ChatClassified    → Chat Projector (ES 인덱싱)
    2. ChatAnswered      → Chat Projector (대화 이력 저장)

    AI Pipeline의 각 단계에서 발생하는 이벤트가 독립적으로 처리되며, RewardGranted 이벤트만 Saga를 트리거하여 다음 도메인으로 전파된다. 서비스 간 직접적인 결합(Coupling)을 제거하여 독립적인 배포와 확장이 가능해진다.

    CQRS & Event Sourcing

    • Event Sourcing: 모든 도메인의 상태 변경 이력을 Event Store에 저장하여 데이터의 완전성을 보장한다. (Audit Log 역할 포함)
    • CQRS: 명령(Write)과 조회(Read) 모델을 분리한다.
      • Write: Event Store (Append-only)
      • Read: Kafka Consumer(Projector)가 이벤트를 구독하여 Redis나 Elasticsearch에 최적화된 조회 모델을 생성.

    Data Layer 변경: 도메인별 DB/Cache 물리적 분리

    EDA의 장점인 장애 격리와 독립적인 스케일링을 극대화하기 위해, 통합되어 있던 DB와 Cache 노드를 도메인별로 물리적으로 분리한다.

    [AS-IS] 통합 노드
    - k8s-postgresql (t3.large)
    - k8s-redis (t3.medium)
    
    [TO-BE] 도메인별 분리 (Micro Instances)
    - Domain DBs (t3.micro × 7): 각 도메인 전용 DB 및 Event Store
    - Domain Caches (t3.micro × 3): 도메인별 전용 캐시

    이를 통해 특정 도메인의 부하가 전체 시스템에 영향을 주는 것을 방지(Bulkhead Pattern)하며, 오히려 통합 노드 대비 비용 효율적인 구성이 가능하다.


    단계별 도입을 통해 온전한 형태의 EDA까지 전환하는 게 목표다.
    EDA는 가파른 학습곡선이 요구되는 아키텍처 패턴이다. Eventual Consistency, Idempotency, Saga, Outbox Pattern, CQRS 등 기존의 CRUD 기반 사고방식과는 다른 개념들을 이해해야 한다.
    구현 과정에서 마주치는 필수 지향점과 핵심 패턴들을 별도의 학습 자료로 정리하며 진행할 예정이다.

    댓글

ABOUT ME

🎓 부산대학교 정보컴퓨터공학과 학사: 2017.03 - 2023.08
☁️ Rakuten Symphony Jr. Cloud Engineer: 2024.12.09 - 2025.08.31
🏆 2025 AI 새싹톤 우수상 수상: 2025.10.30 - 2025.12.02
🌏 이코에코(Eco²) 백엔드/인프라 고도화 중: 2025.12 - Present

Designed by Mango