포스트

2026년 5월, “확장 가능한 AI 앱”을 만드는 6가지 아키텍처 설계 패턴: MCP·Durable Execution·Observability까지

2026년 5월, “확장 가능한 AI 앱”을 만드는 6가지 아키텍처 설계 패턴: MCP·Durable Execution·Observability까지

들어가며

2026년 5월 기준 AI 애플리케이션(LLM/agent/RAG)은 “모델을 잘 고르는 문제”를 넘어 아키텍처가 곧 품질/비용/신뢰성을 결정하는 국면으로 넘어왔습니다. 특히 팀이 겪는 고통은 비슷합니다.

  • PoC는 되는데 프로덕션에서 실패 원인을 못 찾는다(retrieval이 틀렸는지, tool이 죽었는지, 모델이 헛소리했는지).
  • 기능이 늘수록 LLM 호출이 직렬로 늘어나 p95 latency·비용이 폭발한다.
  • “툴 연동”이 늘수록 권한/감사(audit)/정책 적용이 복잡해진다.
  • 장시간 작업(리포트 생성, 배치 분석, 워크플로우)이 타임아웃/중단/재시도 지옥으로 간다.

이 글은 “2026년 5월에 실제로 확장 가능한 구조”를 만들기 위한 패턴을 정리합니다. 핵심은 한 문장입니다:

Deterministic backbone(결정론적 오케스트레이션) + LLM은 ‘필요한 지점’에만 배치 + Durable Execution + Observability/평가가 기본값 (zylos.ai)

언제 쓰면 좋나

  • 고객-facing AI 기능(헬프데스크, 리서치 요약, 내부 지식 Q&A)처럼 오류 비용이 크고 트래픽이 있는 서비스
  • tool/API/DB 연동이 많은 “agentic workflow”
  • SLA(p95 latency, 오류율, 비용/요청)가 있는 팀

언제 쓰면 안 되나

  • 단발성 내부 스크립트(운영/감사 필요 없음)
  • “대화만 잘하면 되는” 단순 챗봇(툴/데이터 연동 적음)
    → 이 경우 오버엔지니어링이 됩니다.

🔧 핵심 개념

여기서는 “패턴”을 6개로 묶어, 내부 흐름(왜 이렇게 작동하는지)을 중심으로 설명합니다.

패턴 1) Deterministic Orchestrator + LLM Step(“LLM을 함수처럼”)

2026년 agent 시스템 설계에서 가장 중요한 변화는 LLM이 워크플로우를 ‘전부’ 지배하게 두지 말고, 상태 머신/그래프/DAG 같은 결정론적 실행 뼈대가 흐름을 통제하는 접근입니다. 연구/현업 가이드 모두 이 방향을 “승자 접근”으로 강조합니다. (zylos.ai)

  • Orchestrator(그래프/워크플로우 엔진): 분기, 재시도, 타임아웃, 승인(HITL), 상태 저장
  • LLM Step: 분류, 계획 생성, 요약, 스키마 변환, 후보 생성 등 “지능이 필요한 좁은 구간”

차이점(LLM-first vs backbone-first)

  • LLM-first(“ReAct로 계속 툴 호출”): 빠른 PoC, 그러나 호출이 늘면 비용/지연/디버깅이 망가짐
  • backbone-first: 설계 비용이 들지만, 운영/확장/컴플라이언스가 쉬워짐

패턴 2) Durable Execution(장시간·실패·재시도에 강한 실행 계층)

Agent/RAG는 외부 시스템(I/O)에 의존하므로 실패가 “정상 상태”입니다. 그래서 2026년에는 durable execution + checkpointing이 “있으면 좋은 것”이 아니라 기본 전제가 됐습니다. (docs.langchain.com)

  • 각 step 실행 후 상태를 durable store에 저장
  • 워커가 죽거나 타임아웃이 나도 중간부터 재개
  • 재시도 정책(백오프/서킷브레이커/보상 트랜잭션)을 워크플로우 레벨에서 통일

