CodeGym /행동 /ChatGPT Apps /MCP Gateway와 현지화 아키텍처: 단일언어 서버, 매개변수로서의 locale, 클라이언트 상태

MCP Gateway와 현지화 아키텍처: 단일언어 서버, 매개변수로서의 locale, 클라이언트 상태

ChatGPT Apps
레벨 9 , 레슨 4
사용 가능

1. 왜 현지화 아키텍처를 고민해야 할까

언어가 하나이고 카탈로그가 작을 때는 간단합니다. gift_catalog.json에 보관하고, 모든 텍스트는 러시아어, MCP 서버는 그 선물들을 모두에게 그대로 반환하면 됩니다. 하지만 다음을 원하기 시작하는 순간:

  • 미국과 유럽용 영어 UI,
  • 마트료시카와 러시아어 서적이 담긴 별도의 러시아어 카탈로그,
  • 서로 다른 마켓(미국은 Amazon, 러시아는 Ozon),

나이브한 접근인 ‘각 핸들러마다 하나 더 추가하는 if(locale === "ru")’는 코드가 금세 크리스마스트리처럼 변하게 만듭니다.

MCP는 한편으론 프로토콜이자, 다른 한편으로는 그 프로토콜의 서버 구현입니다. 서버는 ChatGPT로부터 localeuserLocation을 포함한 메타데이터와 함께 요청을 받습니다. 핵심은 “locale을 읽을 줄 아느냐”가 아니라, 아키텍처의 정확히 어디에서 그 신호를 고려할 것이냐입니다. 각 도구에서 처리할 수도 있고, 일부 로직을 별도 계층 — Gateway — 로 분리할 수도 있습니다.

좋은 현지화 아키텍처는 다음 세 가지에 답해야 합니다.

  1. 어떤 언어와 지역을 사용할지 어디에서 결정하는가.
  2. 어디에서 필요한 데이터와 통합(카탈로그, 상점 API, 통화)을 선택하는가.
  3. 사용자 상태(locale, 통화, 기타 선호)를 어디에 어떻게 저장해, 매 호출마다 수동으로 넘기지 않게 할 것인가.

이제 이를 차근차근 살펴보겠습니다.

2. MCP, _meta, 그리고 stateless 특성: 왜 locale을 명시적으로 전달해야 하는가

어디에서 locale을 고려할지 결정하기 전에, MCP 요청이 프로토콜 수준에서 어떻게 생겼는지와 플랫폼이 이미 어떤 메타데이터를 전달하는지 떠올려 봅시다.

중요한 사실을 기억해 둡시다. MCP 요청은 JSON‑RPC 메시지입니다. 각 메시지는 독립적이며, 프로토콜은 상태 유지 세션을 강제하지 않습니다. 따라서 서버가 로케일을 고려하도록 하려면 다음 중 하나를 수행해야 합니다.

  • 도구의 인자(localeinputSchema에 추가)로 명시적으로 전달하거나,
  • ChatGPT가 요청에 추가하는 _meta["openai/locale"]에서 읽습니다.

다음은 _meta에서 locale을 읽는 가장 단순한 핸들러 예시입니다.

server.registerTool(
  "suggest_gifts",
  {
    title: "Suggest gifts",
    inputSchema: { /* ... */ },
  },
  async (args, extra) => {
    const meta = extra?._meta ?? {};
    const locale = (meta["openai/locale"] as string | undefined) || "en-US";
    const country = meta["openai/userLocation"]?.country as string | undefined;

    // 이후 locale과 country를 사용해 카탈로그를 선택
    const gifts = await loadGiftCatalog(locale, country);
    return { structuredContent: { gifts } };
  }
);

여기서는 locale을 인자로 넘기지 않고, SDK가 이미 extra에 넣어 준 _meta에 의존합니다. 충분히 실용적인 방식이며, 첫 번째 모델(하나의 다국어 MCP)에서 유용합니다.

두 번째 모델(Gateway)에서도 _meta는 핵심 역할을 합니다. 게이트웨이가 메타데이터에서 locale을 읽고, 그에 따라 요청을 어디로 보낼지 결정합니다. locale을 어떤 형태로 보관할지 — 오직 _meta에 둘지, 도구 스키마에도 둘지 — 는 아래에서 따로 살펴보겠습니다.

