개발 slecs

멀티테넌트 LLM 프록시로 테넌트별 사용량 추적 구현

목차

멀티테넌트 환경에서 외부 LLM API 요청을 투명하게 프록시하면서 동시에 사용량을 추적하는 구조를 만들었다. 초반에는 간단한 작업처럼 보였지만, 팀 전체의 비용 관리와 테넌트별 할당량 제어에 영향을 미치는 꽤 중요한 인프라 변경이었다.

투명 프록시의 필요성

우리 시스템은 여러 고객(테넌트)을 동시에 지원하는 구조인데, 각 테넌트가 외부 LLM API를 직접 호출할 수 없다. 클라이언트에서 직접 API 키를 노출하는 것도 보안 리스크고, 각 테넌트가 API 요청을 얼마나 하는지 추적해야 비용 분배와 요금 청구가 가능해진다. 투명 프록시라는 건 클라이언트 입장에서는 평상시처럼 API를 호출하지만, 실제로는 우리 서버를 거쳐서 가도록 하는 방식이다.

/anthropic/<tenant>/* 경로 구조를 선택한 건 URL 차원에서 테넌트를 명시하기 위함이다. 라우팅이 간단하고, 로그와 모니터링에서도 어느 테넌트의 요청인지 한눈에 파악할 수 있다. 원본 API 엔드포인트 구조를 최대한 유지하면서(/* 와일드카드 사용) 테넌트 정보만 삽입하는 방식이라, 클라이언트 코드 변경을 최소화할 수 있었다.

구현 레벨의 선택들

새 파일 anthropic-proxy.ts 를 만들었는데, 별도 모듈로 분리한 이유는 명확한 책임 분리 때문이다.

관점 이유
단일 책임 프록시 로직이 메인 서버 파일에 섞이면 나중에 이해하고 수정하기 어려워짐
테스트 용이성 proxy 모듈을 독립적으로 테스트하고, anthropic-proxy.test.ts 로 단위 테스트 작성 가능
재사용성 다른 프록시(예: OpenAI, 내부 API)를 추가할 때 패턴을 따라갈 수 있는 구조

server.ts 에서는 이 프록시를 마운트하고 라우팅을 연결하는 정도로만 처리했다. 팀 내에서 나중에 유지보수할 때 "프록시 로직이 뭔데?" 하고 물어오면 anthropic-proxy.ts 파일을 보라고 하면 되니까 심플하다.

사용량 캡처 설계의 고민

"usage capture" 가 핵심인데, 투명 프록시 입장에서는 다음을 추적해야 한다:

  • 요청을 보낸 테넌트 ID
  • 요청 시각, 응답 코드, 응답 시간
  • 토큰 사용량 (input/output 토큰)
  • API 호출 실패 여부

외부 API 응답에 포함된 사용량 정보(예: usage.input_tokens, usage.output_tokens)를 파싱해서 DB나 로깅 시스템에 기록하는 방식이다. 이게 있어야 나중에 "이 테넌트가 이번 달에 얼마나 썼는가" 를 계산할 수 있고, 할당량 초과 시 차단할 수도 있다.

// 개념적 흐름 (실제 구현과는 다를 수 있음)
const response = await callAnthropicAPI(request);
const usage = response.usage; // input_tokens, output_tokens

await logUsage({
  tenantId,
  inputTokens: usage.input_tokens,
  outputTokens: usage.output_tokens,
  timestamp: new Date(),
  status: response.status
});

return response; // 클라이언트에 투명하게 반환

이런 구조면 사용량 추적이 프록시 계층에서 일관되게 일어나므로, 클라이언트가 직접 API를 호출하든 뭘 하든 상관없이 우리 시스템을 거치는 모든 요청을 捕捉할 수 있다.

테스트 전략과 팀 리딩

anthropic-proxy.test.ts 를 작성할 때는 여러 시나리오를 고려했다:

  • ✅ 정상 요청 → 응답 그대로 반환 + 사용량 기록
  • ✅ 인증 실패 (테넌트 유효하지 않음) → 401 반환
  • ✅ API 타임아웃 → 에러 처리 및 로깅
  • ✅ 사용량 정보 파싱 실패 → graceful fallback (로그만 남기고 응답은 전달)

마지막 케이스가 중요한데, 사용량 추적 실패가 사용자 요청 자체를 막아서는 안 된다는 뜻이다. 추적 시스템이 다운 되더라도 API 통신은 계속 되어야 하니까. 이건 팀 내 논의에서 "프로덕션 안정성 vs 추적 완전성" 트레이드오프를 어떻게 할 것인가 하는 부분이었다. 우리는 안정성을 우선하기로 했고, 추적 실패는 모니터링 알람으로 감지하기로 했다.

패키지 정리와 의존성

package.json / package-lock.json 변경이 생겼다면, 프록시 구현을 위해 새 라이브러리가 추가되었거나 버전 업을 한 것 같다. 예를 들어, HTTP 클라이언트 재시도 로직이 필요하면 axios-retry 같은 패키지를 쓸 수도 있고, 타입 정의를 위해 @anthropic-sdk/types 같은 게 필요할 수도 있다. 이런 의존성 추가는 항상 신중해야 하는데, 그 패키지가 얼마나 active 하게 유지되는지, 보안 이슈는 없는지, 번들 사이즈에 얼마나 영향을 주는지 체크해야 한다.

회고

이 작업을 통해 느낀 건, 프록시 패턴이 멀티테넌트 아키텍처에서 얼마나 강력한지다. 클라이언트 코드는 거의 변경하지 않으면서도 서버 레벨에서 인증, 로깅, 할당량 제어 같은 기능을 추가할 수 있다. 다만 프록시 레이어 자체가 병목이 될 수 있다는 것도 기억해야 한다. 추후에 메트릭을 모니터링해서 응답 시간이 기하급수적으로 늘어나진 않는지 확인해야 한다.

투명하다는 게 중요한 부분인데, 사용자 입장에서는 프록시가 있다는 걸 모르고 평상시처럼 API를 쓸 수 있어야 한다. 에러 메시지도 원본 API 응답 그대로 전달해야 혼동이 없다. 이런 세세함들이 모여서 좋은 인프라가 되는 거 같다.


🛒 이 글과 어울리는 추천 상품

*위 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.

댓글 0

첫 댓글 달아줘.