패턴 3) MCP Gateway(툴 연동 표준화 + 권한/정책의 중앙집중)

툴 연동이 늘수록 “각 agent가 각 API에 직접 붙는” 구조는 곧 무너집니다. 2026년에는 MCP(Model Context Protocol)가 사실상 툴/리소스/프롬프트를 표준 인터페이스로 노출하는 축으로 자리잡고 있습니다. (semantic.io)

핵심은 MCP Server(도구 제공) / MCP Client(앱) / Host로 역할을 분리하고, JSON-RPC 기반으로 capabilities(tools/resources/prompts)를 노출한다는 점입니다. (raftlabs.com)

프로덕션에서는 보통 “그냥 MCP”가 아니라:

  • MCP Gateway: allowlist, 정책(OPA 등), 감사로그, rate-limit, tool sandboxing, PII 마스킹
  • “tool을 보여주는 것”과 “자동 실행해도 되는 것”을 분리(중요)
    (실무에서 이 2단 allowlist가 사고를 줄입니다—커뮤니티에서도 반복 언급) (reddit.com)

패턴 4) Async/Background + Streaming(긴 작업을 기본으로 다루기)

리서치/분석/코드 수정 같은 agent 작업은 수십 초~수 분이 자연스럽습니다. OpenAI Responses API는 background mode로 장시간 작업을 안정적으로 처리하는 패턴을 공식화했고, WebSocket 기반으로 agentic workflow의 상호작용 성능도 개선하고 있습니다. (platform.openai.com)

구조적으로는:

  • request/response 동기 처리 → (대기/타임아웃/재시도 어려움)
  • job 기반 비동기(enqueue → status poll/webhook → 결과 저장)로 전환

패턴 5) Guardrails를 “워크플로우 경계”가 아니라 “툴 호출마다” 건다

2025~2026의 중요한 교훈: 최종 답변만 검사하면 늦습니다. 실제 사고는 “툴 호출”에서 터집니다(권한 과다, 잘못된 파라미터, 데이터 유출 등). 그래서 Agents SDK는 tool guardrails(툴 실행 전/후 검증)를 별도 개념으로 강조합니다. (openai.github.io)

패턴 6) Observability + Evaluation을 CI/CD에 연결(“LLM readiness”)

2026년의 “관찰 가능성”은 로그 몇 줄이 아니라,

  • retrieval hit rate
  • groundedness / policy compliance
  • cost / p95 latency
  • workflow success rate
    같은 지표를 트레이스로 연결하고, 배포 파이프라인에서 quality gate로 쓰는 방향으로 진화 중입니다. (arxiv.org)

💻 실전 코드

시나리오: “사내 Incident 요약/원인 추정” 기능을 만든다고 가정합니다.

  • 입력: incident id
  • 흐름: 1) DB/로그 시스템에서 데이터 가져오기(MCP tool) 2) LLM로 “요약 + 원인 후보 + 다음 액션” 생성 3) 결과 저장(다시 MCP tool)
  • 요구: 확장성(비동기), 툴 호출마다 guardrail, 추적(Trace ID), 실패 재시도 기반

아래는 OpenAI Responses API의 background mode를 이용해 “job 형태”로 실행하는 최소 실전 골격입니다. (MCP Gateway는 HTTP endpoint로 가정해 붙입니다.)

1) 초기 셋업

1
2
3
4
5
python -m venv .venv
source .venv/bin/activate
pip install openai fastapi uvicorn httpx pydantic
export OPENAI_API_KEY="..."
export MCP_GATEWAY_URL="http://localhost:9000"   # 사내 MCP Gateway

2) FastAPI: 비동기 Job 실행 + MCP Gateway 연동

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# app.py
import os, json
from typing import Any, Dict, Optional
import httpx
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

from openai import OpenAI

app = FastAPI()
client = OpenAI()

MCP_GATEWAY_URL = os.environ["MCP_GATEWAY_URL"]

