[MCP] MCP 응답 토큰 사용량 최적화하기
1. MCP 응답이 커진 이유
한국 세법 자료를 검색하고 조회하는 korean-taxlaw-mcp를 운영하면서 응답에 비슷한 내용이 반복되는 문제가 있었다.
MCP 도구의 응답은 결국 모델의 입력 컨텍스트에 포함된다. 사용자에게 필요한 결과는 몇 줄인데, 같은 본문이나 메타데이터를 여러 필드에 담아 보내면 그만큼 토큰을 불필요하게 사용한다. 한 번의 호출에서는 차이가 작아 보여도 검색 결과를 받은 뒤 상세 문서를 연이어 조회하는 흐름에서는 중복이 계속 쌓인다.
실제 응답을 확인해 보니 다음과 같은 문제가 있었다.
summary와gist에 같은 내용이 들어갔다.fullText와sections에 문서 본문이 중복으로 담겼다.- 검색 결과마다 동일한 출처와 분류 정보가 반복됐다.
- 문서를 찾은 경우에도 검색 진단 정보가 항상 포함됐다.
null, 빈 배열, 빈 객체처럼 의미 없는 값도 그대로 직렬화됐다.- 기본 검색 개수가 많아 사용하지 않을 후보까지 한 번에 반환했다.
이번 작업에서는 도구의 개수와 역할을 바꾸지 않고 응답 구조와 호출 흐름을 정리했다. 기존 12개 도구의 이름과 입력 파라미터는 유지하면서, 모델이 실제로 사용할 정보만 전달하는 것이 목표였다.
2. 같은 내용을 두 번 보내지 않기
가장 먼저 한 일은 응답 안에서 같은 의미를 가진 필드를 찾는 것이었다.
기존 상세 응답은 요약을 summary와 gist에 각각 넣고, 문서 본문도 fullText와 sections에 함께 담았다. 출처 정보 역시 상위 필드와 citation 객체에 중복으로 들어갔다.
{
"summary": "결정 내용 요약",
"gist": "결정 내용 요약",
"fullText": "문서 전체 본문",
"sections": [
{"title": "주문", "text": "문서 전체 본문의 일부"},
{"title": "이유", "text": "문서 전체 본문의 일부"}
],
"sourceUrl": "https://example.com/document",
"citation": {
"sourceUrl": "https://example.com/document"
}
}
sections와 문서 서두인 preamble만으로 본문을 빠짐없이 복원할 수 있다면 fullText는 전달하지 않아도 된다. 반대로 문서 구조를 제대로 나누지 못한 경우에는 내용이 사라지지 않도록 fullText를 남긴다.
{
"summary": "결정 내용 요약",
"preamble": "문서 서두",
"sections": [
{"title": "주문", "text": "주문 내용"},
{"title": "이유", "text": "판단 이유"}
],
"sourceUrl": "https://example.com/document"
}
무조건 필드를 없애기보다 다른 필드가 같은 정보를 온전히 담고 있는지 먼저 확인했다. 이 기준을 적용해 gist, 중복된 상세 필드, 별도로 만들던 citation 정보를 정리했다.
3. 검색 결과와 상세 문서 분리하기
검색 단계에서 필요한 것은 문서 전체가 아니라 어떤 문서를 열어 볼지 판단할 수 있는 정보다. 제목, 문서 번호, 날짜, 짧은 요약 정도면 충분하다.
기존에는 검색 결과에도 상세 데이터가 많이 포함돼 있었다. 결과를 10개 받으면 선택하지 않은 9개 문서의 상세 정보까지 모델 컨텍스트에 들어갔다. 이를 다음 두 단계로 나눴다.
1. search_tax_interpretations
→ 문서 번호, 제목, 날짜, 짧은 요약을 최대 10개 반환
2. get_tax_document
→ 선택한 1~3개 문서의 본문을 조회
문서 번호를 이미 알고 있다면 검색 단계를 거칠 필요가 없다.
lookup_tax_document
→ 정확한 문서 번호로 상세 문서 한 건 조회
통합 검색 도구인 search_taxlaw도 정확히 일치하는 결과를 찾을 때 먼저 metadata_only 형태로 확인하도록 바꿨다. 문서의 존재 여부를 확인하는 단계에서 본문 전체를 가져오지 않기 위해서다.
검색 결과의 기본 개수도 일반 검색은 20개에서 10개로, 안내·서식 검색은 40개에서 20개로 줄였다. 더 많은 결과가 필요한 경우에는 호출하는 쪽에서 limit을 늘릴 수 있다.
4. 반복되는 값은 상위로 올리기
안내 자료와 서식 목록에서는 모든 항목에 동일한 값이 반복됐다. 예를 들어 검색어, 자료 유형, 출처 같은 정보가 결과 40개에 각각 들어가면 같은 문자열만 수십 번 전송된다.
{
"items": [
{"query": "양도소득세", "type": "guide", "title": "자료 1"},
{"query": "양도소득세", "type": "guide", "title": "자료 2"}
]
}
항목마다 달라지는 값만 items에 남기고 공통 값은 응답 상위로 한 번만 올렸다.
{
"query": "양도소득세",
"type": "guide",
"items": [
{"title": "자료 1"},
{"title": "자료 2"}
]
}
결과 개수가 많을수록 이 차이는 커졌다. 안내 자료 검색 응답은 36,295자에서 4,759자로 줄었고, 안내 목차 응답은 39,005자에서 1,653자로 줄었다.
5. 성공 응답에는 성공에 필요한 정보만 담기
정확한 문서 번호로 조회할 때도 검색 과정에서 생성한 진단 정보가 항상 포함돼 있었다. 문서를 정상적으로 찾은 경우에는 사용자에게 필요하지 않은 내부 정보였다.
진단 정보는 NOT_FOUND일 때만 반환하도록 바꿨다. 문서를 찾지 못했을 때는 어떤 조건으로 검색했는지, 비슷한 문서가 있는지 알아야 다음 행동을 정할 수 있다. 반면 정상 조회에서는 문서 내용만 있으면 된다.
비슷한 문서 후보도 최대 5개로 제한하고 제목, 문서 번호처럼 다시 조회하는 데 필요한 필드만 남겼다. 내부 처리에 쓰는 ID는 응답에서 제외했다.
tax_research 응답도 같은 방식으로 정리했다. 법령, 시행령, 시행규칙을 각각 별도의 계층으로 반복해 보여 주던 구조를 하나의 법령 목록으로 합치고, 결과마다 붙던 긴 안내 문구는 상위에서 한 번만 전달하도록 줄였다.
6. JSON 직렬화 비용 줄이기
응답 내용뿐 아니라 JSON을 만드는 방식도 함께 정리했다.
- 들여쓰기와 불필요한 공백을 제거해 compact JSON으로 직렬화했다.
null, 빈 문자열, 빈 배열, 빈 객체를 재귀적으로 제거했다.- 모든 결과에 반복되던 검색 방식 설명과 로컬 자료 안내 문구를 줄였다.
- 도구 설명은 핵심 사용법만 남겨 전체 스키마 크기를 줄였다.
예를 들어 같은 객체라도 모델이 구조만 파악하면 되는 MCP 응답에서는 두 번째 형태로 충분하다.
{
"documentNo": "사전-2026-법령해석-0001",
"title": "질의 제목"
}
{"documentNo":"사전-2026-법령해석-0001","title":"질의 제목"}
공백 제거만으로 문제를 해결할 수는 없지만, 중복 필드를 제거한 뒤 마지막으로 적용하면 추가 비용 없이 응답 크기를 더 줄일 수 있다.
7. 얼마나 줄었나
동일한 fixture와 호출 조건으로 변경 전후 JSON 응답의 문자 수를 비교했다.
| 시나리오 | 변경 전 | 변경 후 | 감소율 |
|---|---|---|---|
| 검색 결과 10개 | 6,868자 | 4,565자 | 34% |
| 검색 결과 5개 + 상세 문서 1개 | 13,994자 | 7,322자 | 48% |
| 정확한 문서 번호 조회 | 10,784자 | 4,945자 | 54% |
| 긴 결정문 상세 조회 | 51,235자 | 25,349자 | 51% |
| 통합 법령 검색 | 4,599자 | 2,539자 | 45% |
| 세법 연구 응답 | 13,919자 | 7,703자 | 45% |
| 안내 자료 검색 | 36,295자 | 4,759자 | 87% |
| 안내 자료 목차 | 39,005자 | 1,653자 | 96% |
| MCP 도구 스키마 | 2,605자 | 1,979자 | 24% |
여기서 측정한 값은 실제 모델이 계산한 토큰 수가 아니라 직렬화된 응답의 문자 수다. 토큰 수는 사용하는 모델과 토크나이저에 따라 달라진다. 다만 동일한 데이터를 같은 형식으로 비교했기 때문에 응답 중복과 컨텍스트 사용량이 얼마나 줄었는지 판단하는 기준으로 사용했다.
특히 결과가 여러 개인 안내 자료에서 공통 값을 상위로 이동한 효과가 컸다. 반면 검색 결과처럼 각 항목의 내용이 대부분 다른 응답에서는 감소 폭이 상대적으로 작았다.
8. 기존 기능과 호환성 확인하기
응답을 줄이더라도 도구 자체가 달라지면 기존 호출 흐름이 깨질 수 있다. 이번 변경에서는 다음 항목을 유지했다.
- 12개 MCP 도구의 이름과 입력 파라미터
- 캐시, 요청 제한, 재시도 처리
- 정확한 문서 번호 조회 방식
- 문서를 찾지 못했을 때의 진단 정보와 오류 응답
- 본문 길이 제한과 전체 본문 포함 옵션
대신 응답 필드 일부는 삭제되거나 조건부로 바뀌었고 JSON의 공백도 제거됐다. LLM이 결과를 읽는 흐름에는 영향이 적지만, 특정 필드의 존재나 들여쓰기 형식에 의존하는 클라이언트 코드는 수정이 필요할 수 있다. 따라서 이 작업은 단순한 내부 최적화가 아니라 응답 계약이 일부 바뀌는 API 변경으로 보는 것이 안전하다.
기능 회귀와 응답 크기를 함께 확인하기 위해 전체 테스트 278개를 실행했고, 그중 9개는 시나리오별 응답 크기가 다시 커지지 않는지 검사하는 테스트로 추가했다.
기능 테스트 → 검색과 상세 조회 결과가 기존과 같은가
호환성 테스트 → 도구 이름과 입력 파라미터가 유지되는가
토큰 예산 테스트 → 주요 시나리오의 응답 크기가 기준을 넘지 않는가
응답 최적화는 한 번 줄이고 끝나는 작업이 아니다. 기능이 추가될 때마다 편의를 위해 필드가 하나씩 늘어나기 때문에 주요 호출 흐름의 응답 크기를 테스트로 고정해 두는 것이 필요했다.
9. 정리
MCP 토큰 사용량을 줄이기 위해 특별한 압축 알고리즘을 적용한 것은 아니다. 모델이 한 번만 읽으면 되는 정보를 한 번만 보내고, 검색 단계에서는 선택에 필요한 정보만 보여 주도록 응답의 역할을 나눴다.
이번 작업에서 적용한 기준을 정리하면 다음과 같다.
- 같은 의미의 필드는 하나만 남긴다.
- 검색 결과와 상세 본문을 한 응답에 섞지 않는다.
- 목록에서 반복되는 값은 상위 필드로 올린다.
- 진단 정보는 실패했을 때만 제공한다.
- 빈 값과 JSON 공백은 직렬화 단계에서 제거한다.
- 실제 호출 시나리오별 응답 크기를 테스트로 관리한다.
가장 중요한 것은 필드를 무조건 없애는 것이 아니라 모델이 다음 행동을 결정하는 데 필요한 정보가 무엇인지 구분하는 것이다. 검색에서는 후보를 고를 정보가 필요하고, 상세 조회에서는 선택한 문서의 본문이 필요하다. 이 경계를 분명히 나누자 도구의 기능은 유지하면서도 주요 시나리오의 응답 크기를 34~96% 줄일 수 있었다.
관련 글: 한국 세법 자료 통합 MCP 원격 공개 배포
Leave a comment