본문 바로가기

OSSCA/2025

[OSSCA 2025] SRP, 모듈화 그리고 디자인 패턴

✨ 공부하며 성장하는 기록 공간입니다.
아직 부족한 점이 많고 배워야 할 것도 많지만, 그만큼 배우는 즐거움도 큽니다.
틀리거나 부족한 내용이 있다면 언제든지 편하게 지적해 주세요 😁

이 블로그는 오픈소스 프로젝트 githru-vscode-extension을 공부하고 기여한 내용을 정리한 공간입니다.
처음보다 성장해 있는 나 자신을 기대하며 꾸준히 기록해나가겠습니다!

 

서론

이전 포스팅에서는 Githru MCP Server의 GitHub Token 관리 구조를 개선하는 과정을 살펴보았습니다.

 

기존에는 여러 MCP Tool이 GitHub Token을 입력값으로 전달받았지만, PR #972에서는 Config Singleton을 통해 토큰을 중앙에서 관리하도록 변경했습니다.

 

그 과정에서 Singleton, DI(Dependency Injection), IoC(Inversion of Control), 그리고 인증정보 관리에 대해 고민할 수 있었습니다. 이번에는 조금 다른 문제를 다뤄보려고 합니다.

 

MCP Tool이 계속 증가한다면, 서버의 코드는 어떻게 관리해야 할까?

 

Githru MCP Server에는 Git 저장소의 데이터를 분석하는 Tool뿐 아니라 React Component를 이용해 시각화하는 Tool, 개발 과정에서 사용하는 테스트 Tool 등 다양한 기능이 추가되고 있었습니다.

 

Tool이 늘어나면서 자연스럽게 서버의 진입점인 server.ts의 역할도 커지기에, 처음에는 하나의 파일에서 Tool을 등록하는 것이 간단했지만, 기능이 증가하면서 Tool 등록 로직을 분리할 필요가 생겼습니다.

 

이를 해결하기 위해 다음 두 PR을 진행했다.

이번 글에서는 실제 리팩토링의 배경과 작업 내용을 살펴보고, 이를 SRP, 모듈화, DI/IoC, Composition Root, Registry 패턴과 연결해 보려고 합니다.

 

MCP Server의 Tool 이 늘어나면서 발생한 문제

MCP Server의 Tool 등록

MCP Server는 Client가 사용할 수 있는 Tool을 등록하고, Client의 호출 요청이 들어오면 해당 Tool의 실행 함수를 호출합니다.

 

TypeScript SDK에서는 서버 객체에 Tool의 이름, 설명, 입력 스키마, 실행 함수를 등록하는 방식으로 구현할 수 있는데

server.tool(
  "analyze_repository",
  "Analyze a Git repository",
  {
    repoPath: z.string()
  },
  async ({ repoPath }) => {
    const result = await analyzeRepository(repoPath);

    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(result)
        }
      ]
    };
  }
);

 

하지만 만약 Tool을 여러개 등록해야하는 경우에는 다음과 같은 일이 발생합니다.

// server.ts

server.tool("generate_csm_dict", ...);

server.tool("contributor_recommender", ...);

server.tool("feature_impact_analyzer", ...);

server.tool("render_storyline_ui", ...);

server.tool("author_work_pattern", ...);

각 Tool의 등록 코드가 하나의 파일에 계속 추가된다면 server.ts가 점점 비대해집니다.

 

파일이 길어지는 문제?

파일의 길이가 길어진다는 것 자체가 반드시 잘못된 설계라는 의미는 아니지만 더 중요한 것은 서로 다른 책임이 하나의 모듈에 집중되는 것입니다.

 

예를 들어 server.ts가 다음과 같은 역할을 모두 담당한다고 가정해 볼 때

  1. MCP Server 인스턴스 생성
  2. Tool의 입력 스키마 정의
  3. Tool의 실행 함수 연결
  4. Tool별 오류 처리
  5. 서버 실행 및 전송 계층 연결

이러한 구조에서는 새로운 Tool을 추가할 때마다 서버의 진입점을 수정해야 합니다.

 

분석 Tool의 구현을 변경하는 작업과 서버 초기화 코드를 변경하는 작업이 같은 파일에서 이루어지는 것이고, 또한 여러 개발자가 동시에 Tool을 추가한다면 동일한 파일을 수정하면서 Git 충돌이 발생할 가능성도 높아집니다.

 

이것이 이번 리팩토링의 출발점이었습니다.

 

Tool 등록 책임을 분리

PR #967에서는 기존 server.ts의 유지보수성을 높이기 위해 Tool 등록 구조를 개선했습니다.

 