# ---- Models ----
class StartRequest(BaseModel):
    incident_id: str
    actor: str  # 요청자(권한/감사에 필요)

class StartResponse(BaseModel):
    response_id: str  # OpenAI background 작업 핸들

class StatusResponse(BaseModel):
    status: str
    result: Optional[Dict[str, Any]] = None

# ---- MCP Gateway helper ----
async def mcp_call(tool: str, args: Dict[str, Any], actor: str, trace_id: str) -> Dict[str, Any]:
    """
    MCP Gateway에 tool 실행을 요청.
    - 실무에서는 여기서 OPA 정책검사, allowlist, PII 마스킹, audit log가 같이 걸림.
    """
    payload = {
        "tool": tool,
        "args": args,
        "actor": actor,
        "trace_id": trace_id,
    }
    async with httpx.AsyncClient(timeout=30) as hx:
        r = await hx.post(f"{MCP_GATEWAY_URL}/call", json=payload)
        r.raise_for_status()
        return r.json()

# ---- API ----
@app.post("/incidents/start", response_model=StartResponse)
async def start(req: StartRequest):
    # 1) incident 데이터는 MCP tool로 가져온다 (LLM이 DB 직접 접근 X)
    trace_id = f"inc-{req.incident_id}"  # 실무: UUID + 상관관계 키
    incident = await mcp_call(
        tool="incidents.get_context",
        args={"incident_id": req.incident_id},
        actor=req.actor,
        trace_id=trace_id,
    )

    # 2) LLM은 “결정론적 뼈대”가 만든 컨텍스트를 받아서 생성만 한다
    prompt = {
        "incident": incident,
        "instructions": [
            "너는 SRE assistant다.",
            "출력은 반드시 JSON 하나로만 반환한다.",
            "키: summary, likely_root_causes (array), next_actions (array), confidence (0..1).",
            "근거는 incident 데이터에서 인용 가능한 범위에서만 작성한다."
        ],
    }

    # 3) background mode: 긴 작업을 job으로 실행 (타임아웃/재시도에 유리)
    resp = client.responses.create(
        model="gpt-4.1",  # 예시. 팀 표준 모델로 교체
        input=[
            {"role": "system", "content": "Return ONLY valid JSON. No prose."},
            {"role": "user", "content": json.dumps(prompt)}
        ],
        background=True,
        # 실무: metadata에 trace_id/actor/incident_id 넣어 observability 연결
        metadata={"trace_id": trace_id, "actor": req.actor, "incident_id": req.incident_id},
    )
    return StartResponse(response_id=resp.id)

@app.get("/incidents/status/{response_id}", response_model=StatusResponse)
async def status(response_id: str):
    r = client.responses.retrieve(response_id)

    # status: "in_progress" / "completed" 등
    if r.status != "completed":
        return StatusResponse(status=r.status)

    # output_text는 모델별/SDK별 다를 수 있어, 실제론 structured output 사용 권장.
    # 여기선 "JSON only"를 강제했으니 파싱 시도.
    text = r.output_text
    try:
        data = json.loads(text)
    except Exception:
        raise HTTPException(500, "Model did not return valid JSON")

    # 완료 후: MCP tool로 결과 저장(감사로그/권한/정책 적용 지점)
    trace_id = (r.metadata or {}).get("trace_id", "unknown")
    actor = (r.metadata or {}).get("actor", "unknown")
    await mcp_call(
        tool="incidents.store_analysis",
        args={"analysis": data},
        actor=actor,
        trace_id=trace_id,
    )

    return StatusResponse(status="completed", result=data)

예상 동작(현실적인 출력)

  • /incidents/start{"response_id":"resp_..."}
  • /incidents/status/resp_...
    • 진행 중: {"status":"in_progress"}
    • 완료:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      
      {
        "status": "completed",
        "result": {
          "summary": "2026-05-12 03:14 UTC부터 checkout API에서 5xx 급증...",
          "likely_root_causes": ["DB connection pool exhaustion", "Recent deploy regression in payment-service"],
          "next_actions": ["Rollback payment-service v2.3.7", "Increase pool size with cap", "Add alert on queue depth"],
          "confidence": 0.63
        }
      }
      