3. 모델 1: 단일 다국어 MCP 서버(‘폴리글롯 모놀리스’)

가장 단순한 아키텍처부터 시작해 봅시다. MCP 서버 하나, URL 하나, 배포 한 번, 코드베이스 하나입니다. 각 도구 내부에서는 다음을 수행합니다.

  1. _meta나 인자에서 locale을 받는다.
  2. locale에 따라 적절한 리소스를 고른다: gift_catalog.en.json, gift_catalog.ru.json 등.
  3. 결과를 해당 언어로 반환한다.

GiftGenius 예시

두 개의 카탈로그 파일이 있다고 가정해봅시다.

  • data/gift_catalog.en.json
  • data/gift_catalog.ru.json

필요한 파일을 고르는 작은 헬퍼 loadGiftCatalog(locale)를 만들어 봅시다.

async function loadGiftCatalog(locale: string) {
  const lang = locale.split("-")[0]; // "en-US" → "en"
  const fileName = lang === "ru" ? "gift_catalog.ru.json" : "gift_catalog.en.json";
  const data = await import(`../data/${fileName}`);
  return data.default; // 선물 배열
}

이제 도구 suggest_gifts에서 이 헬퍼를 호출하면 됩니다.

server.registerTool(
  "suggest_gifts",
  { title: "선물 추천", inputSchema: {/* ... */} },
  async (args, extra) => {
    const locale = (extra?._meta?.["openai/locale"] as string) || "en-US";
    const catalog = await loadGiftCatalog(locale);
    const filtered = filterGifts(catalog, args);
    return { structuredContent: { gifts: filtered } };
  }
);

결국 현지화 로직은 loadGiftCatalog 한 곳에 숨어 있고, 도구들은 그저 locale을 전달하기만 하면 됩니다. 날짜/통화 포맷 등 다른 지역 의존 항목도 동일한 방식으로 다룰 수 있습니다.

이 모델의 장단점

글이 길어지지 않도록, 첫 번째 모델의 장단점을 작은 표로 정리해 보겠습니다(아직 Gateway와의 비교는 뒤에서).

기준 단일 다국어 MCP
MCP 인스턴스 수 1
locale을 고려하는 위치 각 도구 코드 내부
배포 및 스케일링 간단함, 단일 지점
카탈로그 현지화 조건부 파일/요청 로딩으로 처리
if(locale ... ) 코드량 점점 많아짐
다양한 시장/API 지원 이질적 요소가 한 코드베이스에 뒤섞임

이 모델이 특히 잘 맞는 경우:

  • MVP나 작은 앱으로 언어가 2–3개이고 시장 차이가 크지 않은 경우,
  • 학습용 프로젝트(예: 이 강의의 GiftGenius 샘플).

다음과 같은 경우에는 적합하지 않습니다.

  • 지원 언어가 많아지는 경우,
  • 시장별 팀과 데이터가 근본적으로 다른 경우(별도 DB, 각기 다른 e‑commerce API, 법적 요구사항 등).

바로 이런 상황에서 두 번째 모델이 등장합니다.

4. 모델 2: MCP Gateway + 단일언어 백엔드 서버

이제 GiftGenius가 미국, 러시아, 독일에서 동작한다고 가정해 봅시다. 미국은 Amazon API, 러시아는 Ozon, 독일은 로컬 리테일러를 사용합니다. 시장마다 계약, 특징, 담당 팀이 다릅니다. 이를 단일 MCP 모놀리스에 모두 밀어 넣는 건 불편합니다.

모델 2의 아이디어는 다음과 같습니다.

ChatGPT와 실제 MCP 서비스들 사이에 Gateway를 둡니다. ChatGPT 입장에서는 Gateway도 MCP 서버 하나일 뿐이며, 내부적으로는 요청을 서로 다른 백엔드 서버로 라우팅합니다. 각 백엔드는 “한 언어”만 다루고 “한 시장”만 상대합니다.

다이어그램으로 보면

먼저 두 모델을 비교해 봅시다.

