1. 왜 MCP 이벤트가 필요한가
지금까지 ChatGPT와 여러분의 백엔드 간 소통은 거의 모두 RPC 같았습니다: 모델이 도구를 호출하고, 도구가 무언가를 수행한 뒤 결과를 반환하면 — 끝. 작업이 짧을 때는(200–500ms, 많아야 몇 초) 매우 편리합니다.
하지만 장기 실행 작업이 등장하는 순간 — GiftGenius에서 직원 선호도가 담긴 큰 파일 분석, 수많은 외부 API에서 추천을 모아 집계, 거대한 피드 재계산 — 온갖 불편이 시작됩니다. HTTP 타임아웃, 함수 재시작, 끝나지 않는 스피너, 그리고 사용자는 “아직 살아 있는 건가? 이미 죽었나?” 하고 의문을 품게 됩니다.
바로 여기서 이벤트 모델이 필요해집니다. 하나의 긴 도구 호출을 붙잡고 있지 말고, 작업을 시작해 jobId를 받은 다음, 서버가 자발적으로 이벤트를 보냅니다: 시작, 진행 중, 완료, 실패. MCP에서 이러한 이벤트는 JSON-RPC notifications로 구현됩니다 — 상위 수준에 id가 없고, 이에 대한 응답을 기대하지 않는 단방향 메시지입니다.
중요한 점: 이벤트는 “선(wire) 위의 console.log”가 아닙니다. 이는 특정 스키마를 갖춘 프로토콜의 정식 메시지이며, 여러분의 UI(위젯) 및/또는 에이전트는 도구 호출 결과만큼이나 엄격하게 이를 처리할 수 있어야 합니다.
리마인더: MCP의 메시지 유형
본격적으로 들어가기 전에 MCP에 어떤 메시지가 있는지 간단히 상기해 봅시다.
마케팅 레이어를 모두 걷어내면 MCP는 JSON-RPC 2.0에 기반합니다. 거기에는 세 가지 기본 메시지 유형이 있습니다: 요청, 응답, 알림.
목록으로 나열하는 대신, 작은 비교 표를 봅시다:
| 유형 | 필드 id | 발신자 | 응답 기대 여부 | MCP 예시 |
|---|---|---|---|---|
| Request | 있음 | 보통 클라이언트(ChatGPT) | 예 | 도구 호출 tools/call |
| Response | 있음 | MCP 서버 | 그 자체가 응답 | tools/call의 결과 |
| Notification | 없음 | 클라이언트 또는 서버 | 아니오 | notifications/progress, resources/updated, logging/message |
MCP 이벤트는 바로 세 번째 행, 즉 notifications에 속합니다. 주요 특징:
- 상위 수준에 id가 없음 — result나 error 응답이 오지 않습니다;
- 발신자는 ACK를 기다리지 않음 — 프로토콜 수준에서 “발사하고 잊기”;
- 신뢰성은 확인 응답이 아닌, 핸들러의 멱등성과 재전송 정책에 의해 보장됩니다.
중요한 제약: MCP 이벤트는 “언제든 우주 어딘가에서” 날아다니지 않습니다. 이는 특정 전송 위에 설정된 MCP 연결 내부에서만 존재합니다. 대부분 SSE와 비슷한 스트림을 사용합니다(전송의 세부 및 변형은 별도 강의에서 다룹니다).
2. 실전에서 ‘MCP 이벤트’란 무엇인가
형식적으로 MCP 이벤트는 JSON-RPC notification, 즉 다음 형태의 객체입니다:
{
"jsonrpc": "2.0",
"method": "notifications/job/progress",
"params": {
"jobId": "job_123",
"percentage": 30,
"stage": "카탈로그에서 후보를 찾는 중",
"eventId": "evt_abc123",
"timestamp": "2025-11-21T10:15:00Z"
}
}
여기서 중요한 포인트 몇 가지:
- method 필드에는 이벤트 유형과 그 “네임스페이스”를 인코딩합니다. MCP에는 이미 로그, 진행률, 리소스 변경을 위한 notifications/... 형태의 표준 메서드가 일부 정의되어 있지만, 여러분은 notifications/job/progress나 notifications/job/completed처럼 비즈니스에 특화된 메서드를 추가해야 합니다.
- 모든 비즈니스 데이터는 params에 있습니다. 작업 식별자(jobId), 이벤트의 고유 id(eventId), 시간(timestamp), 사람이 읽을 수 있는 메시지 등이 이곳에 들어갑니다.
- 상위 수준에 id 필드가 없습니다 — 그래서 notification입니다. 이에 대한 응답은 프로토콜에 없습니다. 서버가 “이해했는지” 확인하고 싶다면 또 다른 이벤트를 보내거나, 클라이언트의 반응(예: 새로운 요청)을 기다릴 수 있습니다. 하지만 JSON-RPC 관점의 ACK는 없습니다.
멘탈 모델로는 이렇게 생각할 수 있습니다: tools/call 호출은 “답장을 기다리는 편지”, 이벤트는 “Slack 봇의 알림: “백그라운드 작업 #123이 완료되었습니다””에 가깝습니다.
3. 이벤트 택소노미: 어떤 알림이 있는가
“아무 JSON이나 notifications로 보내도 된다”라고 허용하면, 2주 뒤 시스템은 난장판이 됩니다: 이벤트 이름이 제각각이고, 필드가 들쭉날쭉하며, UI는 이를 어떻게 처리해야 할지 모릅니다. 그래서 작은 택소노미에 합의하는 것이 유용합니다.
아래는 MCP 스펙과 실제 ChatGPT Apps 사례에 잘 맞는 분류의 한 예입니다.
작업 생명주기 이벤트 (Job Lifecycle)
작업 상태의 핵심 전이를 반영하는 이벤트입니다. 보통 작업에는 pending → running → (completed | failed | canceled) 같은 상태 머신이 있습니다.
대표적인 이벤트:
- job.created — 작업이 등록됨;
- job.started — 워커가 작업을 시작함;
- job.completed — 작업이 성공적으로 완료됨;
- job.failed — 작업이 오류로 실패함;
- job.canceled — 사용자가 작업을 취소함.
job.completed의 GiftGenius 예시:
{
"jsonrpc": "2.0",
"method": "notifications/job/completed",
"params": {
"eventId": "evt_gg_100",
"jobId": "giftjob_42",
"timestamp": "2025-11-21T10:20:00Z",
"summary": "선물 추천이 완료되었습니다",
"resultResourceId": "resource:gifts:giftjob_42"
}
}
여기서 resultResourceId는 이후 위젯이나 에이전트가 읽을 MCP 리소스를 가리킬 수 있습니다.
진행률 이벤트 (Progress Updates)
이는 생명주기 내의 “작은 단계”입니다: 최종 상태를 바꾸지는 않지만, 무언가 진행 중임을 사용자에게 알려 줍니다.
전형적인 job.progress 이벤트:
{
"jsonrpc": "2.0",
"method": "notifications/job/progress",
"params": {
"eventId": "evt_gg_101",
"jobId": "giftjob_42",
"timestamp": "2025-11-21T10:18:30Z",
"percentage": 40,
"stage": "예산 기준으로 선물을 필터링하는 중",
"etaSeconds": 25
}
}
여기서 percentage는 100을 향해 합리적으로 증가해야 하며, 이리저리 뛰면 안 됩니다. 진행률 필드는 하나의 이름(예: percentage)으로 정하고 모든 이벤트에서 동일하게 사용하세요. MCP의 공식 진행률 유틸리티에도 “진행률은 오직 증가한다”는 규칙이 있습니다.
데이터/리소스 갱신 이벤트 (Resource/Data events)
때로는 특정 jobId 자체는 중요하지 않습니다. 어떤 엔터티가 바뀌었다는 사실이 더 중요합니다: 상품 피드가 업데이트되었거나, 새로운 리포트 스냅샷이 생성되었거나, 개인화 프로필이 재생성되는 경우처럼요.
MCP에는 이미 resources/updated, resources/list_changed 같은 표준 알림이 있으며, 이는 클라이언트에게 “리소스 목록을 다시 읽어라, 무언가 변경되었다”고 신호를 보냅니다.
GiftGenius에서는 다음과 같을 수 있습니다:
{
"jsonrpc": "2.0",
"method": "resources/updated",
"params": {
"eventId": "evt_feed_17",
"timestamp": "2025-11-21T09:00:00Z",
"resourceId": "resource:product-feed",
"changeType": "snapshot_ready"
}
}
위젯은 이런 이벤트를 받으면, 예를 들어 “선물 목록 새로 고침” 버튼을 하이라이트할 수 있습니다.
UX 및 시스템 이벤트
엄밀히 비즈니스는 아니지만 UX나 진단에 중요한 이벤트도 있습니다:
- 로그 메시지 logging/message — 로그를 위한 표준 MCP 알림;
- heartbeat/ping — 서버의 주기적인 “살아있음” 신호;
- 성능 저하 경고: 예를 들어 “지금 외부 API가 느려 결과가 더 늦게 도착할 수 있습니다”.
이러한 이벤트는 모니터링과 디버깅에 유용하며, 때로는 UI에서 “시스템이 죽은 게 아니라 바쁘다”는 신호로 적절히 보여 줄 수 있습니다.
4. 이벤트 구조: 필수 필드와 payload
이벤트도 도구 요청과 마찬가지로 API 객체입니다. 설계가 필요합니다. 좋은 습관은 기본 필드 세트를 합의하는 것입니다.
개념적으로 이벤트를 세 부분으로 나누는 것이 유용합니다: 메타데이터, 상관관계(correlation), 페이로드.
일반 형태 예시:
{
"jsonrpc": "2.0",
"method": "notifications/job/progress",
"params": {
"eventId": "evt_gg_103",
"type": "job.progress",
"timestamp": "2025-11-21T10:19:00Z",
"jobId": "giftjob_42",
"payload": {
"percentage": 60,
"stage": "리뷰를 비교하는 중",
"etaSeconds": 15
}
}
}
이 구조에서 다음을 구분할 수 있습니다:
- eventId — 이벤트의 고유 식별자. 클라이언트에서 중복 제거에 필요;
- type — 이벤트의 논리적 이름(method를 보완/정규화할 수 있음);
- timestamp — 이벤트가 서버에서 생성된 시간;
- jobId 또는 다른 correlation-id — 이 이벤트가 무엇에 속하는지 파악하기 위함;
- payload — 실제 데이터. 이벤트 유형마다 고유한 형태를 가짐.
실제 시스템에서는 JSON Schema 또는 최소한 TypeScript 타입으로 이러한 구조를 형식적으로 기술해, 서버와 클라이언트가 메시지를 검증하고 싶어질 가능성이 매우 높습니다. 일부 팀은 CloudEvents에서 영감을 받은 포맷을 사용합니다: id, source, type, time 등의 표준 필드가 있습니다.
하지만 핵심 아이디어는 간단합니다: 이벤트는 기계가 읽을 수 있어야 하고 일관적이어야 합니다 — jobId가 가끔은 jobId, 가끔은 job_id, 가끔은 아예 없는 “놀람”이 없어야 합니다.
아래 예시에서는 코드를 과도하게 복잡하게 만들지 않기 위해 “평탄화된” 형태를 더 자주 사용하겠습니다: 이벤트의 모든 데이터가 params에 직접 있고, type은 그 역할을 method가 대신한다면 생략하기도 합니다. 원칙은 동일합니다: 각 이벤트에는 안정적인 메타데이터(eventId, jobId, timestamp)와 예측 가능한 페이로드가 있어야 합니다.
5. 이벤트의 멱등성: 왜, 어떻게
이제 이 강의의 가장 중요한 단어 — 멱등성입니다.
이벤트 핸들러의 멱등성이란 동일한 이벤트를 한 번 처리하든 열 번 처리하든 시스템의 최종 상태가 올바르게 유지되는 것을 의미합니다. 네트워크와 재시도가 있는 분산 시스템에서는 생존에 직결되는 문제입니다.
동일한 이벤트가 여러 번 도착할 수 있는 이유는 무엇일까요?
이유는 많습니다: 연결 끊김과 재연결, 서버 측 재시도(“혹시 몰라서” 알림을 한 번 더 보냄) 등. 스트리밍 프로토콜을 사용할 때(예: 서버가 열린 연결로 이벤트를 푸시하는 SSE — 자세한 내용은 전송 파트의 별도 강의에서) 흔한 일입니다. 클라이언트가 Last-Event-ID로 재연결하면, 서버는 빠진 이벤트를 다시 보내고 클라이언트는 그중 일부를 두 번 보게 됩니다.
핸들러가 멱등적이지 않으면 이상한 일들이 발생합니다:
- job.completed 이벤트가 보너스를 두 번 적립하거나 주문 상태를 두 번 바꿈;
- resource.updated 이벤트가 올 때마다 위젯이 카드를 “추가”만 해서 UI에 중복 표시됨;
- 중복된 job.progress가 오면 진행률 표시줄이 앞뒤로 흔들려 사용자에게 혼란을 줌.
올바른 전략은 두 층으로 작동합니다: 서버에서의 이벤트 생성, 클라이언트에서의 이벤트 처리.
서버 측: 안정적인 id와 상태 머신
서버는 다음을 보장해야 합니다:
- 각 논리적 이벤트에 대해 고유한 eventId를 생성;
- 동일한 jobId의 이벤트가 유효한 상태 전이 시퀀스를 이루도록 보장: job.completed 이후에 job.failed를 보낼 수 없고, 서로 다른 결과를 가진 두 개의 job.completed를 보낼 수 없습니다.
즉, 사실상 작업에 대한 상태 머신이 있으며, 각 이벤트는 허용된 전이입니다.
클라이언트 측: 중복 제거와 “부드러운” 업데이트
클라이언트(위젯, 에이전트 또는 다른 컴포넌트)는 다음을 수행해야 합니다:
- 현재 연결/세션 동안 최소한 이미 처리한 eventId 집합을 보관;
- 처리 전에 확인: eventId를 이미 봤다면 단순히 무시하거나 부작용 없이 UI만 다시 그리기;
- 작업 상태를 바꾸는 이벤트(job.completed, job.failed)를 받을 때 전이가 타당한지 확인: 예를 들어 작업이 이미 completed로 표시되었다면, 반복된 job.completed는 아무것도 바꾸지 않아야 하고, failed는 부적절한 것으로 간주해 무시하는 편이 낫습니다.
커머스 세계의 고전적 예: 결제 확인 웹훅 처리입니다. 동일한 order.paid가 두 번 올 수 있으므로 백엔드는 paymentId와 “이미 적립됨” 플래그를 저장합니다. 웹훅이 다시 와도 주문 상태는 바뀌지 않습니다. MCP 이벤트도 같은 사고방식으로 설계해야 합니다.
6. 예시: GiftGenius를 위한 이벤트 설계
이를 우리의 학습용 GiftGenius에 적용해 봅시다. 시나리오: 사용자가 직원과 관심사 목록이 담긴 큰 CSV를 업로드하고 “모두에게 선물 아이디어를 추천해줘”라고 요청합니다. 이 작업은 수십 초가 걸릴 수 있습니다.
합리적인 이벤트 모델은 다음과 같이 묘사할 수 있습니다:
- 사용자가 도구 start_bulk_gift_analysis를 실행합니다. 도구는 jobId를 반환합니다: "bulk_2025_001".
- MCP 서버는 작업을 생성하고 거의 즉시 간단한 설명과 함께 job.started를 보냅니다.
- 수행 중에 여러 번의 job.progress를 보냅니다. 예:
- 10% — “파일을 파싱하고 형식을 검사”;
- 40% — “관심사와 부서를 추출”;
- 70% — “카테고리별로 선물을 매칭”;
- 100% — 완료 직전.
- 마지막에 job.completed가 도착하며, 최종 추천이 담긴 리소스에 대한 링크를 제공합니다.
- 문제가 생기면 — completed 대신 오류 코드와 수정 힌트가 담긴 job.failed가 옵니다.
실제로는 이렇게 동작하지만, 두 가지 핵심 이벤트 job.progress와 job.completed를 JSON 스키마 형태로 고정해 봅시다. 의사 JSON Schema(단순화):
{
"job.progress": {
"type": "object",
"properties": {
"eventId": { "type": "string" },
"jobId": { "type": "string" },
"timestamp": { "type": "string", "format": "date-time" },
"percentage": { "type": "number", "minimum": 0, "maximum": 100 },
"stage": { "type": "string" },
"etaSeconds": { "type": "number" }
},
"required": ["eventId", "jobId", "timestamp", "percentage", "stage"]
}
}
{
"job.completed": {
"type": "object",
"properties": {
"eventId": { "type": "string" },
"jobId": { "type": "string" },
"timestamp": { "type": "string", "format": "date-time" },
"summary": { "type": "string" },
"resultResourceId": { "type": "string" }
},
"required": ["eventId", "jobId", "timestamp", "resultResourceId"]
}
}
당장 완전한 스키마 검증을 구현할 필요는 없지만, 이런 구조를 마음속에 두는 것은 유익합니다. 필드가 다양한 포맷으로 “흩어지는” 것을 막고 중요한 메타데이터를 빠뜨리지 않게 해 줍니다.
7. 미니 실습: MCP 이벤트를 보내는 서버
이제 이론을 간단한 TypeScript 의사 코드와 연결해 봅시다. 지금은 실제 MCP 라이브러리를 사용하지 않겠습니다(첫째, 아직 진화 중이고, 둘째, 여기서는 모델이 핵심이므로). 대신 구조적 골격을 그려 보겠습니다.
우리 MCP 서버에 ChatGPT로 JSON-RPC notification을 보낼 수 있는 추상화 sendNotification이 있다고 합시다. 의사 인터페이스:
// MCP notification을 보내는 유틸리티
async function sendNotification(
method: string,
params: Record<string, unknown>
) {
// 여기서 JSON을 직렬화해 활성 MCP 연결로 전송합니다
}
이제 도구 start_bulk_gift_analysis의 핸들러를 구현해 봅시다. 작업을 등록하고 jobId를 반환하며, 어딘가에서 “틱”거리며 진행률을 보냅니다. 실제로는 워커와 큐가 필요하겠지만, 여기서는 타이머로 제한하겠습니다.
type Job = {
id: string;
status: "pending" | "running" | "completed" | "failed";
};
const jobs = new Map<string, Job>();
export async function startBulkGiftAnalysisTool() {
const jobId = `bulk_${Date.now()}`;
jobs.set(jobId, { id: jobId, status: "pending" });
// 바로 job.started 전송
await sendNotification("notifications/job/started", {
eventId: `evt_${jobId}_started`,
jobId,
timestamp: new Date().toISOString(),
summary: "대량 선물 목록 분석이 시작되었습니다"
});
simulateJob(jobId); // 백그라운드에서 작업을 "시작"
return { jobId };
}
작업 시뮬레이션 자체:
async function simulateJob(jobId: string) {
jobs.set(jobId, { id: jobId, status: "running" });
const stages = [
{ percent: 10, stage: "CSV 파싱" },
{ percent: 40, stage: "관심사 분석" },
{ percent: 70, stage: "선물 매칭" },
{ percent: 100, stage: "결과 생성" }
];
for (const s of stages) {
await sendNotification("notifications/job/progress", {
eventId: `evt_${jobId}_${s.percent}`,
jobId,
timestamp: new Date().toISOString(),
percentage: s.percent,
stage: s.stage
});
await new Promise(r => setTimeout(r, 1000));
}
jobs.set(jobId, { id: jobId, status: "completed" });
await sendNotification("notifications/job/completed", {
eventId: `evt_${jobId}_done`,
jobId,
timestamp: new Date().toISOString(),
summary: "선물 분석이 완료되었습니다",
resultResourceId: `resource:gifts:${jobId}`
});
}
코드는 일부러 단순하지만, 다음이 잘 드러납니다:
- started → progress* → completed 이벤트 시퀀스를 사용합니다;
- 각 이벤트는 고유한 eventId를 가집니다;
- 모든 이벤트는 동일한 jobId에 묶입니다.
나중에 실제 큐와 워커를 추가해도 이벤트 구조는 거의 같을 것입니다 — 달라지는 것은 sendNotification이 호출되는 위치뿐입니다.
8. 클라이언트: 가장 단순한 멱등적 이벤트 핸들러
클라이언트 측(예: Apps SDK 위젯)에서는 이런 이벤트를 수신해 현재 작업과 연결하고, 중복에도 흔들리지 않도록 처리할 수 있어야 합니다.
전송(트랜스포트) 세부는 나중에 다루기로 하고, MCP 클라이언트 레이어가 들어오는 notification마다 호출하는 onMcpNotification 함수가 있다고 가정해 봅시다.
가장 단순한 중복 제거를 추가해 보겠습니다:
const processedEvents = new Set<string>();
function handleNotification(method: string, params: any) {
const eventId = params.eventId as string | undefined;
if (!eventId) return; // 아주 위험하지만, 예제로는 넘어갑니다
if (processedEvents.has(eventId)) {
// 중복 — 무시하거나 UI를 부드럽게 갱신
return;
}
processedEvents.add(eventId);
if (method === "notifications/job/progress") {
updateJobProgress(params.jobId, params.percentage, params.stage);
} else if (method === "notifications/job/completed") {
markJobCompleted(params.jobId, params.resultResourceId);
}
}
updateJobProgress와 markJobCompleted 구현은 순수한 React/UI 코드입니다:
function updateJobProgress(jobId: string, percent: number, stage: string) {
// 예: Zustand/Redux/React state에 저장
console.log(`Job ${jobId}: ${percent}% — ${stage}`);
}
function markJobCompleted(jobId: string, resourceId: string) {
console.log(`Job ${jobId} 완료, 리소스: ${resourceId}`);
}
이런 핸들러는 다음과 같습니다:
- 이벤트가 두 번 와도 망가지지 않습니다;
- 부작용을 만들지 않습니다(예: “완료!” 모달을 두 번 띄우지 않음);
- 상태 전이의 타당성 검증 같은 더 복잡한 로직으로 확장하기 쉽습니다(이미 completed 상태인데 failed를 허용하지 않음).
실전 코드에서는 MCP 서버에 재연결할 때 processedEvents를 초기화하고, 각 jobId의 현재 상태도 함께 저장해, 이상한 이벤트 시퀀스에 더 합리적으로 대응하고 싶을 것입니다.
이제 MCP 이벤트가 에이전트/위젯을 거쳐 실제 사용자 경험으로 어떻게 변환되는지 이해하는 것이 중요합니다: 진행률 표시줄, 단계, 최종 결과 표시 등. 이벤트를 run/workflow 및 UX와 연결해 봅시다.
9. 이벤트, run/workflow, UX의 연결
워크플로와 에이전트에 대한 전체 모듈을 이미 다뤘지만, 이제 전체 그림을 보게 될 것입니다. 우리는 이미 이벤트 계열(job.*, resource.*, 시스템)을 도입했고, 이것이 에이전트/위젯과 ChatGPT를 통과하여 구체적인 사용자 경험으로 어떻게 바뀌는지 살펴보겠습니다.
장기 실행 작업의 전형적 시나리오는 다음과 같습니다: ChatGPT가 MCP 도구를 호출해 jobId를 받고, 그 jobId에 대한 진행/완료/오류 이벤트를 서버가 보냅니다. 위젯 또는 에이전트 로직은 이를 바탕으로 UI를 업데이트하고 결정을 내립니다.
시퀀스 다이어그램으로 표현하면 다음과 같습니다:
sequenceDiagram
participant User as 사용자
participant GPT as ChatGPT (모델)
participant App as GiftGenius MCP 서버
participant Widget as GiftGenius 위젯
User->>GPT: "직원 2000명을 위한 선물을 추천해줘"
GPT->>App: tools.call start_bulk_gift_analysis
App-->>GPT: response { jobId: "bulk_2025_001" }
GPT->>Widget: ToolOutput { jobId }
Widget->>Widget: 프로그레스 바 표시
App-->>GPT: notification job.started
App-->>GPT: notification job.progress (10%, 40%, 70%, 100%)
App-->>GPT: notification job.completed { resultResourceId }
GPT->>Widget: 이벤트/데이터를 위젯으로 전달
Widget->>User: 프로그레스를 갱신하고 결과 표시
실제 다이어그램은 조금 더 복잡하겠지만, 핵심은 간단합니다: MCP 이벤트는 백그라운드 작업과 사용자 경험을 잇는 “신경계”입니다.
10. MCP 이벤트 작업 시 흔한 오류
오류 №1: “이벤트 = 프로덕션 포맷의 로그”.
때때로 개발자는 이전에 console.log로 쓰던 내용을 MCP로 그대로 보내는 것으로 시작합니다. 그 결과 이벤트에는 eventId, jobId, 제대로 된 timestamp가 없고, “거의 끝났습니다” 같은 반시(半詩)적 메시지만 있습니다. 이런 접근은 시스템을 취약하게 만듭니다: 파싱하기 어렵고, 중복 제거가 불가능하며, UI는 메시지가 어떤 작업에 속하는지 알 수 없습니다. 처음부터 이벤트를 명확한 계약으로 설계하는 편이 좋습니다: 분명한 메서드 이름, 안정적인 필드 세트, 논리적인 페이로드.
오류 №2: 멱등성과 고유 eventId의 부재.
많은 사람들이 순진하게 시작합니다: “어차피 이벤트는 한 번만 오잖아.” 일주일 후에 시작됩니다: 클라이언트 재연결 시 알림이 중복되고, 사용자는 같은 내용을 두 번 받고, 커머스 백엔드는 보너스를 두 번 적립합니다. 고유 eventId와 최소한의 클라이언트 측 중복 제거 없이는 언젠가 큰 버그를 만나게 됩니다. 분산 시스템에서는 “at-least-once delivery” 모델을 전제로 해야 합니다: 중복은 피할 수 없습니다.
오류 №3: 시스템 이벤트와 비즈니스 이벤트를 한데 뒤섞음.
예를 들어 동일한 스트림에 logging/message, job.progress, job.completed, resources/updated가 type/method 경계 없이 섞여 들어옵니다. 결국 UI 레이어는 작업 완료를 파악하기 위해 if (message.includes("완료")) 같은 괴상한 코드를 쓰기 시작합니다. 더 나은 방법은 명확한 분리입니다: 시스템 알림(로그, 하트비트)과 엄격히 기술된 스키마를 가진 비즈니스 이벤트(job.*, resource.*)를 구분하세요.
오류 №4: 일관성 없는 작업 상태 전이.
어떤 스트림에서는 서버가 먼저 job.completed를 보내고, 그다음 갑자기 job.progress, 그리고 job.failed를 보냅니다. 이는 명시적 상태 머신과 이벤트 발행 시 검증이 없을 때 발생합니다. 클라이언트는 무슨 일이 일어나는지 이해할 수 없게 됩니다. 유한 상태 자동자를 명시하고 이를 깨는 이벤트를 내보내지 마세요: 예를 들어 completed 이후에는 추가 정보 이벤트 정도만 허용하고, 작업을 running으로 되돌리는 전이는 금지합니다.
오류 №5: 현재 스펙 버전의 MCP 메서드 이름에 과도하게 고정됨.
MCP 스펙은 아직 발전 중입니다. 시스템 이름의 현재 메서드에만 모든 것을 묶고 자체 네임스페이스를 고려하지 않으면, 프로토콜의 작은 변화에도 시스템 절반을 뜯어고쳐야 할 수 있습니다. 이벤트를 MCP 위의 자체 미니 스펙으로 여기는 편이 좋습니다: 기존 메서드(notifications/progress, resources/updated)를 토대로 하되, 비즈니스 이벤트(notifications/job/*)는 여러분의 네임스페이스에서 비교적 독립적으로 설계하세요.
오류 №6: 이벤트와 UX의 단절.
때로는 백엔드에서 멋진 이벤트 모델을 만들었지만, 이를 위젯까지 전달하지 못합니다: job.progress는 로그에만 존재하고, UI는 40초 동안 외로운 스피너 하나만 보여줍니다. 이런 시나리오에서 사용자는 MCP도, AI도 신뢰하지 않게 됩니다. 이벤트를 설계할 때 항상 어떤 구체적인 UI 효과를 원하는지 생각하세요: 진행률 바, 단계, 부분 결과 등. MCP 이벤트는 프로토콜을 위한 것이 아니라 이해 가능한 애플리케이션 동작을 위한 것입니다.
GO TO FULL VERSION