목표는 다음과 같았습니다.

  • server.ts는 Tool 등록을 연결하는 역할만 담당한다.
  • 각 Tool이 서버에 자신을 등록할 수 있도록 구조를 변경한다.
  • Tool의 성격에 따라 패키지를 분류한다.

변경 전 구조

server.ts
  │
  ├── MCP Server 생성
  │
  ├── Test Tool 등록
  │
  ├── Visualization Tool 등록
  │
  ├── React Test Tool 등록
  │
  └── Server 실행

모든 책임이 sever.ts에 집중되어있었습니다.

 

변경 후 구조

리팩토링 이후에는 Tool이 속한 모듈에서 등록 함수를 제공하도록 변경했습다.

server.ts
  │
  ├── registerTestTools(server)
  │
  ├── registerVisualizationTools(server)
  │
  ├── registerReactTestTools(server)
  │
  └── Server 실행

각 함수는 관련 Tool의 등록을 담당합니다.

 

이 구조의 핵심은 server.ts가 개별 Tool의 상세 구현을 몰라도 된다는 점으로, 어떤 Tool이 존재하는지, 어떤 입력 스키마를 사용하는지, 어떤 실행 함수를 호출하는지는 각 모듈에서 관리할 수 있습니다.

 

결국, server.ts는 필요한 Tool 등록 함수를 호출하는 역할에 집중합니다.

 

실제 코드로 이해하는 모듈화

이해를 돕기 위해 리팩토링 구조를 TypeScript로 재구성해 보면

Tool별 등록 함수 정의

먼저 기여자 추천 Tool을 구현한다고 가정할 때

// tools/analysis/contributorRecommender.ts

import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

export function registerContributorRecommender(
  server: McpServer
): void {
  server.tool(
    "contributor_recommender",
    "Recommend contributors",
    {
      repoPath: z.string()
    },
    async ({ repoPath }) => {
      const result = await recommendContributors(repoPath);

      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(result)
          }
        ]
      };
    }
  );
}

여기서 registerContributorRecommender()는 Tool을 서버에 등록하는 책임을 가집니다.

이 함수는 분석 로직을 직접 구현할 필요가 없고 실제 분석은 recommendContributors()와 같은 별도 함수에서 수행할 수 있습니다.

 

즉, Tool의 등록과 비즈니스 로직을 분리할 수 있다.

패키지 단위로 등록 함수 구성

관련 Tool들을 하나의 패키지 등록 함수에서 처리하도록 구성할 수도 있습니다.

// tools/analysis/index.ts

export function registerAnalysisTools(
  server: McpServer
): void {
  registerContributorRecommender(server);
  registerFeatureImpactAnalyzer(server);
  registerAuthorWorkPattern(server);
}

이제 분석 Tool이 추가되더라도 server.ts를 직접 수정할 필요가 없고 분석 패키지 내부의 등록 함수만 변경하면 됩니다.

server.ts 단순화

// server.ts

const server = new McpServer({
  name: "githru-mcp",
  version: "1.0.0"
});

registerAnalysisTools(server);
registerVisualizationTools(server);
registerTestTools(server);

// Transport 연결 및 서버 실행

이 구조에서는 서버 초기화와 Tool 등록의 세부 구현이 분리되어 파일을 탐색하기 쉬워집니다.

 

디자인 패턴 관점에서 살펴보기

이번 리팩토링은 설계 원칙의 관점에서 살펴보면 몇 가지 중요한 개념을 사용했습니다.

SRP — 단일 책임 원칙

SRP(Single Responsibility Principle)는 SOLID 원칙 중 하나로 하나의 클래스나 모듈은 하나의 책임만 가져야 합니다.

조금 더 정확하게는 하나의 모듈이 변경되어야 하는 이유를 하나의 책임 영역으로 제한하는 것에 가까운데,

 

기존 server.ts는 Tool 등록과 서버 초기화에 관한 여러 세부 사항을 함께 관리하고 있었습니다.

리팩토링 이후에는 책임을 다음과 같이 구분할 수 있습니다.

 

모듈 책임
server.ts 서버 생성 및 등록 모듈 연결
Analysis 등록 모듈 분석 Tool 등록
Visualization 등록 모듈 시각화 Tool 등록
개별 Tool 입력 스키마와 호출 처리
Analysis Service 실제 분석 로직 수행

이렇게 분리하면 특정 Tool의 구현 변경이 서버 초기화 코드에 영향을 줄 가능성을 줄일 수 있습니다.

 

OCP — 개방 폐쇄 원칙

OCP(Open-Closed Principle)는 소프트웨어 구성 요소가 확장에는 열려 있고 수정에는 닫혀 있어야 한다는 원칙으로