flowchart LR
    subgraph Model1["모델 1: 단일 MCP"]
      A1[ChatGPT] --> B1["GiftGenius MCP (다국어)"]
    end

    subgraph Model2["모델 2: Gateway + 단일언어"]
      A2[ChatGPT] --> G[MCP Gateway]
      G --> R["GiftGenius MCP RU (ru-RU, Ozon)"]
      G --> E["GiftGenius MCP EN (en-US, Amazon"]
      G --> D["GiftGenius MCP DE (de-DE, Local shop)"]
    end

두 번째 모델에서 ChatGPT가 바라보는 MCP 엔드포인트는 Gateway 하나뿐입니다. 내부적으로 Gateway는 _meta["openai/locale"] 및/또는 _meta["openai/userLocation"]을 분석해 적절한 백엔드를 선택합니다.

이 강의 맥락에서 Gateway가 하는 일

Gateway가 모든 비즈니스 로직을 품은 “두 번째 모놀리트”가 되지 않도록 주의해야 합니다. 이 모듈에서 Gateway의 역할은 매우 제한합니다.

  1. ChatGPT로부터 MCP 메시지를 수신한다(_meta 포함).
  2. locale / userLocation을 꺼낸다.
  3. 그에 따라 적절한 백엔드 서버를 선택한다.
  4. 요청을 해당 서버로 프록시(JSON‑RPC)하고 응답을 그대로 반환한다.

어떤 선물 카탈로그를 쓸지, Amazon이나 Ozon을 어떻게 호출할지는 각 언어별 MCP 서버 내부의 일입니다. Gateway는 “완벽한 장모님 선물”이 무엇인지 알 필요가 없습니다. ru-RUmcp-giftgenius-ru, en-USmcp-giftgenius-en으로 보내면 충분합니다.

TypeScript로 보는 아주 단순한 MCP Gateway 스켈레톤

세부에 빠지지 않기 위해 많이 단순화하겠습니다. 내부 MCP 서버들과 JSON‑RPC로 통신하는 헬퍼 callDownstreamTool이 있다고 가정합니다(HTTP 요청이나 지속 SSE 연결일 수도 있지만, 자세한 내용은 모듈 16에서).

import { Server } from "@modelcontextprotocol/sdk/server";

const server = new Server({ name: "giftgenius-gateway" });

function chooseBackend(locale?: string) {
  if (!locale) return "en";              // 기본값
  const lang = locale.split("-")[0];     // ru-RU → ru
  return ["ru", "de"].includes(lang) ? lang : "en";
}

server.registerTool(
  "suggest_gifts",
  { title: "Suggest gifts (via gateway)", inputSchema: {/* ... */} },
  async (args, extra) => {
    const locale = extra?._meta?.["openai/locale"] as string | undefined;
    const backendKey = chooseBackend(locale); // "ru" | "en" | "de"
    // 적절한 백엔드 서버에서 동일한 도구를 호출
    return await callDownstreamTool(backendKey, "suggest_gifts", args, extra);
  }
);

내부 MCP 서버들은 동일한 계약으로 suggest_gifts를 등록하지만, 각자 자신의 언어/시장만 처리하며, 다른 언어의 존재를 알 필요가 없습니다.

동일하게 Gateway는 listTools, listResources 등 다른 MCP 메서드도 프록시할 수 있지만, 이는 별도 모듈의 주제입니다.

5. 현지화를 위한 두 모델 비교

앞서 “단일 MCP” 모델의 장단점을 따로 보았습니다. 이제 두 모델의 차이를 주요 항목별로 정리해 보겠습니다.

기준 단일 다국어 MCP Gateway + 단일언어 MCP 서버
MCP 서비스 수 1 1 Gateway + N개의 백엔드 서버
locale을 고려하는 위치 각 도구 내부(if locale ... 로직) 라우팅하는 Gateway에서; 서비스 내부 언어는 고정
UX 유연성(언어 전환) 쉬움, 한 곳에서 처리, LLM이 locale만 바꾸면 됨 가능하지만 Gateway의 백엔드 전환 방식을 설계해야 함
인프라 복잡도 최소 상대적으로 높음: 언어별 별도 배포 필요
시장별 격리 낮음: 단일 코드/프로세스 높음: RU 서버 장애가 EN에 영향 없음(반대도 동일)
팀 분리/소유권 책임 분리가 어려움 자연스러움: RU/EN/DE 팀이 각자 MCP를 독립 개발
코드 내 현지화 로직 각 핸들러의 비즈니스 로직과 뒤섞임 Gateway와 각 백엔드 경계 내로 집중

