
여섯 가지 문제와 여섯 가지 결정: sharp-mcp의 내부 작동 원리
요약
C# 및 .NET 개발 환경에 특화된 로컬 MCP 서버인 sharp-mcp의 설계 원리와 아키텍처 결정 과정을 다룹니다. 범용 AI 코딩 도구의 한계를 극복하기 위해 Roslyn을 활용한 구조적 파싱과 캐싱 전략을 도입한 배경을 설명합니다.
핵심 포인트
- 범용 도구의 토큰 오버헤드를 줄이기 위해 C#/.NET에 집중한 설계
- 단순 파일 읽기 방식의 성능 및 컨텍스트 소모 문제 해결
- Roslyn을 활용한 AST 기반의 구조적 코드 제공 방식 채택
- 실제 백엔드 개발 워크플로우에 적용 가능한 실무 중심 아키텍처
두 달 전, 저는 Claude에게 C# 코드베이스에 대한 실제 Roslyn 기반 액세스 권한을 부여하는 로컬 MCP 서버인 sharp-mcp에 관한 첫 번째 기사를 게시했습니다. 그 기사에서는 _무엇(what)_인지에 대해 소개했습니다. 이번 기사에서는 _모든 결정 뒤에 숨겨진 이유(why behind every decision)_를 다룹니다.
먼저 한 가지 분명히 말씀드리고 싶습니다. 저는 지난 두 달 동안 실제 운영 업무에서 sharp-mcp를 주요 코딩 워크플로우(workflow)로 사용해 왔습니다. 데모나 장난감 프로젝트가 아니라, 제 직장에서 수행하는 실제 백엔드 .NET 개발에 사용했습니다. 이는 제 작업 방식을 완전히 바꾸어 놓았습니다. 제가 설명하려는 모든 아키텍처(architectural) 결정은 실제 벽에 부딪혔거나, 잘못되었다고 판단되는 트레이드오프(tradeoff)를 수용하기를 거부한 결과에서 비롯되었습니다.
GitHub: https://github.com/patilprashant6792-official/sharp-mcp
💬 GitHub Discussions에서 빌드 과정을 팔로우하고 피드백을 공유해 주세요
시작점: 집중 (focus)
Claude Code, GitHub Copilot, Cursor — 이들은 모두 모든 언어와 모든 생태계를 위해 구축되었습니다. 그러한 범용성에는 비용이 따릅니다. C# 백엔드 작업을 할 때, 여러분은 Python, JavaScript, Ruby를 위해 구축된 추상화(abstractions) 때문에 토큰 오버헤드(token overhead)를 지불하게 됩니다. 저는 오직 C#만을 알고, 오직 .NET만을 알며, 그 집중력을 바탕으로 그 어떤 범용 도구보다 일을 더 잘 수행할 수 있는 무언가를 원했습니다.
광범위한 호환성 대신 좁은 집중을 선택한 그 결정이, 이후의 모든 결정으로 이어지는 흐름이 되었습니다.
문제 1: 직접적인 파일 읽기는 당연한 첫 번째 단계였지만 — 그리고 그것은 실패했습니다
저는 누구나 시작하는 방식대로 시작했습니다. 파일을 읽어서 모델에 전달하는 것이죠. 작동은 했습니다. 하지만 실제 코드베이스(codebase)에 적용하기에는 비현실적으로 느렸습니다. 검색 지연 시간(Search latency)은 높았고, 큰 파일은 컨텍스트(context)를 빠르게 소모했으며, 매 요청마다 실제 파일 시스템(filesystem)에 접근했습니다.
500줄짜리 서비스 클래스는 원문 그대로 약 2,000개의 토큰을 소모합니다. 마이크로서비스(microservices) 솔루션 전체에서 파일 10개만 로드해도, 단 한 줄의 코드를 쓰기도 전에 컨텍스트(context) 예산을 모두 써버리게 됩니다. 이것은 도구가 아니라 장애물입니다.
대규모 환경에서 직접적인 파일 읽기(Direct file reading)는 실행 불가능했습니다. 그리고 그 실패는 해결책을 명확하게 만들어 주었습니다. 구조를 한 번 파싱(parse)하고, 이를 캐싱(cache)한 뒤, 메모리에서 제공하는 것입니다.
문제 2: Roslyn은 정답이었지만, 거처가 필요했습니다
Roslyn은 클래스, 메서드(methods), 라인 스팬(line spans), 의존성(dependencies) 등 모든 것이 구조화된 전체 추상 구문 트리(AST, Abstract Syntax Tree)를 제공합니다. Claude에게 500줄짜리 파일을 통째로 넘겨주는 대신, 클래스 이름, 생성자 의존성, 정확한 줄 번호가 포함된 메서드 시그니처(method signatures)와 같은 API 표면(API surface)을 전달합니다. 모델은 추론(reasoning)에 필요한 정보만 정확히 얻고, 불필요한 정보는 받지 않게 됩니다.
하지만 매 요청마다 파싱을 수행하는 것은 목적에 완전히 어긋납니다. 데이터는 어딘가 빠른 곳에 존재해야 했습니다.
Redis가 명확한 선택지였습니다. .NET 백엔드 개발자라면 이미 Redis를 사용하고 있을 것입니다. 이는 생소한 의존성(dependency)이 아니라, 운영 환경(production)에서 신뢰할 수 있는 인프라입니다. 결정된 사항은 다음과 같습니다. 시작 시 Roslyn으로 한 번 파싱하고, AST 메타데이터를 Redis에 직렬화(serialize)하여 저장한 뒤, 이후의 모든 호출은 디스크 I/O 없이 밀리초(milliseconds) 단위로 제공하는 것입니다.
어떤 파일에 대한 두 번째 호출은? Redis 읽기입니다. 그게 전부입니다.
문제 3: NuGet 환각(hallucinations) — 아무도 해결하지 못하고 있는 문제
이것은 가장 고통스럽게 마주한 문제였으며, 동시에 해결하기에 가장 흥미로운 문제였습니다.
AI 모델은 겉보기에는 올바른 C# 코드를 생성하지만, 실제로는 컴파일되지 않는 경우가 많습니다. 사용 중인 NuGet 패키지 내부에 실제로 무엇이 들어있는지 모르기 때문입니다. System.Text.Json은 6.0 버전과 8.0 버전 사이에서 nullable 처리 방식이 변경되었습니다. EF Core는 7.0 버전과 8.0 버전 사이에서 DbContext 설정 방식이 바뀌었습니다. IgnoreNullValues는 라이프사이클 중간에 지원 중단(deprecated)되었습니다. 모델은 사용자의 버전에는 더 이상 존재하지 않는 API를 대상으로 자신 있게 코드를 생성합니다.
문서(documentation)는 불완전합니다. 학습 데이터(training data)는 오래되었습니다. 모델은 추측하며, 그리고 틀린 추측을 합니다.
해결책: 추측을 완전히 중단하는 것입니다. 바이너리(binary)를 디코딩(decode)하십시오.
MetadataLoadContext는 실제 .nupkg를 다운로드하고, 전이적 의존성 (transitive dependencies)을 해결하며, DLL을 격리된 컨텍스트 (isolated context)에 로드하고, 실제 IL을 읽어 실제 시그니처(signatures) — 실제 메서드 이름, 실제 매개변수 타입, 실제 제네릭 (generics) — 를 반환합니다. 문서도 아니고, 학습 데이터도 아닙니다. 모델은 컴파일러가 사용하는 것과 동일한 그라운드 트루스 (ground truth)를 얻게 됩니다.
최첨단 LLM (Frontier LLMs)은 필요할 때마다 동작 컨텍스트 (behavioral context)를 위해 웹에서 문서를 검색할 수 있습니다. 하지만 그들이 할 수 없는 것은 사용자의 특정 패키지 버전 내의 정확한 메서드 시그니처 (method signatures)를 아는 것입니다. 그것이 바로 간극입니다. 그리고 이것이 그 간극을 메웁니다.
실제 토큰 비용 차이:
| 수행 작업 | 원시 문서 덤프 (Raw docs dump) | sharp-mcp |
|---|---|---|
| 네임스페이스 (namespace) 탐색 | ~6,000 토큰 | ~250 토큰 |
| ... | ||
| 전체 구현 세션 동안 이는 수 시간의 추가적인 유용한 컨텍스트로 누적됩니다. |
문제 4: 캐시는 따뜻하게(warm) 유지되어야 합니다
캐싱된 Roslyn 분석은 파일의 현재 상태를 반영할 때만 유용합니다. 이것이 대부분의 캐싱 접근 방식을 망가뜨리는 일관성 문제 (consistency problem)입니다. 속도를 얻는 대신 정확도를 희생하게 됩니다.
해결책: 파일 변경 사항을 감지하고 관련 캐시 항목을 자동으로 무효화하거나 업데이트하는 FileSystemWatcher 아키텍처입니다. Visual Studio에서 수행하는 모든 .cs 파일 편집은 300ms 이내에 캐시로 전파됩니다. 이는 VS가 단일 저장 시 발생하는 이벤트 폭주를 처리하기 위한 디바운스 (debounce) 창입니다.
삭제 (Delete) 이벤트는 키를 제거합니다. 이름 변경 (Rename) 이벤트는 이전 키를 제거하고 새 경로를 인덱싱합니다. 백그라운드 인덱서 (indexer)는 시작 시와 매 60분마다 실행되어 Watcher가 놓친 모든 것을 포착합니다.
결과: 전역 코드 검색은 실제 코드베이스를 건드리지 않고도 몇 초 이내에 따뜻한 캐시 데이터(warm cache data)를 대상으로 실행됩니다. Claude는 항상 디스크에 존재하는 현재의 코드를 보게 되며, 절대 오래된(stale) 정보를 보지 않습니다. 이것이 긴 기능 구현 세션을 실용적으로 만드는 요소입니다.
문제 5: Claude.ai에는 localhost로 연결할 브릿지가 필요합니다
웹상에서 실행되는 LLM은 사용자의 로컬 머신에 직접 접근할 수 없습니다. ngrok은 SSE 터널을 제공합니다. 명령어 하나, URL 하나를 생성하여 Claude.ai의 Settings → Connectors에 붙여넣기만 하면, Claude가 23개의 모든 도구(tools)를 자동으로 발견합니다.
이것은 실제적인 의존성(dependency)이며, 저는 이를 미화하지 않겠습니다. 이를 제거하기 위한 VS Code 확장 프로그램이 로드맵에 올라와 있습니다. 하지만 현재로서는 명령어 하나면 충분하며, 나머지 설정은 dotnet run입니다.
문제 6: 마지막 마찰 지점을 제거하는 UI
제가 구축하기를 거부한 한 가지는, 프로젝트를 추가할 때마다 JSON 설정 파일을 편집하거나 서버를 재시작해야 하는 도구였습니다.
/config.html에는 내장된 프로젝트 관리 UI가 있습니다. 솔루션(solution) 폴더를 지정하기만 하면 됩니다. 그게 전부입니다. sharp-mcp는 클래스(classes), 메서드(methods), 의존성(dependencies), 파일 크기 등 모든 것을 인덱싱(indexing)하며, LLM은 즉시 해당 코드베이스에 대한 완전한 구조적 인식(structural awareness)을 갖게 됩니다.
솔루션 폴더를 지정하기만 하면 됩니다. 그게 전부입니다. sharp-mcp는 클래스(classes), 메서드(methods), 의존성(dependencies), 파일 크기 등 모든 것을 인덱싱(indexing)하며, LLM은 즉시 해당 코드베이스에 대한 완전한 구조적 인식(structural awareness)을 갖게 됩니다. 각 프로젝트는 독립적으로 활성화할 수 있고, 필요에 따라 재인덱싱(re-indexable)이 가능하며, 어떤 설정 파일도 건드리지 않고 삭제할 수 있습니다. 그리고 맞습니다 — sharp-mcp는 동일한 UI를 통해 자신의 소스 코드도 관리합니다. 이 도구는 스스로가 첫 번째 사용자입니다.
모든 것이 구축된 세 가지 원칙
여섯 가지 결정을 모두 되돌아보면, 이 모든 것은 세 가지 요소로 귀결됩니다:
- 속도 (Speed) — Redis 캐시와 파일 와처 (file watcher)를 통해 상태를 따뜻하게 (warm) 유지합니다. 첫 번째 인덱싱 패스 이후에는 콜드 스타트 (cold start)가 발생하지 않습니다.
- 토큰 효율성 (Token efficiency) — 모든 도구 설명은 모델에게 언제 해당 도구를 호출하지 말아야 하는지를 알려줍니다. 배치 모드 (batch modes), 크기 힌트 (size hints), "최후의 수단" (last resort) 레이블 등은 단순한 문서화가 아니라 아키텍처에 내장된 프롬프트 엔지니어링 (prompt engineering)입니다. LLM은 정확히 알아야 할 내용만을 전달받아야 하며, 그 이상은 필요하지 않습니다.
- 정확성 (Correctness) —
MetadataLoadContext를 사용한다는 것은 모델이 근사치가 아닌 실제 시그니처 (signatures)를 바탕으로 작동함을 의미합니다. 여러분의 NuGet 패키지에 대해 학습 데이터 컷오프 (training cutoff) 문제는 존재하지 않습니다.
시스템 프롬프트 — 두 달간의 정제 과정
충분히 논의되지 않는 부분이 여기 있습니다: 도구만으로는 에이전트 (agent)를 만들 수 없습니다. 판단력 (judgment)이 필요합니다.
실제 운영 코드 (production code)에서 두 달간 매일 사용해 본 결과, 제가 사용하는 정확한 시스템 프롬프트를 저장소의 prompts/CODING_SYSTEM_PROMPT.md에 공개했습니다.
이 프롬프트는 두 부분으로 나뉩니다:
파트 A — 엔지니어링 철학 (Engineering philosophy). 11가지 원칙: 행동 전 명확성 확보, 명시되지 않은 문제 해결, 원자적 분해 (atomic decomposition), 다각도 검증 (multi-perspective validation) 등이 포함됩니다. 이는 LLM 전용 지침이 아닙니다. 어떤 시니어 엔지니어도 키보드에 손을 올리기 전에 적용하는 원칙들입니다. 프롬프트는 모델이 시간 압박 속에서도 이를 건너뛰지 않도록 명시적으로 규정합니다.
파트 B — 상식적인 운영 규칙 (Common sense production rules). 13가지 영역: 에러 핸들링 (error handling), 검증 (validation), 리소스 관리 (resource management), 로깅 (logging), 동시성 (concurrency), API 설계 (API design), 데이터베이스 패턴 (database patterns) 등이 있습니다. 서드파티 라이브러리 (third-party libraries) 섹션의 규칙 중 인용할 만한 것이 하나 있습니다:
어떤 라이브러리를 사용하기 전에 항상 최신 공식 문서를 웹 검색하십시오. 설치된 정확한 버전을 확인하십시오. 해당 특정 버전에 대한 메서드 시그니처 (method signatures)와 API를 검증하십시오. 오래된 지식에 기반하여 동작을 추측하지 마십시오.
해당 규칙이 존재하는 이유는 바로 sharp-mcp가 해결하고자 했던 NuGet 환각 (hallucination) 문제 때문입니다. MetadataLoadContext로부터 얻은 실제 시그니처 (signatures)가 있더라도, 코드를 작성하기 전에 가정을 검증하지 않는 모델은 여전히 틀린 방식을 찾아낼 것입니다. 프롬프트 (prompt)와 도구 (tools)는 서로를 강화합니다.
세 번째 섹션은 23개의 도구를 엄격한 결정 트리 (decision tree)에 매핑합니다. 어떤 도구를 먼저 호출할지, 언제 대규모 C# 파일에 대해 read_file_content를 호출하지 말아야 하는지, 왜 무엇인가를 완료로 표시하기 전에 항상 execute_dotnet_command를 실행해야 하는지, 그리고 왜 모든 edit_lines 패치 (patches)를 단일 호출로 배치 (batch) 처리하는지 등을 다룹니다.
이 프롬프트 덕분에 사용 경험은 단순한 자동 완성 (autocomplete)이 아니라, 숙련된 .NET 엔지니어와 페어 프로그래밍 (pair programming)을 하는 것처럼 느껴집니다. 그리고 이는 Claude뿐만 아니라 MCP 지원이 가능한 모든 LLM에서 작동합니다.
이것이 대체하지 않는 것
이것은 인라인 자동 완성 (inline autocomplete)이 아닙니다. 타이핑하는 동안 다음 줄을 제안하지 않습니다.
이것이 대체하는 것은 추론 세션 (reasoning session)입니다. 즉, 코드베이스를 이해하기 위해 채팅을 열거나, 리팩터링 (refactor)을 계획하거나, 메서드 시그니처 (method signature)를 변경했을 때 무엇이 깨지는지 확인하거나, 의존성 (dependency)이 실제로 어떻게 작동하는지 찾아보거나, 여러 파일에 걸친 기능을 구현할 때의 세션입니다. 그러한 세션이야말로 컨텍스트 제한 (context limits), 환각된 API (hallucinated APIs), 그리고 클라우드 데이터 노출이 실제로 피해를 입히는 지점입니다.
그것이 바로 이 도구가 구축된 범위입니다. 프로덕션 .NET 백엔드 작업에서 두 달 동안 매일 사용해 본 결과, 확신을 가지고 말씀드릴 수 있습니다. 이것은 제가 C#을 작성하며 경험한 가장 생산적인 방식입니다.
만약 첫 번째 기사를 읽고 "흥미롭지만 실제로 어떻게 작동하는지 알고 싶다"라고 생각하셨다면, 이 글은 바로 당신을 위해 작성되었습니다. 질문은 댓글로 환영합니다.
— Prashant
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기