기존 구조에서는 Tool을 추가할 때 server.ts를 수정해야 했습니다.

 

패키지별 등록 함수로 분리하면 기존 서버 진입점을 수정하지 않고 패키지 내부에서 Tool을 추가할 수 있었지만

여전히 새로운 종류의 패키지를 추가한다면 여전히 server.ts에 새로운 등록 함수를 연결해야 한다.

 

따라서 이번 리팩토링이 OCP를 완벽하게 구현했다고 보기보다는, Tool 추가에 따른 변경 범위를 축소했다고 설명하는 것이 정확합니다.

DI — 의존성 주입

이번 코드에서는 다음과 같이 등록 함수에 MCP Server 인스턴스를 전달합니다.

registerAnalysisTools(server);

등록 함수가 내부에서 새로운 MCP Server를 생성하는 것이 아니라 외부에서 전달받습니다.

function registerAnalysisTools(
  server: McpServer
) {
  // 전달받은 서버에 Tool 등록
}

 

IoC — 제어의 역전

기존에는 서버 진입점이 개별 Tool 등록의 세부 사항을 직접 관리했습니다.

리팩토링 이후에는 각 모듈이 등록 세부 사항을 관리하고, 서버 진입점은 모듈의 등록 함수를 호출합니다.

 

IoC의 핵심은 객체 생성이나 의존성 연결과 같은 제어 책임을 외부로 이동시키는 것입니다.

 

리팩토링이 이후 변화

이번 작업을 통해 개선된 부분은 크게 세 가지입니다.

server.ts의 역할이 명확해졌다

서버 진입점에서 개별 Tool의 등록 세부 사항을 직접 관리하지 않고, 등록 모듈을 연결하도록 변경했기에 서버가 어떤 기능 그룹을 제공하는지 파악하기 쉬워졌습니다.

Tool의 성격에 따른 분류가 가능해졌다

Analysis, Visualization, Test와 같이 기능별로 등록 코드를 관리할 수 있게 되었습니다.

이는 새로운 Tool을 추가하거나 기존 Tool을 수정할 때 관련 코드를 찾는 데 도움이 됩니다.

유지보수성과 가독성이 개선되었다

PR #967의 코드 리뷰에서도 구조 변경으로 유지보수성과 가독성이 좋아졌다는 피드백을 받을 수 있었습니다.

이는 단순히 코드 줄 수를 줄인 것이 아니라, 각 모듈의 역할을 명확하게 만든 결과라고 생각합니다.

마무리

이번 PR #967과 #975에서는 MCP Server가 확장되면서 발생한 Tool 관리 문제를 해결하기 위해 등록 구조를 리팩토링했습니다.

 

처음에는 하나의 server.ts에서 Tool을 관리하는 방식으로 충분했지만, Analysis, Visualization, Test 등 다양한 Tool이 추가되면서 책임을 분리할 필요가 생겨서, 이를 해결하기 위해 개별 Tool의 등록 로직을 모듈로 분리하고, 관련 Tool을 패키지 단위로 관리하도록 변경했습니다.

 

이 작업을 통해 다음과 같은 점을 배웠는데

 

  1. 첫째, 코드의 규모보다 책임의 경계가 중요하다.
    1. 파일이 길어진다는 이유만으로 무조건 분리하는 것이 아니라, 어떤 이유로 코드가 변경되는지 파악해야 한다.
  2. 둘째, 모듈화는 단순한 파일 분리가 아니다.
    1. 각 모듈의 역할을 정의하고, 외부에서 어떤 인터페이스로 접근할 수 있는지 결정하는 설계 과정이다.
  3. 셋째, 디자인 패턴은 실제 문제에서 출발해야 한다.
    1. Singleton, DI, IoC, Registry 등 다양한 개념이 있지만 모든 패턴을 한 번에 적용할 필요는 없다.

이번 작업에서는 Tool 등록 책임을 분리하는 것이 가장 중요한 문제였고, 그 문제를 해결하는 데 집중했습니다.

 

이전 포스팅의 GitHub Token 중앙 관리 작업과 이번 Tool 등록 구조 개선 작업을 함께 살펴보면, MCP Server의 기능이 증가할 때 코드 구조와 의존성 관리가 어떻게 달라져야 하는지 이해할 수 있습니다.

 

오픈소스 프로젝트에서 기능을 구현하는 경험뿐 아니라, 기존 코드를 더 읽기 쉽고 유지보수하기 좋은 구조로 개선하는 경험도 중요하다는 것을 느낄 수 있었습니다!

 

참조