[자율에이전트 #2] 24시간 잠들지 않는 파이프라인: 이벤트 기반 비동기 에이전트 백엔드 설계

No Comments

Photo of author

By 데블

🧭 [기획 연재] [엔터프라이즈 AX] 단순 챗봇을 넘어 24시간 무중단 자율 에이전트 아키텍처 구축기 제 2편 / 총 3부작
이 포스팅은 [엔터프라이즈 AX] 단순 챗봇을 넘어 24시간 무중단 자율 에이전트 아키텍처 구축기 시리즈의 2번째 이야기입니다.

LLM의 추론 지연 시간은 본질적으로 비결정적(Non-deterministic)입니다. 단순 분류 태스크는 500ms 안에 끝나기도 하지만, 복잡한 사유 체인(Chain-of-Thought)과 다단계 도구 호출(Tool Calling)이 얽히면 단일 사이클이 30초를 훌쩍 넘깁니다. 이러한 비동기적 특성을 무시한 채 전통적인 동기식 HTTP 요청-응답(Request-Response) 모델 위에 자율 에이전트를 얹는 순간, 커넥션 풀 고갈과 스레드 병목은 피할 수 없는 현실이 됩니다.

챗봇이 사용자의 질문을 기다리는 ‘수동적 인터페이스’라면, 자율 에이전트는 정해진 비즈니스 목표를 달성하기 위해 밤낮없이 스스로 이벤트를 감지하고 판단을 이어가는 ‘독립된 프로세스’입니다. 시스템의 본질이 달라졌다면 백엔드 아키텍처의 근간 역시 근본적으로 다시 정의되어야 합니다.

💡 바쁜 엔지니어를 위한 3줄 핵심 요약
* 동기식 HTTP 파이프라인 기반의 에이전트는 외부 API 지연과 커넥션 타임아웃으로 인해 24시간 무중단 자율 업무를 지탱할 수 없습니다. * 분산 메시지 브로커(Kafka)와 명시적 상태 머신(State Machine)을 결합하여, 장기 실행 태스크의 트랜잭션 내구성과 재시도 탄력성을 확보해야 합니다. * 분산 멱등성(Idempotency) 검증 계층과 적응형 서킷 브레이커를 구축해야 LLM 장애나 외부 레거시 지연 시 발생하는 연쇄 붕괴를 원천 차단할 수 있습니다.

1. 패러다임의 전복: ‘사람의 대기’에서 ‘일의 연속성’으로

우리가 구축하려는 엔터프라이즈 자율 에이전트의 핵심은 사람이 모니터 앞에 없어도 백그라운드에서 매출 지표를 수집하고, 이상 징후를 감지하며, 광고 소재를 교체하거나 이상 거래를 차단하는 데 있습니다. 즉, 사람이 작업을 지시하고 결과를 기다리는 것이 아니라 “일 자체가 24시간 멈추지 않고 흘러가는 구조”를 만드는 것입니다.

이를 실현하려면 무엇보다 에이전트의 작동 규칙이 감이나 모호한 자연어가 아닌, 시스템이 해석할 수 있는 엄밀한 조건식으로 격하되어야 합니다. “반응이 저조하면 광고를 끈다”가 아니라, “클릭률(CTR)이 기준치 1.2% 미만으로 2시간 이상 지속되고 전환비용(CPA)이 임계값을 초과하면 즉각 캠페인을 일시 중지하고 대체 소재를 프로비저닝한다”와 같이 코드로 강제된 제어 흐름이 존재해야 합니다.

이러한 장기 실행 프로세스(Long-Running Process)를 동기식 웹 서버(예: FastAPI, Spring Web MVC)의 단일 트랜잭션 안에서 루프로 돌리는 구조는 운영 환경에서 치명적인 재앙을 초래합니다. 외부 LLM 공급자의 간헐적인 레이턴시 스파이크나 타사 SaaS API의 일시적 장애가 발생하는 순간, 워커 프로세스 전체가 블로킹되며 파이프라인 전체가 마비되기 때문입니다.

결국 자율 에이전트의 생명력은 AI 모델 자체의 성능보다, 불확실성을 견뎌내도록 받쳐주는 AI 시대에도 흔들리지 않는 시스템 설계의 힘에서 비롯됩니다.


💡 함께 읽으면 좋은 추천 글

본문의 내용을 실제로 적용하기 전에 아래 글을 먼저 읽어보시면 훨씬 더 큰 효과와 깊이 있는 이해를 얻으실 수 있습니다.
👉 [[MCP 아키텍처 #2] 점진적 도구 탐색과 분산 HTTP 전송: 고성능 에이전트 백엔드 설계 패턴] ↗

2. 뼈아픈 오답노트: 무한 추론 루프와 연쇄 결제 실패의 회고

과거 프로덕션 환경에서 마케팅 자동화 에이전트를 처음 배포했을 당시, 필자는 뼈아픈 장애를 겪었습니다. 당시 아키텍처는 인메모리 큐와 비동기 태스크 러너에 지나치게 의존하고 있었습니다.

에이전트에게 주어진 미션은 주말 동안 실시간 광고 효율을 모니터링하고 예산을 자동 재분배하는 작업이었습니다. 그런데 특정 광고 채널 API에서 일시적인 502 Bad Gateway 에러가 반환되었고, 에이전트 내부의 LLM은 이를 ‘예산 소진에 따른 데이터 미수신’으로 잘못 추론했습니다. 에이전트는 결함을 메우기 위해 긴급 추가 예산 승인 툴을 연속으로 호출하는 자율 루프에 빠져들었습니다.

설상가상으로 메시지 브로커의 재시도(Retry) 설정에는 멱등성 키가 누락되어 있었습니다. 동일한 이벤트가 큐에서 컨슘될 때마다 에이전트는 중복된 태스크를 생성했고, 30분 만에 설정된 일일 예산 한도를 초과해 결제 트랜잭션이 폭주했습니다. 외부 LLM 제공사의 분당 토큰 제한(TPM)까지 동시에 걸리면서 모든 복구 스크립트조차 타임아웃으로 실패했습니다.

이 사고를 통해 얻은 기술적 교훈은 세 가지였습니다.

  1. 상태 머신의 명시적 격리 부재: 에이전트의 추론 단계와 외부 실행 단계(Tool Execution)가 분리되지 않아, LLM의 오판이 즉각 시스템 쓰기(Write) 작업으로 직결되었습니다.
  2. 분산 멱등성(Distributed Idempotency)의 결여: 네트워크 재시도나 컨슈머 리밸런싱이 발생했을 때, “동일한 작업은 단 한 번만 실행된다”는 보장이 없었습니다.
  3. 서킷 브레이커와 데드 레터 큐(DLQ)의 부재: 외부 종속성의 실패가 전체 파이프라인으로 전파되는 것을 차단하는 완충 장치가 전무했습니다.

이러한 실패를 겪은 후, 에이전트 파이프라인 전체를 이벤트 스트리밍 기반의 분산 비동기 아키텍처로 전면 재설계하게 되었습니다.


3. 이벤트 기반 비동기 에이전트 시스템 아키텍처

우리가 도달한 최종 아키텍처는 모든 에이전트의 행동을 ‘이벤트(Event)’로 정의하고, 실행 흐름을 ‘내구성 있는 상태 머신(Durable State Machine)’으로 통제하는 구조입니다.

비동기 에이전트 아키텍처 시스템 아키텍처 다이어그램
비동기 에이전트 아키텍처 시스템 아키텍처 다이어그램

이 아키텍처에서 에이전트는 단일 프로세스로 동작하지 않습니다. Kafka로 유입된 이벤트는 디스패처를 거쳐 상태 머신에 의해 태스크 단위로 쪼개집니다. 각 태스크의 상태 전이(IDLE -> THINKING -> TOOL_EXECUTION -> AWAITING_VERIFICATION -> COMPLETED)는 외부 분산 저장소에 영속화됩니다.

이러한 상태 기반 비동기 파이프라인 설계는 분산 환경에서 발생하는 데이터 불일치를 원천 차단하는 핵심 기법이며, 이는 대규모 분산 아키텍처 실무: 이론과 현실의 간극을 메우는 시스템 설계 풀코스에서 다루는 캐시 무효화 및 트랜잭션 격리 원칙과도 일맥상통합니다.


4. 프로덕션 레벨 실전 엔지니어링: 멱등성 제어와 서킷 브레이커

이벤트 기반 시스템에서 가장 중요한 것은 중복 실행 방지와 실패 격리입니다. 아래는 프로덕션 환경에서 실제 사용되는 비동기 에이전트 워커의 핵심 구현 코드입니다. Redis를 활용한 멱등성 잠금(Idempotency Lock)과 분산 서킷 브레이커를 결합하여 작성되었습니다.

PYTHON
import asyncio
import logging
from typing import Any, Dict, Optional
from pydantic import BaseModel, Field
import redis.asyncio as aioredis

logger = logging.getLogger("agent.resilience")

class AgentTaskEvent(BaseModel):
    event_id: str = Field(..., description="글로벌 고유 이벤트 식별자")
    task_type: str
    payload: Dict[str, Any]
    max_retries: int = 3
    retry_count: int = 0

class DistributedCircuitBreaker:
    """분산 환경을 위한 Redis 기반 슬라이딩 윈도우 서킷 브레이커"""
    def __init__(self, redis_client: aioredis.Redis, service_name: str, threshold: int = 5, recovery_timeout: int = 60):
        self.redis = redis_client
        self.key = f"circuit_breaker:{service_name}"
        self.threshold = threshold
        self.recovery_timeout = recovery_timeout

    async def can_execute(self) -> bool:
        state = await self.redis.get(f"{self.key}:state")
        if state == b"OPEN":
            return False
        return True

    async def record_failure(self):
        failures = await self.redis.incr(f"{self.key}:failures")
        if failures >= self.threshold:
            await self.redis.set(f"{self.key}:state", "OPEN", ex=self.recovery_timeout)
            logger.critical(f"서킷 브레이커 OPEN: 서비스 {self.key} 호출이 차단됩니다.")

    async def record_success(self):
        await self.redis.delete(f"{self.key}:failures")
        await self.redis.set(f"{self.key}:state", "CLOSED")

class ResilientAgentWorker:
    """멱등성과 탄력적 격리가 보장된 이벤트 기반 에

---

### 📚 [시리즈 연재 목차] 단순 챗봇을 넘어 24시간 무중단 자율 에이전트 아키텍처 구축기
본 포스팅은 총 3부작으로 기획된 테크 시리즈입니다. 아래 목차를 통해 전체 연재를 확인하실 수 있습니다.

- 제 1편: 프롬프트 복붙의 종말: 왜 당신의 사내 AI 도입은 매출로 이어지지 못했는가 ↗/) ↗
- ▶ **제 2편: 24시간 잠들지 않는 파이프라인: 이벤트 기반 비동기 에이전트 백엔드 설계 (현재 글)**
- 제 3편: 토큰 폭탄과 환각 루프의 악몽: 실전 프로덕션에서 건져 올린 가드레일 오답노트 ↗/) ↗

> 💡 **다음 편 예고**: 다음 에피소드 <토큰 폭탄과 환각 루프의 악몽: 실전 프로덕션에서 건져 올린 가드레일 오답노트>에서는 본 편에 이어 실전 프로덕션 최적화와 트레이드오프를 심층 분석합니다. 많은 기대 부탁드립니다!
🏃‍♂️ 이 글을 읽은 분들이 다음으로 선택한 추천 코스
☕ 함께 고민을 나누고 싶으신가요?

24시간 잠들지 않는 파이프라인: 이벤트 기반 비동기 에이전트 백엔드 설계 설계, 아키텍처 트레이드오프, 혹은 프로덕션 도입 과정에서 고민이 있으시다면 언제든 편하게 이야기 나눠요. 같은 엔지니어로서 함께 최적의 해법을 고민해 드리겠습니다.

댓글 남기기