본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 27. 17:15

AI 제공업체를 하드코딩하는 것을 멈추세요: 범용 클라이언트 접근 방식

요약

특정 AI 제공업체의 API에 종속되는 문제를 해결하기 위해 추상화된 범용 AI 클라이언트를 구축하는 방법을 설명합니다. 인터페이스를 통해 OpenAI, Anthropic 등 다양한 모델을 코드 수정 없이 교체할 수 있는 설계 패턴을 제안합니다.

핵심 포인트

  • 특정 API에 종속된 하드코딩은 유지보수 비용을 증가시킴
  • if-else 문을 통한 분기 처리보다 추상 기본 클래스(ABC) 활용이 권장됨
  • 인터페이스 추상화를 통해 새로운 AI 제공업체 추가가 용이해짐
  • 모델 교체 시 비즈니스 로직의 수정 없이 초기화 부분만 변경 가능

몇 달 전, 저는 사용자 대화를 요약해야 하는 챗봇을 만드는 데 몰두하고 있었습니다. 저는 OpenAI의 API로 시작했습니다. 솔직히 말해서, 우리 대부분에게 그것은 기본값이니까요. 모든 것이 잘 작동했지만, 비용과 지연 시간 (latency) 문제로 인해 팀에서 다른 모델들을 지원하기로 결정하면서 상황이 바뀌었습니다. 갑자기 저는 서로 다른 API 형식, 인증 방법 (authentication methods), 그리고 스트리밍 동작 (streaming behaviors)을 처리하기 위해 코드베이스의 절반을 다시 작성해야 했습니다.

저는 "더 나은 방법이 분명히 있을 거야"라고 생각했습니다. 그래서 저는 제공업체별 세부 사항을 추상화 (abstract)하는 범용 AI 클라이언트를 구축했습니다. 이 글에서는 저의 유지보수 시간을 몇 주나 단축해 준 접근 방식과 여러분도 어떻게 똑같이 할 수 있는지에 대해 설명하겠습니다.

문제점: API 종속 (API lock-in)

처음 시작했을 때 제 코드는 다음과 같았습니다:

import openai

def summarize(text):
...

단순하고 깔끔하며, 완전히 OpenAI에 묶여 있었습니다. 그러다 Anthropic의 Claude를 시도해보고 싶어졌습니다. 저는 함수를 복사하고, 임포트 (import)를 변경하고, 요청 형식 (request format)을 수정하여 거의 동일한 두 개의 함수를 만들게 되었습니다. 세 번째 제공업체(더 작고 저렴한 API)를 추가했을 때는 세 개가 되었습니다. API가 변경될 때마다 저는 그 모든 것을 업데이트해야 했습니다.

작동하지 않았던 것: if-else 지옥

저의 첫 번째 시도는 provider 파라미터를 가진 단일 함수였습니다:

def summarize(text, provider="openai"):
    if provider == "openai":
        # openai code
...

작동은 했지만, 지저분했습니다. 새로운 제공업체를 추가한다는 것은 함수를 수정해야 함을 의미했고, 함수는 CVS 영수증보다 더 길어졌습니다. 테스트는 악몽 같았습니다. 저는 더 깔끔한 패턴이 반드시 있을 것이라는 점을 알고 있었습니다.

해결책: 추상 기본 클라이언트 (abstract base client)

저는 각 제공업체가 구현할 범용 인터페이스 (interface)를 만들기로 결정했습니다. 핵심 아이디어는 다음과 같습니다:

from abc import ABC, abstractmethod
from typing import Dict, Any, AsyncIterator

...

그 다음 저는 구체적인 제공업체 (concrete providers)들을 구현했습니다. 여기 OpenAI를 위한 구현 예시가 있습니다:

import openai

class OpenAIProvider(AIProvider):
...

