내가 간단한 AI 프로바이더 래퍼(wrapper)를 만든 이유 (그리고 여러분도 그래야 할 이유)
요약
다양한 AI 모델(OpenAI, Anthropic, Ollama 등)을 하나의 일관된 인터페이스로 관리하기 위해 Python의 추상 클래스를 활용한 래퍼(wrapper) 설계 방법을 소개합니다. 복잡한 if-else 조건문을 제거하고 확장성을 높이는 추상화 계층 구축 과정을 다룹니다.
핵심 포인트
- 다양한 AI 프로바이더를 통합하기 위한 추상화 계층의 필요성
- Python ABC(Abstract Base Classes)를 활용한 인터페이스 설계
- 새로운 모델 추가 시 기존 코드 수정 없이 확장 가능한 구조 구축
- 프로바이더별 인증 및 응답 형식을 정규화하는 방법
지난달로 돌아가 보겠습니다. 저는 사용자 입력으로부터 요약을 생성해야 하는 사이드 프로젝트에 몰두하고 있었습니다. 처음에는 OpenAI의 API로 시작했습니다. 아주 잘 작동했죠. 하지만 저는 사용자에게 선택권을 주고 싶다는 생각이 들었습니다. 어떤 사용자는 로컬 모델을 선호할 수도 있고, Anthropic의 Claude를 선호할 수도 있으며, 혹은 더 저렴한 것을 원할 수도 있기 때문입니다. 그래서 저는 모든 개발자가 그렇듯, 프로바이더 URL을 교체하고 잘 되기를 바랐습니다. 하지만 API는 금세 if provider == 'openai': ... elif provider == 'anthropic': ...와 같은 엉킨 코드의 덩어리로 변해버렸습니다. 새로운 프로바이더가 추가될 때마다 다섯 개의 서로 다른 파일을 수정해야 했습니다. 이는 취약했고, 테스트하기 어려웠으며, 중괄호 하나만 잘못 놓아도 운영 환경에 장애가 발생할 수 있는 상황이었습니다.
저는 호출 코드를 다시 작성하지 않고도 다양한 AI 백엔드(backend)를 플러그인처럼 연결할 수 있는 일관된 인터페이스가 필요했습니다. 프로젝트에 바로 적용하고, 한 번 설정하면 다음 새로운 모델이 나올 때까지 신경 쓰지 않아도 되는 무언가를 원했습니다.
시도했지만 실패했거나(또는 절반만 성공했던) 것들
저의 첫 번째 시도는 환경 변수(environment variable)와 적절한 요청 파라미터(request parameters)를 설정하는 거대한 if-elif 체인이었습니다. 그것은 빠르게 관리 불가능한 수준으로 커졌습니다. 그다음에는 프로바이더 이름을 함수에 매핑하는 간단한 dict 방식을 시도했습니다. 이전보다 나았지만, 각 프로바이더마다 인증(authentication), 모델 이름, 응답 형식(response formats)이 달랐습니다. 딕셔너리만으로는 이러한 차이점들을 정규화(normalize)할 수 없었습니다.
또한 인기 있는 멀티 프로바이더 라이브러리 중 하나를 사용하는 것도 고려했습니다. 하지만 그중 상당수는 무겁거나, 지나치게 독자적인 방식(opinionated)을 고수하거나, 새로운 모델이 출시되었을 때 업데이트가 늦었습니다. 저에게는 가볍고 투명하며, 500페이지짜리 문서를 읽지 않고도 스스로 확장할 수 있는 무언가가 필요했습니다.
결국 성공한 방법: 얇은 추상화 계층 (A thin abstraction layer)
저는 작은 Python 클래스 계층 구조를 만들기로 결정했습니다. 핵심 아이디어는 공통된 generate(prompt, **kwargs) 메서드를 정의하는 **기본 클래스(base class)**를 만드는 것이었습니다. 그런 다음 각 프로바이더가 해당 메서드를 구현하여, 내부적으로 자체적인 요청 포맷팅, 인증, 응답 파싱을 처리하도록 했습니다. 호출자(caller)는 그 차이를 전혀 알 수 없습니다.
핵심 인터페이스는 다음과 같습니다:
from abc import ABC, abstractmethod
class AIProvider(ABC):
...
그 다음 구체적인 구현체(concrete implementations)들을 작성했습니다. OpenAI의 경우:
import openai
class OpenAIProvider(AIProvider):
...
Anthropic의 경우 (해당 SDK 사용):
import anthropic
class AnthropicProvider(AIProvider):
...
그리고 호환 가능한 API(Ollama와 같은)를 통해 실행되는 로컬 모델의 경우:
import requests
class OllamaProvider(AIProvider):
...
이제 진짜 마법 같은 부분입니다. 설정(config)에 따라 프로바이더를 선택하는 팩토리(factory)입니다:
from typing import Dict, Type
class AIProviderFactory:
...
이를 통해 제 애플리케이션 코드는 깔끔해졌습니다:
config = {
"provider": "openai",
"api_key": "sk-...",
...
만약 내일 Anthropic으로 전환하고 싶다면, 단순히 설정 파일만 변경하면 됩니다. 코드 변경은 필요 없습니다. 테스트 또한 더 쉬워졌습니다. 정해진 응답(canned responses)을 반환하는 모의 프로바이더(mock provider)를 주입(inject)할 수 있기 때문입니다.
배운 점과 트레이드오프 (trade-offs)
이 접근 방식이 완벽한 것은 아닙니다. 제가 배운 점들은 다음과 같습니다:
- 추상화는 프로바이더별 특화 기능을 숨깁니다. 모든 모델이 동일한 파라미터를 지원하는 것은 아닙니다 (예: OpenAI의 구조화된 JSON을 위한
response_format). 제 베이스 클래스(base class)는**kwargs를 허용하지만, 어떤kwargs가 어떤 프로바이더에서 작동하는지 문서화하는 것은 유지보수의 부담이 됩니다. 제 사용 사례(단순 텍스트 생성)에서는 괜찮습니다. 만약 스트리밍(streaming), 함수 호출(function calling), 또는 이미지 입력이 필요하다면, 추상화가 더 풍부해져야 합니다. 그렇지 않으면 일부 클라이언트가 구체적인 클래스(concrete class)를 직접 사용해야 한다는 점을 받아들여야 합니다. - 버전 고정(Version pinning)이 중요합니다. 각 프로바이더 SDK는 계속 진화합니다. 저는 requirements 파일에서 버전을 고정합니다. 프로바이더를 업그레이드할 때는 모든 구현체를 다시 테스트합니다.
- 에러 핸들링(Error handling)은 프로바이더마다 다릅니다. 네트워크 타임아웃, 속도 제한(rate limits), 잘못된 요청(bad request) 에러 등은 제각각입니다. 저는 팩토리에서 일반적인 예외(generic exceptions)를 잡거나, 베이스 클래스 주변에 재시도 래퍼(retry wrapper)를 추가합니다.
- 단일 프로바이더만 사용한다면 과할 수 있습니다 (overkill). 만약 오직 OpenAI만 사용한다면, 이런 복잡성을 추가하지 마세요. 제가 이것을 만든 이유는 사용자에게 선택권을 주고, 고통 없이 로컬 모델을 실험해보고 싶었기 때문입니다.
다음에 다시 한다면 다르게 할 점
설령 프로바이더(provider)가 하나뿐이라 하더라도, 첫날부터 추상화 (abstraction)를 적용했을 것입니다. 인터페이스 (interface)를 먼저 작성하면, API가 무엇을 제공하는지가 아니라 내 코드가 AI로부터 실제로 무엇을 필요로 하는지에 대해 고민하게 됩니다. 또한, 오류를 조기에 발견할 수 있도록 처음부터 타입 스텁 (type stubs)을 추가하고 mypy를 사용했을 것입니다.
그리고 팩토리 (factory) 패턴이 정말 필요한지도 고려했을 것입니다. 작은 프로젝트의 경우, 적절한 프로바이더 인스턴스를 반환하는 간단한 함수만으로도 충분합니다. 팩토리 패턴은 플러그인 (plugin)과 같이 동적 등록 (dynamic registration)이 필요할 때 빛을 발합니다.
요약 (The takeaway)
AI 프로바이더를 교체하기 위해 무거운 프레임워크 (framework)가 필요하지는 않습니다. 수십 줄의 Python 코드만으로도 여러 백엔드 (backend)와 작업할 수 있는 깔끔하고 테스트 가능하며 유지보수 가능한 방법을 확보할 수 있습니다. 사이드 프로젝트를 만들든 프로덕션 앱을 구축하든, 여러분의 코드가 무엇에 진정으로 의존하고 있는지 생각해보세요. 라이브러리가 아니라 동작 (behavior)에 의존해야 합니다.
이제 궁금합니다. 여러분은 프로젝트에서 여러 AI 프로바이더를 어떻게 처리하시나요? 추상화 (abstract)를 사용하시나요, 아니면 그냥 하나를 선택해 그대로 밀고 나가시나요? 댓글로 알려주세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기