내부망 운영은 route↔Tool Service 매핑을 Portal이 소유하고, MCP 배포 하나가 N개 route를 서비스하며, route 하나에 N개 Tool Service가 붙을 수 있다. 이 판단을 ADR-0010으로 남기고 ADR-0007 전체와 ADR-0009 결정 4를 대체한다. 계약 - Portal-MCP registry 조회 계약 v0.1과 예제 JSON을 docs/contracts/portal-mcp/에 신설한다. 지금까지 이 경로에는 정본이 없었다. - 예제를 PortalToolRegistryClient의 실제 파싱 경로에 태우는 계약 테스트를 추가해 문서와 구현이 따로 표류하지 않게 한다. 실패 격리 - fetchAllTools()의 실패 전파를 route 단위로 격리한다. 계약 v0.2의 "aggregate는 전부 아니면 전무"는 카탈로그 하나를 전제한 규칙인데, route가 N개가 되면서 전 route로 확대돼 있었다. Tool Service 하나의 장애가 cold start에서 Pod 전체를 내리고 steady state에서 모든 route의 갱신을 멈추던 동작을 없앤다. - 제거 판단의 원천을 ToolRegistryClient.knownRoutes()로 분리한다. 조회 결과를 기준으로 지우면 이번 주기에 실패한 route의 정상 snapshot까지 사라져 "어떤 실패도 목록을 비우지 않는다" 불변식이 깨진다. - readiness는 최소 1개 route로 UP을 유지한다. 모든 route를 요구하면 정상 route까지 트래픽에서 빠져 위 격리를 되돌리기 때문이다. 대신 routesWithoutSnapshot을 health detail로 노출해 관제가 부분 상태를 감지하게 한다. 설정과 기동 - warm start가 route별 Redis key를 읽도록 확장하고, 읽을 key를 알기 위해 기동 preload 순서를 registry 조회 → warm start → manifest 조회로 바꾼다. - 어떤 코드도 읽지 않던 mcp.portal.route-key를 제거한다. route key는 요청 URI에서만 결정되며, 설정으로 보정하면 잘못된 단일 진입점 호출이 조용히 성공한다. 정리 - ToolRegistryService.java의 이중 인코딩으로 깨져 있던 한글 Javadoc 33줄을 코드 동작에 맞춰 다시 쓰고, replaceSnapshot 위에 겹쳐 있던 고아 Javadoc 블록을 지운다. - Helm chart는 mcp.bundles 구성에서 계속 유효하므로 삭제하지 않고, 내부망 운영 대상이 아니라는 사실을 deploy/README.md와 values.yaml에 명시한다. 검증: 이 환경은 loopback이 막혀 gradlew check를 실행하지 못했다. CodeStyleContract가 보는 항목(줄바꿈, 탭, 행말 공백, 파일 끝 개행, 미사용 import, import 순서)은 변경된 Java 13개 파일에 대해 따로 재현해 확인했다. 컴파일과 테스트 실행은 미확인이다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
102 lines
7.2 KiB
Markdown
102 lines
7.2 KiB
Markdown
# ADR-0007 MCP 배포 하나는 Tool Service 하나만 본다
|
||
|
||
- 상태: Superseded
|
||
- 결정일: 2026-08-02
|
||
- 대체 결정: [ADR-0010](ADR-0010-portal-owns-route-and-endpoint-registry.md)
|
||
- 관련: [ADR-0001](ADR-0001-stateless-execution-boundary.md), [ADR-0002](ADR-0002-tool-exposure-and-single-call.md), [ADR-0009](ADR-0009-container-handles-public-mcp-path.md), [계약 v0.2](../contracts/tool-service-mcp/protocol-v0.2-bundle-discovery.md)
|
||
|
||
> 이 문서는 당시 검토 이력을 보존한다. 내부망 운영은 endpoint 목록과 route 매핑의 원천을 Portal로 옮겼으므로
|
||
> 현재 구현과 신규 연동에는 [ADR-0010](ADR-0010-portal-owns-route-and-endpoint-registry.md)을 적용한다.
|
||
> 아래 격리 논거는 폐기된 것이 아니라 ADR-0010이 무엇을 포기했는지 판단하는 근거로 남는다.
|
||
|
||
외부에서 여러 MCP를 하나의 host 아래 path로 묶는 방식은 [ADR-0009](ADR-0009-container-handles-public-mcp-path.md)이
|
||
소유한다. OpenShift Route가 원래 path를 유지한 채 각각의 독립 배포로 연결하므로 이 ADR의 1:1 결정은 그대로 유지된다.
|
||
|
||
## 배경
|
||
|
||
MCP 설정의 `mcp.bundles`는 여러 Tool Service를 하나의 카탈로그로 병합할 수 있다. 이 능력을 실제로 쓸지,
|
||
즉 MCP와 Tool Service를 M:N으로 묶을지는 결정되지 않은 상태였다.
|
||
|
||
고객 요구는 **Tool의 군집화**다. MCP 자체를 군집화해 달라는 요구가 아니다. 요구의 목적은 가용성이며,
|
||
중요한 Tool은 다운이 없어야 한다는 것이다. 분할 기준은 먼저 업무로 나누고, 그 안에서 중단 시 업무
|
||
영향도와 가용성 위험도로 다시 나누는 형태다. 예: 처리계-중요, 처리계-비중요, 정보계-중요,
|
||
정보계-비중요. 여기서 위험도는 보안·권한 정책이 아니라 **서비스 중단 위험**을 뜻한다. 업무 정책은
|
||
Tool Service가 관리하며 이 배포 등급의 범위가 아니다.
|
||
|
||
Tool 목록이 확정되지 않아 Tool Service가 몇 개가 될지 모르며, 10~20개 이상이 될 수 있다.
|
||
|
||
## 결정
|
||
|
||
1. **MCP 배포 하나는 Tool Service를 정확히 하나 본다.** `mcp.bundles`는 항상 한 항목이다.
|
||
2. 배포 단위의 분할 축은 **업무 × 등급(tier)** 이다. 등급은 `critical`과 `standard`로 둔다.
|
||
3. 등급은 **배포 속성일 뿐 wire 계약에 나타나지 않는다.** Tool 이름·`namePrefix`·매니페스트에 등급을 넣지 않는다.
|
||
4. 다중 bundle 병합 코드는 **삭제하지 않고 유지**하되, 배포 설정에서 bundle 1개로 잠근다.
|
||
|
||
## 근거
|
||
|
||
### 등급이 다른 Tool Service를 한 MCP가 보면 격리가 깨진다
|
||
|
||
`ToolBundleRegistryClient.fetchTools()`는 사용 가능한 성공본이 없는 bundle이 하나라도 있으면
|
||
카탈로그 전체 교체를 거부한다(계약 v0.2 §1, §7). 한 MCP가 중요·비중요 Tool Service를 함께 보면
|
||
**비중요 쪽 조회가 확정되지 않는 동안 중요 Tool의 카탈로그 갱신까지 멈춘다.** 직전 성공본으로
|
||
서빙은 계속되지만 변경 반영은 막힌다.
|
||
|
||
여기에 두 Tool Service 호출이 같은 프로세스의 HTTP connection pool과 스레드를 공유하므로,
|
||
비중요 쪽 지연이 중요 쪽 여유를 잠식한다.
|
||
|
||
**병합은 가용성 요구와 정면으로 충돌한다.** 등급을 나눈 목적을 배포 구조가 되돌려 놓는다.
|
||
|
||
### 병합해서 얻는 것이 없다
|
||
|
||
MCP에는 업무 로직이 없다. 여러 Tool Service를 하나로 합치는 일이 MCP 안에서 일어나야 할
|
||
기술적 이유가 없다. Agent Builder는 MCP를 개별 등록하면서 하위 Tool 정보를 자기 DB에 저장하고,
|
||
사용자 요청을 판단한 뒤 **해당 Tool을 가진 MCP로 호출을 보낸다.** 여러 Tool 묶음을 아우르는 일은
|
||
Tool 선택을 이미 수행하는 Agent Builder 계층에서 끝난다.
|
||
|
||
런타임에 공유되는 공통 Tool Service도 없다. Tool 파트의 `tool-common`은 각 Tool Service 프로젝트가
|
||
함께 빌드하는 **빌드 타임 라이브러리**이지 별도로 뜨는 서비스가 아니다. 1:1을 깨야 할 사례가 남지 않는다.
|
||
|
||
### 1:1이라야 등급별로 다른 비용을 쓸 수 있다
|
||
|
||
한 MCP가 등급을 섞어 들고 있으면 그 배포 전체에 중요 등급 기준을 적용해야 한다. 나뉘어 있으면
|
||
`critical`에만 replica 여유와 PodDisruptionBudget을 주고 `standard`는 최소로 둘 수 있다.
|
||
**분할의 실질 이득은 격리 자체보다 여기에 있다.**
|
||
|
||
다만 배포를 나누는 것만으로 가용성이 생기지는 않는다. 같은 노드 배치, 같은 namespace의 쿼터,
|
||
공통 Redis·클러스터 장애는 분할로 막히지 않는다. 등급 분리가 의미를 가지려면 replica 하한,
|
||
PodDisruptionBudget, anti-affinity와 usable Tool snapshot 기반 readiness가 함께 가야 한다. Chart의
|
||
`tiers` 설정과 `HelmDeploymentContractTest`가 test·prod의 정적 values를 검사하며, 실제 렌더링 결과는
|
||
배포 파이프라인의 `helm lint`와 `helm template`이 확인한다.
|
||
|
||
## 전제
|
||
|
||
아래가 깨지면 이 결정을 재검토한다.
|
||
|
||
1. Agent Builder는 MCP를 개별 등록하고, 한 Agent가 여러 MCP의 Tool을 사용할 수 있다.
|
||
2. Tool 호출은 한 요청에 하나이며([ADR-0002](ADR-0002-tool-exposure-and-single-call.md)) 그 Tool을 가진 MCP로 직접 간다.
|
||
3. 런타임에 공유되는 공통 Tool Service가 없다.
|
||
|
||
## 영향
|
||
|
||
- **배포 수 = Tool Service 수**다. 10~20개를 전제로 Helm values는 토폴로지를 한 파일에 모으고
|
||
배포 시 `deploymentKey`로 하나를 고른다. 배포가 늘어도 파일 수는 변하지 않는다.
|
||
- 계약 v0.2 §7의 병합 규칙 중 bundle 간 이름 충돌(6번)과 `maxToolsTotal`(3번 후단)은 운영에서 발동하지 않는다.
|
||
규칙 자체는 계약에 남는다.
|
||
- **Tool 이름의 전역 유일성은 Tool Service 책임으로 남는다.** 서로 다른 MCP가 같은 `namePrefix`를
|
||
쓰는 것을 MCP는 막지 못한다. 등급으로 나뉜 두 배포가 같은 업무 prefix(`processing.`)를 공유하는 것은
|
||
의도된 구성이며, 그 안에서 Tool 이름이 겹치지 않아야 한다.
|
||
- Tool을 다른 등급으로 옮기면 그 Tool을 제공하는 **MCP endpoint가 바뀐다.** Agent Builder가 Tool 정보를
|
||
DB에 보관하므로 반영에는 재등록 또는 다음 `tools/list` 주기가 필요하다. 등급은 자주 바꾸지 않는 값으로 다룬다.
|
||
- [ADR-0002](ADR-0002-tool-exposure-and-single-call.md)의 Tool 노출 상한 50개는 한 Agent가 여러 MCP에서
|
||
가져온 Tool의 **합계**에 적용된다. MCP를 나눈다고 상한이 늘지 않는다.
|
||
|
||
## 채택하지 않은 대안
|
||
|
||
**M:N — 한 MCP가 여러 Tool Service를 본다.** 배포 수는 줄지만 위의 격리 문제가 그대로 남는다.
|
||
가용성이 분할의 목적이므로 목적과 수단이 어긋난다.
|
||
|
||
**코드에서 bundle 1개를 강제한다.** `McpProperties`에 검증을 넣으면 다중 bundle 병합 코드가
|
||
도달 불가능해진다. 전제 3이 깨질 때 되돌리는 비용이 커지고, 이미 작성·테스트된 경로를 죽은 코드로
|
||
만든다. 1:1은 애플리케이션 불변식이 아니라 **배포 결정**이므로 배포 정의에서 잠그는 편이 맞다.
|
||
이 선택은 검증 위치를 옮긴 것이지 검증을 뺀 것이 아니다.
|