이 강의에서는 주로 모델 1(단일 MCP + locale을 파라미터로)을 사용하고, Gateway 모델은 여러 시장이 있는 “진짜 서비스”로 확장할 때의 자연스러운 다음 단계로 봅니다. 그럼에도 Gateway가 자연스러운 스텝인 만큼, 이 아키텍처의 중요한 디테일 하나 — 세션 상태에 locale과 국가를 어떻게 보관할지 — 를 살펴보겠습니다.

6. Gateway에서 클라이언트 상태의 일부로서의 locale

지금까지는 각 요청에 필요한 모든 정보가 들어 있다고 가정했습니다. 하지만 실제로는 일부 정보를 세션 상태로 보관하는 게 편리합니다. 예를 들어:

  • 사용자가 한 번 locale = "ru-RU", userLocation.country = "RU"로 들어왔다면,
  • 그 다음부턴 일부 요청에 인자에 locale이 명시되지 않더라도 RU 백엔드로 라우팅하고 싶을 수 있습니다.

MCP에는 유용한 필드 _meta["openai/subject"]가 있습니다. OpenAI가 서비스로 보내는 익명 사용자 식별자이며, 세션 키로 사용할 수 있습니다.

메모리 기반의 간단한 상태 구현

Gateway에 아주 작은 상태 레이어를 작성해 보겠습니다(프로덕션에서는 Map 대신 Redis나 외부 저장소를 권장).

type ClientState = {
  locale?: string;
  country?: string;
};

const clientState = new Map<string, ClientState>();

function getClientId(extra: any): string | undefined {
  return extra?._meta?.["openai/subject"] as string | undefined;
}

function updateClientState(extra: any) {
  const clientId = getClientId(extra);
  if (!clientId) return;

  const meta = extra?._meta ?? {};
  const current = clientState.get(clientId) ?? {};
  const next: ClientState = {
    locale: meta["openai/locale"] || current.locale,
    country: meta["openai/userLocation"]?.country || current.country,
  };
  clientState.set(clientId, next);
}

이제 Gateway 핸들러에서 먼저 상태를 갱신하고, 이후 백엔드 선택에 사용할 수 있습니다.

server.registerTool(
  "suggest_gifts",
  { title: "Suggest gifts (via gateway)", inputSchema: {/* ... */} },
  async (args, extra) => {
    updateClientState(extra);
    const clientId = getClientId(extra)!;
    const state = clientState.get(clientId);
    const locale = state?.locale || "en-US";

    const backendKey = chooseBackend(locale);
    return await callDownstreamTool(backendKey, "suggest_gifts", args, extra);
  }
);

이렇게 하면 한 번만 clientIdlocale, country를 “기억”해 두고, 이후 도구 호출에서 인자에 같은 필드를 매번 복사하지 않아도 됩니다.

같은 방식으로 Gateway는 선호 통화, 가격 포맷 등 커머스 로직에 유용한 설정을 기억할 수 있습니다(자세한 내용은 ACP 모듈에서).

7. GiftGenius: 두 가지 시나리오와 아키텍처 선택의 영향

추상적 네모만 이야기하는 것처럼 느껴지지 않도록, GiftGenius의 구체적 시나리오를 살펴보겠습니다.

시나리오 1: 러시아 사용자, 러시아어로 입력

다음과 같다고 합시다.

  • _meta["openai/locale"] = "ru-RU",
  • _meta["openai/userLocation"].country = "RU".

사용자 입력: "동료에게 줄 선물을 골라줘요. 보드게임을 좋아하고, 예산은 3,000 루블입니다."

모델 1(단일 MCP)에서는:

  1. 핸들러가 _meta에서 locale을 읽어 "ru-RU"를 얻습니다.
  2. gift_catalog.ru.json을 로드합니다. 모든 이름은 러시아어이고, 가격은 루블입니다.
  3. 카테고리와 예산으로 필터링해 러시아어로 된 선물 목록을 구조화해 반환합니다.

모델 2(Gateway + 단일언어)에서는:

  1. Gateway가 locale과 userLocation을 읽고 RU 사용자라고 판단합니다.
  2. suggest_gifts 호출을 mcp-giftgenius-ru로 보냅니다.
  3. 해당 서버는 러시아어 카탈로그와 Ozon API만 사용해, 루블로 된 선물을 반환합니다.

