멀티테넌트 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
첫 댓글 달아줘.