한 사용자가 내 프로젝트를 위해 4일 동안 기능을 설계했습니다. 정답은 코드 0줄이었습니다.
요약
사용자의 상세한 기능 요청을 통해 기존 오픈 소스 프로젝트의 설계 결함과 문서화 부족을 발견한 사례를 다룹니다. 새로운 코드를 작성하는 대신, 기존 기능의 문서화와 세션 격리 버그 수정을 통해 문제를 해결하는 엔지니어링적 접근법을 보여줍니다.
핵심 포인트
- 기능 구현 전 '관심사가 어디에 존재하는가'를 먼저 질문해야 함
- 문서화되지 않은 기능은 구현되지 않은 기능과 다름없음
- 상세한 기능 요청은 시스템의 잠재적 버그를 찾아내는 도구가 됨
- 새로운 코드 작성보다 기존 설계의 올바른 활용이 우선될 수 있음
지난주에 저는 제 오픈 소스 프로젝트가 지금까지 받은 것 중 가장 훌륭한 기능 요청을 받았습니다. 그 요청은 매우 상세했고, 기술적으로 해박했으며, 설계 단계부터 fail-closed(실패 시 차단) 방식이었고, 분명히 저보다 이 도구를 더 격렬하게 사용하는 분으로부터 온 것이었습니다.
저는 그것을 구현하지 않을 것입니다. 그리고 요청한 사람도 동의했습니다. 왜냐하면 그 대화는 그가 자신의 레이어에서 다른 사람들도 이제 사용할 수 있는 패키지로 수정 사항을 배포하며 끝났기 때문입니다.
그 과정이 어떻게 진행되었는지 설명하고 싶습니다. 이 전체 과정은 우리가 너무 드물게 던지는 질문에 대한 사례 연구이기 때문입니다. 즉, "이것을 어떻게 구현할 것인가?"가 아니라 "이 기능의 관심사(concern)가 실제로 어디에 존재하는가?"라는 질문입니다.
요청 내용
제 프로젝트는 safari-mcp로, AI 코딩 에이전트가 macOS의 실제 Safari—사용자의 세션, 로그인, 탭—를 제어할 수 있게 해주는 MCP 서버입니다. 바로 이 점이 동시성(concurrency)을 위험하게 만듭니다. 만약 두 에이전트가 하나의 브라우저를 공유한다면, "활성 탭(the active tab)"은 장전된 총과 같습니다.
일상적인 Safari에서 병렬 AI 에이전트 세션을 실행하는 한 사용자가 제기한 문제는 **격리된 탭 레인(isolated tab lanes)**이었습니다. 각 동시 클라이언트가 자신만의 탭에 대한 안정적인 소유권, 명시적인 타겟팅, 그리고—그의 표현이자 전체 스레드에서 가장 멋진 문구인—앞에 있는 어떤 탭으로도 절대 폴백(fallback)하지 않는 동작을 갖도록 하는 것이었습니다. 왜냐하면 맨 앞의 탭은 보통 인간의 탭이기 때문입니다.
다섯 가지의 구체적인 요청 사항, 디자인 스케치, 그리고 사전에 지정된 fail-closed 의미론(semantics)까지. 기능 요청이 이보다 더 좋을 수는 없습니다.
첫 번째 놀라움: 대부분은 이미 존재했습니다
저의 첫 번째 본능은 로드맵을 보고 답변하는 것이었습니다. 대신 저는 소스 코드를 직접 읽기로 했고, 그 결과는 당혹스러웠습니다. 그의 다섯 가지 요청 중 세 가지는 이미 배포된 상태였습니다. MCP 세션 ID로 키가 지정된 세션별 탭 상태, 인덱스가 아닌 페이지에 찍힌 마커에 의한 탭 식별(따라서 탐색 및 탭 재정렬 시에도 유지됨), 그리고 그가 요청한 fail-closed 거부 기능은 거의 그대로 존재했습니다. 코드베이스는 말 그대로 "'창의 현재 탭'으로 폴백하는 것을 거부합니다 (사용자의 활성 탭을 타겟팅하게 됨)"라고 출력하고 있었습니다.
그는 README를 주의 깊게 읽고 이 모든 것이 존재하지 않는다고 합리적으로 결론을 내린 유능한 엔지니어였습니다. 그것은 그의 잘못이 아닙니다. 이 모든 내용은 README에서 전혀 링크되지 않은 설계 문서 (design doc)에만 기록되어 있었습니다. 아무도 발견할 수 없는 기능은 구현하지 않은 기능과 구별할 수 없습니다. 그래서 제가 이 스레드에서 배포한 첫 번째 "기능"은 README 섹션이었습니다.
두 번째 놀라움: 기능 요청이 실제 버그를 찾아냈다
그 후 그가 보낸 후속 질문은 상황을 더 좋게 만들었습니다. 그는 왜 자신의 설정에서 격리 (isolation)가 작동하지 않는지 파고들었고, 공유 데몬 (shared-daemon) 모드에서는 모든 클라이언트의 브라우저 확장 프로그램 트래픽이 동일한 프로세스 전역 세션 ID (process-wide session id)를 운반한다는 사실을 발견했습니다. 즉, 확장 프로그램이 모든 클라이언트를 하나의 세션으로 인식하게 되어, 한 클라이언트가 다른 클라이언트의 캐시된 탭을 제공받을 수 있는 상태였습니다.
그가 맞았습니다. 저는 소스 코드에서 이를 확인했고, 당일에 수정 사항을 배포했으며, 릴리스 노트 (release notes)에 그의 이름을 올렸습니다. 배포된 버그 수정 (bugfix)을 만들어내는 기능 요청은 이미 그 가치를 충분히 증명한 셈입니다. 하지만 메인 요리는 아직 남아 있었습니다.
내가 좋아했지만 여전히 구현하지 않은 제안
그가 지적한 나머지 간극은 실재했습니다. 그의 클라이언트 툴링은 하나의 연결 풀 (connection pool)을 통해 여러 에이전트 세션을 멀티플렉싱 (multiplexing)하여 이를 단일 MCP 클라이언트로 통합합니다. 따라서 클라이언트 경계를 기준으로 하는 세션별 격리 (per-session isolation)가 작동할 기회를 전혀 얻지 못합니다. 그의 제안은 다음과 같았습니다: 모든 도구 호출 (tool call)에 laneId 파라미터를 추가하고, 이를 필수 사항으로 지정하며, 실패 시 차단 (fail-closed) 방식으로 기존 세션 맵 (session map)으로 라우팅하는 것입니다.
여기에 함정이 있습니다. 내부적으로 이것은 거의 비용이 들지 않는 작업이었습니다. 세션별 상태 머신 (state machinery)은 이미 존재했습니다. 각 호출을 레인 범위 (lane-scoped) 컨텍스트에서 실행하는 것은 그것을 거의 변경 없이 재사용하는 것뿐이었습니다. 어떤 기능이 구현하기까지 단 한 오후면 충분할 때, "구현하기 저렴하다"는 생각은 "구현하는 것이 옳다"는 생각으로 위장하기 시작합니다.
하지만 비용은 구현에 있지 않았습니다. 서버는 97개의 도구 (tools)를 노출하고 있으며, 필수적인 laneId가 추가된다는 것은 97개의 도구 스키마 (tool schemas)에 파라미터가 늘어남을 의미합니다. 이는 두 명의 클라이언트가 존재할 일이 결코 없는 모든 단일 클라이언트 (single-client) 설정에서, 에이전트 (agent)가 읽어야 할 모든 스키마와 소모되는 모든 컨텍스트 (context) 바이트에 영향을 미칩니다. 멀티 클라이언트 (multi-client)에 대한 고려 사항이 모든 사람의 단일 클라이언트 API에 영원히 스며들게 되는 것입니다. 스키마는 MCP 서버에서 나중에 조용히 리팩터링 (refactor)할 수 없는 유일한 부분입니다.
그래서 저는 귀찮은 일을 했습니다. 그것이 좋다고 말한 뒤, 만약 필요하게 된다면 정확히 어떻게 구축되고 제한될 것인지(gated)를 적어두고 — 구축을 보류했습니다. 공개적으로 말이죠. 이슈 (issue)에서요. 이는 마치 시간을 끄는 것처럼 느껴졌지만, 결과적으로 그것이 가장 중요한 결정이었음이 밝혀지기 전까지는 그랬습니다.
우려 사항이 실제로 존재했던 곳
문제의 형태를 다시 살펴보십시오. 격리 (isolation)가 제 서버에 없었던 것이 아닙니다. 그것은 N개의 에이전트 세션 (agent sessions)을 하나의 클라이언트로 통합하는 상위 계층 (upstream layer)에 의해 지워지고 있었던 것입니다. 제 서버에 레인 (lanes)을 추가하라고 요구하는 것은, 상위 계층이 파괴한 무언가를 하위 계층 (downstream layer)이 보상하도록 요구하는 것과 같았습니다.
그 또한 동일한 결론에 도달했고, 자신의 스택 (stack)을 살펴보았습니다. 그리고 자신의 멀티플렉서 (multiplexer)에 이미 인스턴스별 격리 (per-instance isolation) 조절 기능이 있다는 것을 발견했습니다. 그러고 나서 그는 진정으로 훌륭한 일을 해냈습니다. 개인적인 임시방편 (workaround)을 만드는 대신, 각 에이전트 세션에 고유한 데몬 디렉토리 (daemon directory) — 즉, 고유한 연결, 고유한 서버 프로세스, 고유한 레인 — 를 부여하는 확장 기능 (extension)을 구축하고 배포했습니다. 여기에 동시성 (concurrency) 예외 케이스를 위한 복합 레인 키 (composite lane key)와 프로세스가 쌓이지 않도록 하는 유휴 타임아웃 (idle timeout) 기능도 포함되었습니다.
이는 제 프로젝트에만 국한된 문제가 아닙니다. 해당 멀티플렉서 뒤에 있는 어떠한 상태 유지 (stateful) MCP 서버에도 적용됩니다. 해결책은 문제를 소유하고 있는 계층에 반영되었으며, 다른 사람들이 재사용할 수 있는 인프라 (infrastructure)의 형태로 제공되었습니다.
점수판
"거절된" 기능 요청에 대한 최종 집계:
- 문서화되었지만 보이지 않는 형태가 아닌, 기존의 격리(isolation)를 발견할 수 있게 만든 README 섹션
- 발견되고, 검증되었으며, 수정 사항으로 배포된 실제 크로스 클라이언트 세션(cross-client session) 버그 — 제보자에게 공로를 돌림
- 사용자가 직접 게시하여 해당 문제를 소유한 계층(layer)에서 해결한 재사용 가능한 확장 기능(extension)
- 기록으로 남겨진
laneId에 대한 설계안 — 필요성이 실제로 나타날 경우 하루 만에 구축 가능함 - 내 저장소(repo)에는 기능 코드가 단 한 줄도 없음
YAGNI(You Ain't Gonna Need It)는 보통 거절, 즉 요청자의 면전에서 문을 닫아버리는 행위로 프레임이 잡히곤 합니다. 하지만 제대로 수행된다면, 그것은 방향 전환(redirection)입니다. 노력은 사라진 것이 아니라 마땅히 있어야 할 곳으로 이동했으며, 결과적으로 생태계는 제가 기능을 직접 구현했을 때보다 더 많은 것을 얻게 되었습니다. 제가 받은 최고의 코드 리뷰(code review)는 기능 요청(feature request)이었습니다. 제가 지난주에 배포한 최고의 기능은 다른 사람의 패키지로 연결되는 링크였습니다.
여러분이 거절했던 최고의 기능 요청은 무엇인가요? 그리고 "여기서는 안 됩니다"라고 말한 것이 직접 만드는 것보다 더 나은 결과물을 만들어낸 적이 있었나요? YAGNI 원칙을 고수했으나 결과적으로 그것이 정말 필요했던 상황과 같은 반례(counter-examples)도 진심으로 듣고 싶습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기