본문 바로가기

OSSCA/2025

[OSSCA 2025] MCP Server의 GitHub Token 관리 개선하기 — Singleton, DI/IoC

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

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

 

서론

이전 포스팅에서는 Githru MCP Server에 React Component를 연결하고, Storyline을 시각화하는 결과를 MCP Tool을 통해 제공한 과정들과 그 속에서의 저의 고민들을 녹여보았습니다!

 

이번에는 새로운 기능을 구현하기 전에, 앞으로 Githru MCP Tool 을 널리 발전시키고, 더욱 유지보수성이 높은 작업을 위해 기존 MCP Server의 구조를 개선한 경험을 정리한 포스팅입니다!

 

그 중에서 제가 작업한 Singleton 처리, DI/IOC 및 Tool 등록 등 다뤄보려고 합니다.

Github Token관리하기

1. 기존 구조의 문제.

Git의 메타데이터를 가져오기 위해서는 Github의 Token값이 필요합니다. 기존 githru에서는 vscode 내장 extension으로 작동을 하기에, vscode에서 제공하는 API를 통해서 github에 연결을 했었는데요, 저희 MCP 서버는 클라우드 환경에 띄워져 있어, 매 요청마다 Githru에 새로운 커넥션을 맺어줘야하기에 사용자의 token값이 커스텀하게 필요로 했습니다.

 

그래서 기존에는 MCP Tool에 입력스키마에 githubToken 값을 넣었습니다.

server.tool(
  "analyze_repository",
  {
    repoPath: z.string(),
    githubToken: z.string(),
  },
  async ({ repoPath, githubToken }) => {
    return analyzeRepository(repoPath, githubToken);
  }
);

 

근데 github Token은 사용자마다의 인증정보이기에, 이는 보안상 취약할 수 있습니다. 토큰이 tool에 노출되면, 호출 기록, 디버그 로그 등 컨텍스트에 모두 남을 수 있고, 각 Tool 모두에서 github Token을 사용하기에 관리 측면에서도 좋지 않았습니다.

 

그래서 다음과 같은 플로우를 생각했습니다.

Github Token 중앙 관리

Github token

기존에 Tool 입력 값으로 받던 githubToken을 제거하고, 공통 Config 객체에서 토큰을 가져오도록 변경했습니다.

해당 Config는 Singleton으로 관리하며, 이를 통해 MCP Host는 분석에 필요한 정보만 입력값으로 전달할 수 있습니다!

 

Config 코드

export class Config {
  private static instance: Config;
  private readonly githubToken: string;

  private constructor() {
    this.githubToken = process.env.GITHUB_TOKEN || '';
  }

  public static getInstance(): Config {
    if (!Config.instance) {
      Config.instance = new Config();
    }
    return Config.instance;
  }

  public getGithubToken(): string {
    if (!this.githubToken) {
      throw new Error("github token missing");
    }
    return this.githubToken;
  }
}

 

다음과 같은 생각을 가지고 작업을 진행했습니다.

  1. Github Token 캐싱
    1. getInstance()가 처음 불릴 때 생성자가 process.env를 읽어 readonly 필드에 고정합니다.
    2. 이후 환경변수가 바뀌어도 반영되지 않습니다.
  2. readonly
    1. githubToken을 readonly 키워드를 통해 관리
  3. Singleton
    1. 상태 없이 만약 환경 변수만 읽는 구조였다면 Singleton은 필요 없었을 것입니다.
    2. 저는 여기서 첫 호출 시점을 고정한다는 생각으로 DI에 대해 논의를 진행했습니다.

Singleton 패턴이란

Singleton 패턴에 대해서 잠시 짚고 넘어가자면, Singleton은 클래스의 인스턴스 생성을 제한하고, 하나의 인스턴스에 접근할 수 있도록 하는 디자인 패턴입니다.

 

TypeScript에서 예시를 살펴보면

class Config {
  private static instance: Config;

  private constructor() {}

  public static getInstance(): Config {
    if (!Config.instance) {
      Config.instance = new Config();
    }

    return Config.instance;
  }

  public getGithubToken(): string {
    const token = process.env.GITHUB_TOKEN;

    if (!token) {
      throw new Error("GITHUB_TOKEN is required");
    }

    return token;
  }
}

 

구성요소는 다음과 같습니다.

  1. private constructor() - 외부에서 new Config()를 호출하지 못하도록 제한.
  2. static instance - 클래스 수준에서 인스턴스 참조.
  3. getInstance() - 인스턴스가 없으면 생성하고 기존 인스턴스를 반환.
const configA = Config.getInstance();
const configB = Config.getInstance();

console.log(configA === configB); // true

이를 통해 두 변수는 동일한 객체를 참조하도록 할 수 있습니다.

 

DI / IOC

Singleton 패턴에 대한 얘기가 나올 때 가장 많이 따라나오는 개념이 DI와 IOC입니다.

의존성이란