두 경우 모두 사용자는 모국어로 결과를 보지만, 두 번째 모델에서는 영어 MCP 서버가 러시아 카탈로그의 존재를 아예 몰라도 됩니다.

시나리오 2: 독일 사용자, 영어로 입력

이제 다음과 같습니다.

  • _meta["openai/locale"] = "en",
  • _meta["openai/userLocation"].country = "DE".

사용자 입력: "Gift for my German coworker, budget 50 EUR".

모델 1:

  • locale "en"이면 텍스트는 영어,
  • country "DE"는 유럽 가격(유로)과 상품 구성을 선택하는 데 사용할 수 있습니다.

모델 2:

  • Gateway는 locale = "en" → 영어 서비스, country = "DE" → 유럽 재고 기반 상품으로 판단할 수 있습니다. 비즈니스 로직에 따라 다음 중 하나를 선택할 수 있습니다.
  • mcp-giftgenius-en으로 보내되, country=DE 파라미터를 추가하거나,
  • 유럽 전용 mcp-giftgenius-eu를 별도로 운영합니다.

즉, 로케일(언어)과 지역(userLocation)은 서로 다른 차원이며, Gateway는 “어떤 서비스를 호출하고 어떤 상품을 보여줄지” 결정을 조합하기에 좋은 위치입니다.

8. 도구 스키마의 locale vs _meta의 locale만 사용

단일 MCP를 쓰든 Gateway + 단일언어 서비스를 쓰든, 마지막으로 중요한 질문 하나가 남습니다. locale을 _meta에만 둘 것인가, 아니면 도구의 인자로도 명시할 것인가?

접근은 두 가지입니다.

첫째: _meta에만 의존합니다.

도구 스키마가 또 다른 필드로 지저분해지지 않는다는 장점이 있습니다. 서버는 extra._meta에서 locale을 읽고 자체적으로 결정합니다. 모델 1에서는 이 방식만으로도 충분한 경우가 많습니다.

둘째: inputSchemalocale(그리고 필요하면 currency)을 명시적으로 추가합니다.

const suggestGiftsSchema = {
  type: "object",
  properties: {
    locale: {
      type: "string",
      description: "User locale in BCP 47 format, e.g. en-US or ru-RU"
    },
    recipient: { type: "string" },
    // ...
  },
  required: ["recipient"]
};

그 다음 system 프롬프트에서, 사용자 컨텍스트의 값을 사용해 항상 locale 인자를 채우도록 모델에 요청할 수 있습니다. 이렇게 하면 의도가 더 투명해집니다. JSON 인자에 서버가 어떤 언어로 동작해야 하는지가 드러나기 때문입니다. 이 방식은 특히 복잡한 아키텍처(공용 MCP 하나가 내부적으로 locale에 따라 다양한 서비스/리소스로 라우팅하는 경우)에서 유용합니다.

실무에서는 두 방식을 자주 병행합니다. 스키마에 locale 필드가 있지만, 모델이 채우지 않았을 경우 서버가 _meta["openai/locale"]로 보완합니다.

9. Gateway에서 ‘현지화’와 ‘과도한 로직’의 경계

쉽게 빠지는 함정이 있습니다. “어차피 똑똑한 Gateway가 있으니 다음도 거기서 하자”는 생각입니다.

  • 어떤 선물을 보여줄지 자체적으로 결정하고,
  • 날짜와 가격 포맷을 자체적으로 처리하고,
  • 클릭 리포트를 자체적으로 모으는 등.

그럴듯해 보이지만 Gateway를 “두 번째 모놀리트”로 만들고, 업데이트와 운영을 복잡하게 합니다. 산업계의 API Gateway 관행(역할상 MCP Gateway도 동일)에서는 인증/인가/라우팅/가벼운 컨텍스트 보강에 집중합니다. 비즈니스 로직과 무거운 작업은 백엔드 서비스에 있어야 합니다.

현지화 관점에서 이는 다음을 의미합니다.

  • Gateway는 _meta["openai/locale"]와 _meta["openai/userLocation"]을 파싱할 수 있습니다.
  • 이를 클라이언트 상태에 기억할 수 있습니다.
  • 적절한 언어 서버를 선택하거나 요청에 locale/country 필드를 덧붙일 수 있습니다.

