메시지 스키마 설계

를 깔았다면 다음은 그릇 안의 모양이다. 를 잡아두지 않으면 양쪽 끝이 서로 다르게 해석한다. 6장이 인프라였다면, 7장은 약속이다.


좋은 메시지 스키마의 5가지 조건

견고한 포맷은 다섯 가지를 만족해야 한다. 빠진 게 있으면 운영 단계에서 반드시 사고가 난다.

  1. 명시적 역할 — 누가 누구에게 말하는지 ().
  2. 본문과 메타의 분리를 한 덩어리로 섞지 않는다.
  3. 추적 ID·· 세 종류를 구분.
  4. 버전 필드 — 스키마는 진화한다. 처음부터 schema_version 박아둔다.
  5. 기계 검증 가능 — JSON Schema 또는 코드 스키마로 양쪽 언어가 같은 정의를 본다.

역할(role)이 첫 번째다

LLM 챗 포맷이 이미 정해놓은 관습이 있다. system, user, assistant, tool 네 가지.

A2A로 넘어가면 한 줄이 더 필요하다 — agent. 같은 어시스턴트라도 자기 에이전트와 다른 에이전트의 발화는 의미가 다르다.

발화자용도
system정체성·규칙 고정
user사람원 요청
assistant같은 본인내부 추론
agent동료 에이전트A2A 응답
tool도구도구 결과

payload vs metadata: 흔한 안티패턴

가장 잦은 실수: 추적 ID·타임스탬프·라우팅 힌트를 본문 안에 섞어 넣는 것. 서로 다른 독자가 읽는다는 사실을 기억하자 — 본문은 이, 메타는 시스템이 읽는다.

# 나쁜 예: 본문에 메타가 섞임
bad = {
  "role": "agent",
  "content": "보고서 완료. trace=abc123, ts=2026-06-02T...",
}

# 좋은 예: payload와 metadata 분리
good = {
  "role": "agent",
  "payload": {"text": "보고서 완료."},
  "metadata": {
      "correlation_id": "abc123",
      "conversation_id": "conv-42",
      "request_id": "req-99",
      "timestamp": "2026-06-02T10:00:00Z",
      "schema_version": "1.0",
  },
}

본문은 LLM이 읽는다. 메타데이터는 시스템이 읽는다. 섞으면 둘 다 헷갈린다.


세 가지 ID — 무엇이 다른가

세 ID는 입자 크기가 다르다. 헷갈리면 추적이 깨진다.

ID범위수명비유
단일 요청·응답초~분한 통의 편지
한 작업 흐름 전체분~시간한 통의 편지가 거치는 우체국 경로
한 세션·대화시간~일한 사람과 주고받은 편지 묶음 전체

규칙: . 거꾸로는 성립하지 않는다.


분산 추적과 correlation-id

다이어그램 로딩…

는 분산 추적의 핵심이다. A가 B에 요청하면, B는 C, D를 부른다. 이때 모든 호출이 같은 correlation-id를 메타에 달고 가야 한 흐름으로 묶인다.

23장에서 다룰 의 trace_id가 정확히 이 역할이다. 직접 지을 거라면 UUIDv4면 충분하다.


Pydantic v2로 메시지 스키마 정의

Python 진영의 사실상 표준은 검증에 pydantic v2. v1 패턴(@validator, Config 클래스)은 잊자. ··을 한 모델로 묶는다.

# Verified against: https://docs.pydantic.dev/latest/concepts/models/
# Verified at: 2026-06-02
from typing import Literal, Any
from pydantic import BaseModel, Field, ConfigDict
from uuid import UUID

Role = Literal["system", "user", "assistant", "agent", "tool"]

class Metadata(BaseModel):
  model_config = ConfigDict(extra="forbid")
  correlation_id: UUID
  conversation_id: UUID
  request_id: UUID
  timestamp: str
  schema_version: str = "1.0"

class Message(BaseModel):
  model_config = ConfigDict(extra="forbid")
  role: Role
  payload: dict[str, Any] = Field(...)
  metadata: Metadata

검증은 양쪽 끝에서 모두

송신자와 수신자 모두 같은 를 검증해야 한다. 한쪽만 검사하면 프로토콜이 깨지는 순간을 놓친다. 가 들어오자마자, 를 쓰기 전에 검증한다.

# Verified against: https://docs.pydantic.dev/latest/concepts/validators/
# Verified at: 2026-06-02
from pydantic import ValidationError

raw = {"role": "agent", "payload": {"text": "hi"}, "metadata": {...}}

try:
  m = Message.model_validate(raw)
except ValidationError as e:
  # 4xx 응답 또는 dead-letter queue로 격리
  print("invalid:", e.errors())

safeParse는 예외를 던지지 않는다. 핸들러 같은 비동기 루프에서는 예외보다 결과 객체가 안전하다.


JSON Schema로 양쪽 언어가 동기화

Python 쪽 pydantic과 TS 쪽 zod를 사람이 따로 관리하면 어긋난다. 둘 다 내보낼 수 있다.

# Verified against: https://docs.pydantic.dev/latest/concepts/json_schema/
# Verified at: 2026-06-02
import json
print(json.dumps(Message.model_json_schema(), indent=2))

전략: 한쪽을 진실의 원천으로 정한다. 보통 Python pydantic → TS zod. 가 빌드 단계에서 어긋나면 CI가 실패하도록 둔다. 한 정의가 두 언어를 동시에 묶는다.


버전 진화: 부수기 vs 늘리기

는 반드시 변한다. 포맷도 예외가 아니다. 두 가지 전략:

# 호환: 새 optional 필드 추가
class MetadataV1_1(Metadata):
  priority: int = 0  # default 있으면 v1 메시지도 유효

# 비호환: 버전 분기
def handle(msg: dict):
  v = msg.get("metadata", {}).get("schema_version", "1.0")
  if v.startswith("1."):
      return handle_v1(Message.model_validate(msg))
  if v.startswith("2."):
      return handle_v2(MessageV2.model_validate(msg))
  raise ValueError(f"unknown schema version: {v}")

디버깅에 친절한 메시지란

메시지를 로그에 찍을 때 마음에 들지 않으면 스키마가 잘못된 거다.

체크리스트:

좋은 는 디버깅 시간을 크게 줄인다. 이 거의 무료가 된다.


흔한 함정 4가지

함정증상처방
String만 쓰기LLM 출력을 그대로 본문에 박음구조화된 강제
버전 없음변경 시 모든 소비자가 한꺼번에 깨짐schema_version 박기
단일 ID추적 불가// 세 종류 분리
extra 허용오타가 조용히 통과extra="forbid" / .strict()

마지막은 가장 사고를 많이 낸다. 새 필드 오타를 검증기가 안 잡으면, 운영 중에야 “왜 동작 안 하지”로 발견한다.


다음 챕터로

이제 진짜로 의 스펙으로 들어갈 준비가 됐다. ··까지, Google A2A가 이 모든 약속을 어떻게 표준화했는지 다음 장에서 분해한다.