본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 23. 11:25

하나의 추상화로 AI 통합의 혼란을 잠재운 방법

요약

다양한 AI 모델과 API를 사용할 때 발생하는 벤더 종속성과 코드 복잡성을 해결하기 위해 Python으로 추상화 계층을 구축하는 방법을 설명합니다. 전략 패턴과 팩토리 패턴을 활용하여 모델 전환을 용이하게 만드는 설계 방식을 다룹니다.

핵심 포인트

  • 모델별 API 호출 로직을 분리하여 벤더 종속성 방지
  • 전략 패턴을 활용한 일관된 AI 프로바이더 인터페이스 구현
  • 팩토리 패턴을 통한 동적인 모델 교체 및 확장성 확보
  • 복잡한 라이브러리 없이 OOP를 이용한 경량 추상화 계층 구축

또 다른 API 호출이 타임아웃(time out)되는 것을 바라보며 터미널을 응시하던 기억이 납니다. 저의 사이드 프로젝트인 간단한 콘텐츠 요약기는 즐거워야 했습니다. 하지만 대신 저는 API 키, 속도 제한(rate limits), 그리고 모델 전환(model-switching) 로직의 늪에 빠져 있었습니다. 시도해보고 싶은 새로운 AI 서비스가 생길 때마다 코드의 절반을 다시 작성해야 했습니다. 더 나은 방법이 분명히 있을 것이었습니다.

그 주말, 저는 한 걸음 물러나 스스로에게 물었습니다. '나에게 정말 필요한 것이 무엇인가?' 저는 텍스트를 어떤 AI로 보내고 텍스트를 돌려받되, 그 AI가 OpenAI인지, 로컬 Ollama 모델인지, 아니면 무작위 블로그에서 찾은 것인지 신경 쓰지 않아도 되는 환경이 필요했습니다. 저에게는 추상화 계층(abstraction layer)이 필요했습니다.

이것은 제가 Python으로 가벼운 AI 프로바이더 인터페이스(AI provider interface)를 어떻게 구축했는지, 그리고 그것이 어떻게 저의 정신 건강을 지켜주었는지에 대한 이야기입니다. 마법도 없고, 벤더 종속(vendor lock-in)도 없으며, 그저 사려 깊은 객체 지향 프로그래밍 (OOP)과 폴백(fallback) 계획이 있을 뿐입니다.

내가 시작했던 혼란스러운 상태

"이 새로운 AI 도구를 써보자"라고 시도한 지 3주가 지난 후 제 코드는 다음과 같은 모습이었습니다:

def summarize_openai(text):
    import openai
    openai.api_key = "sk-..."
...

그 후, 메인 앱에서는 설정 플래그(config flag)에 따라 어떤 함수를 호출할지 결정하기 위해 거대한 if-elif-else 문을 가지고 있었습니다. 추하고, 취약했습니다. 새로운 프로바이더가 추가될 때마다 새로운 함수와 새로운 분기(branch)가 필요했습니다. 저는 ImportError 하나만으로도 운영 환경(production)을 망가뜨릴 수 있는 상태였습니다.

작동하지 않았던 것들

저의 첫 번째 시도는 각 프로바이더를 위한 메서드와 거대한 switch 문을 가진 단일 구조의 "AIClient" 클래스였습니다. 그것은 여전히 프로바이더를 추가할 때마다 클래스를 수정해야 함을 의미했습니다. 추상 기본 클래스 (ABC)를 사용하려고 시도했지만 메타프로그래밍 (metaprogramming)의 늪에서 길을 잃었습니다. languagemodelslangchain 같은 라이브러리들도 살펴보았지만, 저의 단순한 사용 사례에는 과했습니다(overkill). 저는 오후 한나절이면 완전히 이해할 수 있는 무언가를 원했습니다.

그러다 저는 팩토리(factory)와 결합된 **전략 패턴 (Strategy Pattern)**을 발견했습니다. 새로운 것은 아니었지만, 그것이 바로 제가 필요로 했던 것이었습니다.

마침내 작동한 것

저는 모든 AI 프로바이더가 구현해야 하는 단일 인터페이스인 간단한 클래스를 정의했습니다:

from abc import ABC, abstractmethod

class AIProvider(ABC):
...

그 다음 각 제공자(provider)를 위한 구체적인 구현체(concrete implementation)를 작성했습니다. OpenAI의 예시는 다음과 같습니다:

import openai

class OpenAIProvider(AIProvider):
...

그리고 로컬 Hugging Face 모델을 위한 구현체는 다음과 같습니다:

from transformers import pipeline

class HuggingFaceProvider(AIProvider):
...

각 제공자가 어떻게 독립적으로 유지되는지 주목해 보세요. 이들이 공유하는 유일한 것은 generate 메서드의 시그니처(signature)뿐입니다.

팩토리 (Provider Registry)

전환을 쉽게 만들기 위해, 제공자 이름과 클래스를 매핑하는 간단한 레지스트리(registry)를 구축했습니다:

class AIProviderFactory:
    providers = {}

...

