메시지 스키마 설계
를 깔았다면 다음은 그릇 안의 모양이다. 의 를 잡아두지 않으면 양쪽 끝이 서로 다르게 해석한다. 6장이 인프라였다면, 7장은 약속이다.
좋은 메시지 스키마의 5가지 조건
견고한 포맷은 다섯 가지를 만족해야 한다. 빠진 게 있으면 운영 단계에서 반드시 사고가 난다.
- 명시적 역할 — 누가 누구에게 말하는지 ().
- 본문과 메타의 분리 — 와 를 한 덩어리로 섞지 않는다.
- 추적 ID — ·· 세 종류를 구분.
- 버전 필드 — 스키마는 진화한다. 처음부터
schema_version박아둔다. - 기계 검증 가능 — 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 늘리기
는 반드시 변한다. 포맷도 예외가 아니다. 두 가지 전략:
- 호환 변경(추가): 새 필드는 선택으로. 기존 코드는 무시하고 지나간다.
- 비호환 변경(삭제·타입 변경):
schema_version을 올린다. 의 버전 필드를 라우터가 보고 핸들러를 가른다.
# 호환: 새 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}")디버깅에 친절한 메시지란
메시지를 로그에 찍을 때 마음에 들지 않으면 스키마가 잘못된 거다.
체크리스트:
- 한 줄 로그만 봐도 어떤 의 어느 요청인지 보이는가?
- 가 너무 커서 가려야 한다면 미리 truncated 필드를 두는가?
- 개인정보가 본문에 섞여 자동 마스킹이 어려운가? 메타데이터에 마스킹 힌트를 박는다.
좋은 는 디버깅 시간을 크게 줄인다. 이 거의 무료가 된다.
흔한 함정 4가지
| 함정 | 증상 | 처방 |
|---|---|---|
| String만 쓰기 | LLM 출력을 그대로 본문에 박음 | 구조화된 강제 |
| 버전 없음 | 변경 시 모든 소비자가 한꺼번에 깨짐 | schema_version 박기 |
| 단일 ID | 추적 불가 | // 세 종류 분리 |
| extra 허용 | 오타가 조용히 통과 | extra="forbid" / .strict() |
마지막은 가장 사고를 많이 낸다. 새 필드 오타를 검증기가 안 잡으면, 운영 중에야 “왜 동작 안 하지”로 발견한다.
다음 챕터로
이제 진짜로 의 스펙으로 들어갈 준비가 됐다. ··까지, Google A2A가 이 모든 약속을 어떻게 표준화했는지 다음 장에서 분해한다.