이 구조의 핵심은 LLM이 DB/툴을 직접 “알아서” 만지는 게 아니라,

  • tool 호출은 MCP Gateway로 표준화(정책/감사/권한의 중심)
  • LLM은 생성/추론 스텝에만 집중
  • 긴 작업은 background job으로 운영 가능
    이라는 점입니다. (semantic.io)

⚡ 실전 팁 & 함정

Best Practice (2~3개)

1) 툴 노출(visibility)과 자동 실행(auto-exec)을 분리

  • “모델이 볼 수 있는 툴” ≠ “승인 없이 실행해도 되는 툴”
  • 실제로 많은 팀이 2단 allowlist로 사고를 줄입니다. (reddit.com)

2) tool guardrail을 ‘모든 호출’에 걸기

  • agent-level output 검증만으로는 데이터 변경/유출을 막기 어렵습니다.
  • Agents SDK도 tool guardrails를 별도 권장합니다. (openai.github.io)

3) RAG/툴/모델 호출을 하나의 Trace로 엮기(OpenTelemetry 계열)

  • “모델 호출만 추적”하면 문제의 절반만 보입니다(대부분 upstream retrieval/tool에서 깨짐).
  • 2026년에는 RAG 파이프라인을 span 단위로 쪼개 추적하는 접근이 확산 중입니다. (uptrace.dev)

흔한 함정/안티패턴

  • 직렬 tool-call 사슬(LLM 왕복이 5~10번): 성능/비용이 선형이 아니라 체감상 폭발합니다. “결정론적 backbone + batch/병렬화”로 줄이세요. (reddit.com)
  • MCP 도입 = 보안 해결이라고 착각: MCP는 표준 인터페이스일 뿐, 정책·권한·감사·샌드박싱은 별도 레이어가 필요합니다. (techradar.com)
  • 평가 없는 배포: offline benchmark만으로는 운영 품질이 유지되지 않습니다. CI gate(시나리오 기반 readiness score)로 막아야 합니다. (arxiv.org)

비용/성능/안정성 트레이드오프

  • Durable execution/observability를 붙이면 초기 개발 속도는 느려지지만, 장기적으로 incident 대응/디버깅 비용이 크게 감소합니다.
  • background job은 UX가 즉답형보다 복잡(상태 조회 필요)하지만, 타임아웃/재시도/부하제어 측면에서 안정적입니다. (platform.openai.com)
  • MCP Gateway는 중앙집중형이므로 병목이 될 수 있어, rate-limit/캐시/큐잉을 같이 설계해야 합니다(특히 tool latency가 긴 경우).

🚀 마무리

2026년 5월의 “확장 가능한 AI 앱 아키텍처”는 유행하는 프레임워크 이름보다, 다음 3가지를 갖췄는지로 판단하는 게 정확합니다.

1) Deterministic backbone이 실행을 통제하는가? (LLM은 특정 step에만) (zylos.ai)
2) Durable execution + async로 실패/장시간 작업이 기본 처리되는가? (docs.langchain.com)
3) MCP/툴 정책/guardrails/observability/evaluation이 운영 기본값인가? (semantic.io)

다음 학습 추천(순서)

  • MCP: capabilities 설계(툴 스키마, 권한 모델, gateway 패턴) (semantic.io)
  • Durable execution: LangGraph durable execution 개념/상태 모델링 (docs.langchain.com)
  • Observability: RAG 파이프라인을 span으로 쪼개는 OTel 접근 (uptrace.dev)

원하면, 위 예제를 (1) LangGraph 기반 그래프 오케스트레이션 버전 또는 (2) MCP Gateway에 OPA(Rego) 정책을 붙인 버전으로 확장해 드릴게요.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.