내가 AI API 호출을 하드코딩하는 것을 멈추고 간단한 추상화 계층을 구축한 이유
요약
AI API를 직접 하드코딩하는 대신 전략 패턴을 활용하여 다양한 LLM 제공업체를 통합할 수 있는 추상화 계층을 구축하는 방법을 설명합니다. 이를 통해 코드의 유지보수성을 높이고 새로운 모델 추가 시 발생하는 복잡성을 해결할 수 있습니다.
핵심 포인트
- 특정 API에 종속된 하드코딩 방식의 위험성 경고
- 전략 패턴을 활용한 공통 인터페이스 설계
- 확장성을 고려한 제공업체 추상화 패턴 도입
- 팩토리 패턴을 통한 런타임 프로바이더 선택
내 접근 방식을 바꿔야겠다고 느낀 순간
수요일 밤 11시였습니다. 개발자들이 문서 작성을 돕는 작은 도구인 제 사이드 프로젝트에 GPT-4를 막 통합한 참이었습니다. OpenAI와 함께 모든 것이 완벽하게 작동하고 있었습니다. 그때 클라이언트가 말했습니다. "규정 준수 요구 사항 때문에 Anthropic의 Claude도 지원해야 합니다."
제 코드를 살펴보았습니다. OpenAI의 API에 직접 HTTP 호출을 수행하는 ask_gpt라는 단일 함수가 있었습니다. 그 함수는 API 키 확인, 모델 이름, temperature (온도) 설정 등으로 가득 차 있었습니다. 다른 제공업체를 추가하려면 전체를 복사하여 붙여넣고, 이름을 ask_claude로 변경하고, 엔드포인트(endpoint)를 수정해야 했습니다. 이는 폭발하기를 기다리는 유지보수의 악몽이었습니다.
익숙하신가요? 여러분도 분명 이런 경험이 있을 것입니다.
시도했지만 실패했던 것들
처음에는 당연한 것, 즉 환경 변수(environment variables)를 시도했습니다. AI_PROVIDER=openai가 포함된 설정 파일을 만들고 모든 곳에서 if-else 블록을 사용했습니다. 정확히 두 개의 제공업체까지는 작동했습니다. 그러다 세 번째 제공업체(Cohere)가 등장하자, 코드는 조건부 로직의 늪으로 변했습니다. 새로운 제공업체가 추가될 때마다 대여섯 개의 파일을 수정해야 했습니다.
# 순진한 접근 방식 — 이렇게 하지 마세요
def ask_ai(prompt):
provider = os.getenv("AI_PROVIDER")
...
이 방식은 취약하고, 테스트하기 어려우며, 무언가를 망가뜨리지 않고 확장하는 것이 불가능합니다. 저에게는 더 나은 패턴이 필요했습니다.
결국 성공한 방법: 제공업체 추상화 패턴 (Provider Abstraction Pattern)
머리를 싸매고 고민한 끝에, 저는 한 걸음 물러나 제가 정말로 필요로 하는 것이 모든 AI 제공업체를 위한 **공통 인터페이스 (common interface)**라는 것을 깨달았습니다. 즉, "프롬프트(prompt)를 주면 완료(completion)를 주겠다. 어떤 백엔드가 실행 중인지는 상관하지 않겠다"라고 말하는 무언가 말입니다.
저는 가벼운 추상화 계층을 구축했습니다. 프레임워크는 아니며, 전략 패턴 (Strategy pattern)을 따르는 몇 개의 클래스일 뿐입니다. 핵심 아이디어는 다음과 같습니다:
from abc import ABC, abstractmethod
class AIProvider(ABC):
...
그런 다음 각 API에 대한 구체적인 제공업체(concrete providers)를 구현했습니다. OpenAI의 경우:
import openai
class OpenAIProvider(AIProvider):
...
Anthropic의 경우:
import anthropic
class AnthropicProvider(AIProvider):
...
이제 메인 애플리케이션 코드는 특정 API를 직접 다루지 않습니다. 그저 프로바이더 (provider)를 요청할 뿐입니다:
class AIClient:
def __init__(self, provider: AIProvider):
self._provider = provider
...
마법 없이 설정 가능하게 만들기
런타임 (runtime)에 프로바이더를 선택하기 위해, 환경 변수나 설정 파일 (config file)을 읽는 간단한 팩토리 (factory)를 만들었습니다. 다음은 예시 설정 구조입니다 (시작 시점에 단 한 번만 사용됨):
import json
# config.json: {"provider": "anthropic", "api_key": "...", "model": "claude-2"}
...
주석이 보이시나요? 제품 URL이 나타나는 유일한 곳이 바로 여기입니다. 진짜 영웅은 추상화 패턴 (abstraction pattern) 그 자체입니다.
배운 점과 트레이드오프 (trade-offs)
이 접근 방식이 완벽하지는 않습니다. 제가 발견한 점들은 다음과 같습니다:
- 오버엔지니어링 (Over-engineering): 만약 단 하나의 프로바이더만 필요하다면 (그리고 그것이 확실하다면), 이 방식은 과합니다. YAGNI (You Ain't Gonna Need It) 원칙이 적용됩니다.
- API 분기 (API divergence): 어떤 프로바이더는 스트리밍 (streaming)을 지원하지만, 어떤 곳은 지원하지 않습니다. 어떤 곳은 채팅 (chat) 엔드포인트를, 어떤 곳은 완성 (completion) 엔드포인트를 가집니다. 저의 단순한
complete메서드는 이러한 차이점을 숨기는데, 이는 인터페이스 (interface)를 통해 노출하지 않는 한 고유한 기능들에 접근할 수 없음을 의미합니다. 결국 저는stream이나functions같은 선택적 파라미터를kwargs로 추가하게 되었지만, 깔끔한 방식은 아닙니다. - 에러 핸들링 (Error handling): 각 프로바이더는 서로 다른 예외 (exceptions)를 던집니다. 저는 이를 공통된
AIError베이스로 감쌌지만, 이는 매핑 (mapping) 레이어를 하나 더 추가하게 됩니다. - 테스트 (Testing): 모킹 (Mocking)이 매우 아름다워집니다. 고정된 문자열을 반환하는 모의 프로바이더 (mock provider)를 주입하기만 하면 됩니다.
이 접근 방식이 나쁜 경우는 언제일까요? 만약 사용 사례가 프로바이더 특유의 기능(예: Anthropic의 긴 컨텍스트 (long context) 또는 OpenAI의 함수 호출 (function calling))에 크게 의존한다면, 추상화는 누수될 수 있습니다 (leaky abstraction). 결국 클라이언트 내부에서 다시 if-else 블록을 작성하게 될 것입니다.
다음에 한다면 다르게 할 점
저는 훨씬 더 단순하게 시작했을 것입니다. 처음부터 완전한 추상화 팩토리 (abstraction factory)를 구축하는 대신, 첫 번째 프로바이더 (provider)는 구체적으로 작성하되 인터페이스 (interface)를 염두에 두었을 것입니다. 두 번째 프로바이더가 필요해지는 즉시 위에서 언급한 패턴으로 리팩터링 (refactor)했을 것입니다. 그리고 팩토리가 switch 문 폭발 (switch statement explosion)로 커지는 것을 방지하기 위해, 아마도 의존성 주입 컨테이너 (dependency injection container)와 같은 더 정교한 설정 방식을 사용했을 것입니다.
또한, 속도 제한 (rate limiting)과 재시도 (retries)에 대해서도 더 일찍 고민했어야 했습니다. 각 프로바이더는 서로 다른 속도 제한을 가지고 있습니다. 일반적인 추상화 (generic abstraction)로는 이를 투명하게 처리할 수 없습니다. 다음에는 프로바이더 레벨에서 재시도/속도 제한 데코레이터 (retry/rate-limit decorator)를 추가하겠습니다.
이 추상화 패턴 덕분에 세 번째 AI 프로바이더가 등장했을 때 앱 전체를 다시 작성해야 하는 상황을 피할 수 있었습니다. 이것은 엄청난 기술 (rocket science)이 아닙니다. 그저 현대적인 AI API에 적용된 오래된 객체 지향 프로그래밍 (OOP) 디자인 패턴일 뿐입니다.
여러분의 다중 AI 프로바이더 처리 설정은 어떠신가요? LangChain 같은 것을 사용하시나요, 아니면 직접 구현하시나요? 다른 분들은 이 문제를 어떻게 해결하는지 궁금합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기