다음 코드를 살펴보면

class GitHubAnalyzer {
  private config = Config.getInstance();

  analyze() {
    const token = this.config.getGithubToken();
    // GitHub API 호출
  }
}

GitHubAnalyzer는 내부에서 직접 Config를 가져오고 있는데 현재 구조는 특정 Config 구현에 의존하고 있습니다.

이런 구조에서는 테스트할 때 다른 Config 구현으로 교체하기가 불편할 수 있습니다.

 

의존성 역전(Dependency Injection, DI)

DI는 객체가 필요한 의존성을 내부에서 직접 생성하거나 찾아오는 대신, 외부에서 전달받도록 하는 방식입니다.

interface TokenProvider {
  getGithubToken(): string;
}

class GitHubAnalyzer {
  constructor(
    private tokenProvider: TokenProvider
  ) {}

  analyze() {
    const token =
      this.tokenProvider.getGithubToken();

    // GitHub API 호출
  }
}

// 객체 생성시 의존성 주입
const analyzer = new GitHubAnalyzer(Config.getInstance());

이 구조에서는 GithubAnalyzer에서 Config의 구체적인 생성방법을 알 필요 없이 TokenProvider를 통해서만 호출 할 수 있으며, 

테스트에서도 간단하게 구현이 가능합니다.

const fakeTokenProvider: TokenProvider = {
  getGithubToken: () => "test-token"
};

const analyzer = new GitHubAnalyzer(
  fakeTokenProvider
);

이렇게 DI는 구현 교체와 단위 테스트를 쉽게 만드는데 도움이 됩니다.

 

제어의 역전(Iversion of Control)

일반적으로 객체가 직접 의존성을 생성하고 제어하던 책임을 외부의 구성 요소로 이동시키는 설계 원칙입니다.

DI는 IoC를 구현하는 대표적인 방법 중 하나인데, 아래 구조를 비교해보면

 

// 직접 의존성 획득
class Analyzer {
  private config = Config.getInstance();
}

// 외부에서 의존성 등록
class Analyzer {
  constructor(private config: TokenProvider) {}
}

두 번째 방식에서는 객체의 생성과 연결을 외부에서 결정합니다.

이것이 DI를 통한 IoC의 핵심입니다.

 

구현 중 발견한 문제

하지만 이번 구조에서는 DI가 적용된 구조라기보다는 Singleton을 통한 의존성 획득에 가깝습니다.

이것은 추후 적용할 수 있는 설계 개선 방향입니다.

 

또한 로컬에서 테스트할 때는 정확하게 테스트를 할 수 있었지만, 원격 서버에 올릴 시 문제가 발생했습니다.

각 mcp.json에 설정하는 정보를 토대로 각 로컬 PC에 github 토큰을 넣으면 작동할 것이라고 생각했는데, 서버를 클라우드에서 기동을 할 때 환경변수로 읽어오기 때문에 이 마저도 쉽지 않았습니다. 하지만 원격 MCP server에서 mcp.json의 키 값을 읽어오는 구조로 개발하면 충분히 작업할 수 있을 것이라고 생각합니다.

 

마무리

이번 PR은 새로운 MCP Tool을 추가하는 작업은 아니었지만, 기존 Tool들이 GitHub Token을 전달받는 방식을 개선하고, 인증정보 관리 책임을 MCP Server 내부로 이동하는 리팩토링이었습니다다.

 

작업 자체는 비교적 작았지만, 이를 통해 여러 자료를 통해서 배웠던 여러 설계 개념을 다시 생각해 보면서 적용해볼 수 있는 기회였습니다.

 

특히 다음 세 가지가 기억에 남는데,

 

  1. 첫째, 인증정보는 일반적인 Tool 입력값과 분리하여 관리하는 것이 바람직하다. 
    1. MCP 환경에서는 Host, Client, Server 사이에 데이터가 전달되기 때문에 민감정보가 어떤 경로를 통과하는지 고려해야 한다.
  2. 둘째, Singleton과 DI/IoC. Singleton은 인스턴스 공유를 제어하는 패턴이고, DI는 의존성을 외부에서 주입하는 방식. Singleton을 사용한다고 자동으로 결합도가 낮아지는 것은 아님.
  3. 셋째, 디자인 패턴을 사용하는 이유를 설명할 수 있어야 한다. 단순한 환경변수 조회에 Singleton이 필요한지, 함수만으로 충분한지, 향후 어떤 책임이 추가될 수 있는지 고민하는 과정이 중요하다.

오픈소스 기여 과정에서 받은 코드 리뷰는 단순히 코드를 수정하는 데 그치지 않고, 설계 선택의 이유를 다시 생각해 보는 계기가 되었습니다.

 

앞으로도 기능 구현뿐 아니라 보안, 테스트 용이성, 유지보수성까지 고려하는 개발을 이어가는 개발자로 거듭나는 목표가 생겼습니다.

참조