하지만 선물 추천, 연령/예산 필터링 등의 비즈니스 로직은 MCP 백엔드에 남겨야 합니다.

10. MCP와 Gateway로 현지화를 설계할 때 흔한 실수

실수 1: 사용자 텍스트의 언어 추론에만 의존한다.
가끔 사용자 메시지를 언어 감지기에 넣고 그 결과만으로 어떤 서버를 호출할지 결정하고 싶을 수 있습니다. 보조 수단으로는 유용하지만, 주된 메커니즘이어서는 안 됩니다. 플랫폼은 이미 openai/localeopenai/userLocation을 제공합니다. 이는 ChatGPT 설정과 사용자 환경을 반영합니다. 이 신호를 무시하고 언어를 “맞히려는” 시도는 예상치 못한 순간 UX를 망가뜨리는 지름길입니다.

실수 2: locale을 모델 머릿속에만 두고 서버로 전달하지 않는다.
locale_meta에도, 도구 인자에도 전혀 나타나지 않으면, 서버는 사용자 언어를 알 수 없습니다. 모델이 카테고리 이름을 임의로 영어로 번역하려고 시도할 수 있지만, 특히 복잡한 카테고리에서는 신뢰할 수 없습니다. 올바른 방법은 locale을 명시적으로 전달하는 것입니다. 인자 locale로 전달하거나 _meta에서 읽고, 그에 맞춘 아키텍처를 구성하세요.

실수 3: 현지화 관련 비즈니스 로직을 전부 Gateway로 옮긴다.
Gateway가 직접 선물을 고르고, DB를 조회하고, 외부 API와 싸우기 시작하면, 더 이상 가벼운 라우터가 아니라 규모가 큰 서비스를 하나 더 만든 셈이 됩니다. 스케일링과 업데이트가 어려워지고, 모놀리트가 둘로 늘어납니다. Gateway는 최대한 “단순하게” 유지하세요. locale/userLocation을 보고 백엔드를 고르고, 메타데이터를 깔끔하게 전달하는 수준이 적절합니다.

실수 4: 라우팅을 IP나 userLocation에만 경직되게 묶는다.
“국가가 RU면 RU 서버로”처럼 단순화하고 싶을 때가 있습니다. 하지만 사용자가 독일에 있어도 러시아어 인터페이스를 원할 수 있고, 세션 중간에 “영어로 전환”을 요청할 수도 있습니다. Gateway에서 openai/locale을 고려하지 않거나, 사용자가 언어를 바꾸려는 의도를 반영할 수 없다면 라우팅은 “콘크리트”처럼 굳어 UX를 해치게 됩니다. locale과 userLocation의 조합에 의존하고, 세션 상태를 통해 재정의할 수 있는 여지를 두는 것이 좋습니다.

실수 5: _meta["openai/subject"]를 쓰지 않고 모든 파라미터를 매 호출 인자에 중복한다.
각 도구 인자에 locale, country, currency, userId 등 온갖 필드를 매번 넣기 시작하면, 금세 삶이 괴로워집니다. MCP는 이미 _meta["openai/subject"]를 통해 익명 사용자 ID를 전달합니다. 이를 사용해 Gateway나 백엔드에서 클라이언트 상태에 필요한 정보를 저장하면, 계약이 단순해지고 인자 불일치 위험이 줄어듭니다.

실수 6: 진화 전략 없이 “처음부터 10개 언어의 거대한 Gateway”를 만든다.
처음부터 완벽하게 만들고 싶은 유혹이 큽니다. Gateway, 5개 언어, 3개 지역, 10개의 MCP 서비스처럼요. 현실적으로는 “단일 MCP + locale 파라미터 또는 _meta” 모델로 시작해 동작을 안정화한 후, 성장에 맞춰 Gateway와 단일언어 서비스를 분리하는 편이 쉽습니다. 처음부터 거대한 “동물원”을 만들려는 시도는 출시를 지연시키고 디버깅을 어렵게 만듭니다.

1
설문조사/퀴즈
현지화, 레벨 9, 레슨 4
사용 불가능
현지화
현지화 (UI, 데이터, 기능 설명)
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION