AI gateway 라는 추상 레이어
여러 LLM 공급자를 하나의 인터페이스로 통일하는 AI gateway 패턴을 도입하면서 얻은 것과 주의할 점을 정리한다.
서비스에 LLM 을 처음 붙일 때는 단일 공급자를 선택해서 그 SDK 를 직접 호출하는 방식이 가장 빠르다. 그런데 시간이 지나면 문제가 생긴다. 다른 모델을 A/B 테스트하고 싶거나, 특정 공급자가 장애를 일으키거나, 비용 때문에 모델을 교체해야 하거나, 태스크별로 다른 모델을 쓰고 싶을 때다. 이 시점에 공급자가 코드 전체에 퍼져 있다면 교체 비용이 크다.
AI gateway 는 이 문제를 해결하기 위한 추상 레이어다.
기본 구조
핵심 아이디어는 단순하다. 애플리케이션 코드가 특정 공급자 SDK 를 직접 호출하지 않고, 공통 인터페이스를 통해 요청을 보내면 gateway 가 실제 공급자로 라우팅한다.
[애플리케이션] → [AI gateway] → [공급자 A]
→ [공급자 B]
→ [공급자 C]
gateway 를 직접 구현할 수도 있고, 오픈소스 솔루션을 가져다 쓸 수도 있다. 우리 팀은 처음에는 간단한 래퍼 레이어를 직접 만들었다가, 이후 공개된 게이트웨이 솔루션으로 이전했다.
직접 구현했을 때의 구조
자체 구현한 래퍼는 이런 형태였다.
class LLMGateway:
def __init__(self, provider: str, model: str):
self.provider = provider
self.model = model
def complete(self, messages: list, **kwargs) -> str:
if self.provider == "openai":
return self._call_openai(messages, **kwargs)
elif self.provider == "anthropic":
return self._call_anthropic(messages, **kwargs)
raise ValueError(f"Unknown provider: {self.provider}")
설정 파일에서 provider 와 model 을 읽으면 코드 변경 없이 공급자를 바꿀 수 있다. 태스크별로 다른 설정을 쓰는 것도 쉽다.
gateway 가 제공하는 추가 기능
직접 구현한 레이어를 넘어서, 전용 게이트웨이 솔루션들이 제공하는 기능들이 있다.
통합 로깅과 비용 추적: 공급자마다 다른 로깅 형식을 통일된 포맷으로 수집한다. 태스크별, 모델별 비용을 한 곳에서 집계할 수 있다.
레이트 리미팅과 쿼터 관리: 공급자별 API 한도를 초과하지 않도록 요청을 제어한다.
캐싱: 동일한 요청에 대한 응답을 캐시해서 비용을 줄인다. 결정적 응답이 필요한 케이스에 유효하다.
폴백: 주요 공급자가 장애를 일으키면 자동으로 다른 공급자로 전환한다. 가용성이 중요한 서비스에서 유용하다.
주의할 점
추상화에는 비용이 따른다.
첫째, 모델별 고유 기능을 쓰기 어려워진다. 특정 모델만 지원하는 기능(특수한 tool use 형식, 응답 형식 옵션 등)을 게이트웨이 인터페이스에서 모두 노출하지 않는 경우가 있다. 공통 인터페이스는 교집합 기반이라 합집합을 커버하기 어렵다.
둘째, 추가 레이어는 레이턴시를 높인다. 게이트웨이가 동일 서버에 있다면 미미하지만, 외부 서비스를 쓴다면 네트워크 홉이 늘어난다.
셋째, 게이트웨이 자체가 단일 장애점이 될 수 있다. 게이트웨이가 내려가면 LLM 기능 전체가 멈춘다.
우리가 얻은 것
도입 후 가장 실감한 이점은 모델 교체 실험이 쉬워진 것이다. 설정 한 줄을 바꿔서 특정 엔드포인트만 다른 모델로 바꾸고, 품질과 비용을 비교한 뒤 결정하는 흐름이 가능해졌다. 공급자 코드가 여기저기 퍼져 있었을 때는 이런 실험 자체가 부담이었다.
AI gateway 는 LLM 을 단일 기능으로만 쓰는 초기에는 오버엔지니어링일 수 있다. 여러 모델, 여러 태스크, 비용 추적이 필요해지는 시점에 도입하면 그 가치가 분명해진다.
— by slecs