그리고 더 작은 제공업체를 위한 예시가 있습니다 (API를 https://ai.interwestinfo.com/에서 찾은 Interwest라고 부릅시다):

import httpx

class InterwestProvider(AIProvider):
...

이제 범용 클라이언트 (generic client)를 사용하는 방법은 간단합니다:

class AIClient:
    def __init__(self, provider: AIProvider):
        self.provider = provider
...

제공업체를 전환하려면 초기화 부분만 변경하면 됩니다:

# OpenAI 사용
client = AIClient(OpenAIProvider(api_key="sk-..."))

...

배운 점과 트레이드오프 (trade-offs)

이 접근 방식은 채팅 완성 (chat completions)과 같이 인터페이스가 비교적 단순할 때 매우 효과적입니다. 하지만 제공업체마다 기능(예: 함수 호출 (function calling), 이미지 생성 (image generation))이 크게 다를 경우 문제가 발생합니다. 그런 경우에는 인터페이스를 확장하거나, 해당 기능에 대해 제공업체별 전용 코드 (provider-specific code)를 사용해야 합니다.

또한, 스트리밍 (streaming) 구현 방식도 제각각입니다. 어떤 제공업체는 서버 전송 이벤트 (Server-Sent Events)를 사용하고, 다른 곳은 청크 인코딩 (chunked encoding)을 사용합니다. 추상화 (abstraction)가 도움이 되긴 하지만, 잘못된 형식의 토큰 (malformed tokens)과 같은 예외 상황은 여전히 처리해야 합니다.

또 다른 트레이드오프는 제공업체별 최적화 기능을 일부 놓칠 수 있다는 점입니다. 예를 들어, OpenAI의 API는 response_format을 JSON으로 설정할 수 있게 해주지만, 다른 곳은 그렇지 않을 수 있습니다. 이 기능이 필요하다면 인터페이스에 선택적 매개변수 (optional parameters)를 추가해야 할 수도 있습니다.

이 패턴을 사용하지 말아야 할 때

  • 단 하나의 제공업체만 사용할 예정인 경우 (하지만 정말로 확신할 수 있나요?)
  • API가 너무 달라서 추상화가 누수되는 경우 (leaky abstraction) (예: 텍스트 생성과 이미지 생성의 비교)
  • 최대 성능이 필요하며 래퍼 (wrapper)의 오버헤드를 감당할 여유가 없는 경우

하지만 대부분의 채팅 기반 애플리케이션에서 이 패턴은 저에게 수많은 시간을 절약해 주었습니다. 이제 각 제공업체를 독립적으로 테스트할 수 있고, 설정 변경만으로 교체할 수 있으며, 모델 간의 A/B 테스트까지 실행할 수 있습니다.

다음에 다시 한다면 다르게 할 점

저는 첫날부터 이 추상화를 적용했을 것입니다. 설령 하나의 제공업체만 사용할 것이라고 생각하더라도, 인터페이스를 추가하는 비용은 낮고, 전환이 필요할 때 얻는 이득은 매우 크기 때문입니다.

또한, 클라이언트 (Client) 레벨이 아닌 제공업체 (Provider) 레벨에서 더 포괄적인 에러 핸들링 (Error handling) 및 재시도 (Retries) 로직을 추가할 것을 권장합니다. 각 제공업체는 서로 다른 속도 제한 (Rate limits) 및 에러 코드 (Error codes)를 가지고 있기 때문입니다.

마지막으로, 이미 이러한 기능을 수행하고 있는 litellm과 같은 라이브러리 사용을 고려해 볼 수 있습니다. 하지만 직접 구축해 보는 과정은 관련 패턴을 학습하게 해주며 완전한 제어권을 부여합니다.

여러분의 차례

여러분의 설정은 어떤 모습인가요? 범용 클라이언트 (Generic client)를 사용하시나요, 아니면 단순히 하나의 제공업체를 선택하여 고수하시나요? 다른 분들은 이 다중 제공업체 (Multi-provider)의 혼란을 어떻게 처리하고 계신지 궁금합니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0