이제 메인 애플리케이션 코드는 어떤 AI 제공자가 사용되고 있는지 알 필요가 전혀 없습니다. 저는 설정 파일(config file)을 읽고 적절한 객체를 생성합니다:

# config.yaml
# ai_provider: openai
# openai_api_key: sk-...
...

만약 내일 새로운 서비스—심지어 무작위 GitHub 리포지토리(repo)에서 가져온 것이라도—를 시도해보고 싶다면, 저는 단지 새로운 AIProvider 서브클래스(subclass)를 작성하고 이를 등록하기만 하면 됩니다. 나머지 코드에는 아무런 변경이 필요 없습니다.

폴백 체인 (The Fallback Chain)

제가 정말로 원했던 것 중 하나는 자동 폴백(automatic fallback)이었습니다. 만약 OpenAI가 다운되면 Hugging Face를 사용하고, 그것마저 실패하면 로컬 모델을 시도하는 방식입니다. 제가 이 패턴을 확장한 방법은 다음과 같습니다:

class FallbackAIProvider(AIProvider):
    def __init__(self, providers: list):
        self.providers = providers  # AIProvider 인스턴스들의 리스트
...

이제 제 앱은 클라이언트 코드에 추가적인 로직을 넣지 않고도 회복 탄력성(resilience)을 갖게 되었습니다.

교훈 / 트레이드오프 (Lessons Learned / Trade-offs)

  • 추상화는 유연성을 희생합니다 (Abstraction costs flexibility). 만약 스트리밍(streaming)이나 함수 호출(function calling)과 같이 특정 제공자(provider) 전용 기능이 필요하다면, 이 패턴은 다루기 까다로워집니다. 단순한 텍스트 입력/출력(text-in/text-out) 작업에는 완벽하지만, 복잡한 상호작용이 필요하다면 더 정교한 라이브러리를 고려하십시오.
  • 설정(configuration)을 코드와 분리하십시오. 환경 변수(environment variables)나 설정 파일(config files)을 사용하십시오. 제가 만든 팩토리(factory)는 키워드 인자(keyword arguments)를 기대하므로, 비밀 정보(secrets)를 깔끔하게 주입할 수 있습니다.
  • 너무 일찍 과도하게 추상화하지 마십시오. 저는 문제를 겪은 후에야 이 패턴을 구축했습니다. 코드가 이미 엉망이었기 때문에 나중에 추가하는 것이 쉬웠고, 리팩터링(refactoring)할 가치가 있었습니다.
  • 테스트가 매우 간단해집니다. 미리 준비된 문자열을 반환하는 MockAIProvider를 생성할 수 있어, 단위 테스트(unit tests)를 빠르고 결정론적(deterministic)으로 수행할 수 있습니다.

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

설령 제공자가 하나뿐이라 하더라도, 첫날부터 단일 제공자와 추상화 인터페이스(abstraction interface)로 시작했을 것입니다. 비용은 거의 들지 않으면서(몇 줄의 코드 추가) 나중에 코드를 다시 작성해야 하는 수고를 덜어줍니다. 또한 베이스 클래스(base class) 내부나 데코레이터(decorator)를 통해 재시도 로직(retry logic)과 로깅(logging)을 추가하여, 모든 제공자가 이를 기본적으로 사용할 수 있게 했을 것입니다.

말하지 않은 부분

이것을 구축하면서, 통합 AI 게이트웨이(unified AI gateway)의 예시로 InterWest AI (https://ai.interwestinfo.com/)와 같은 서비스들을 살펴보았습니다. 그들의 접근 방식은 여러 백엔드(backends)를 추상화한다는 점에서 정신(spirit)이 유사하지만, 저의 작은 프로젝트에는 간단한 Python 모듈만으로도 충분했습니다. 핵심은 제3자 솔루션(third-party solution)을 채택하기 전에 무엇이 필요한지 이해하는 것입니다. 때로는 50줄짜리 클래스가 500줄짜리 SDK보다 더 많은 일을 해냅니다.

마치며

AI 통합이 악몽이 될 필요는 없습니다. 깔끔한 인터페이스를 정의하고 전략 패턴(strategy pattern)을 사용하면, 비즈니스 로직(business logic)을 건드리지 않고도 제공자 간의 교체, 테스트 및 폴백(fallback)이 가능합니다. 이제 저의 요약기(summarizer)는 오프라인 상태일 때는 로컬 모델(local models)로 실행되고, 더 높은 품질이 필요할 때는 GPT-4로 매끄럽게 전환됩니다.

저는 여전히 이를 개선하고 있습니다. 제공자당 타임아웃(timeout)을 추가하고, 아마도 캐싱 레이어(caching layer)를 추가하고 싶습니다. 하지만 기반은 탄탄합니다.

AI 코드를 유지보수 가능하게(maintainable) 유지하기 위한 당신의 접근 방식은 무엇인가요? 이와 유사한 추상화(abstraction)를 시도해 보셨나요, 아니면 올인원 프레임워크(all-in-one frameworks)를 선호하시나요? 여러분의 경험과 그 과정에서 겪은 고난의 이야기(war stories)를 듣고 싶습니다.

AI 자동 생성 콘텐츠

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

원문 바로가기
0

댓글

0