AI를 활용하는 서비스를 만들다 보면 처음에는 보통 하나의 모델만 사용한다.
예를 들어:
- AWS Bedrock Claude
- Azure OpenAI GPT
- Google Vertex AI Gemini
- 기타 등등..
하지만 서비스가 커지면 대부분 이런 요구가 생긴다.
- Provider를 여러 개 지원해야 한다
- 모델을 상황에 따라 바꿔야 한다
- 이미지 / 문서 같은 입력도 처리해야 한다
이때 단순하게 구현하면 코드가 금방 이런 형태가 될 것이다..
if provider == "aws":
client = ChatBedrockConverse(...)
elif provider == "azure":
client = AzureChatOpenAI(...)
elif provider == "google":
client = ChatVertexAI(...)
이렇게 코드가 늘어나는 게 다가 아니라.. 또 각 모델마다 아래와 같은 요소들이 전부 다르다.
- 인증 방식
- 클라이언트 생성 방식
- 메시지 포맷
- 파일 전달 방식
- 지원 기능 (이미지 / 문서 등)
- 등등...
결국 코드가 점점 이런식으로 변할 것이다 🤦♀️
if provider == "aws":
if image:
...
if document:
...
elif provider == "azure":
...
프로바이더 하나 더 늘어나는 순간 코드 복잡도가 급격히 올라가고, 관리 포인트도 점점 늘어나게 될 것이다.. 으악!
이럴 때는 여러 AI 프로바이더를 하나의 인터페이스로 통합하는 구조를 활용해 보면 된다.
전체 아키텍처

Provider 차이를 비즈니스 로직에서 제거하고
내부 서비스 레이어에서 처리한다.
비즈니스 로직은 단순히 이렇게 호출하면 된다.
llm_service.generate(model="claude-sonnet", messages=messages)
Provider가 무엇인지, 인증이 무엇인지, 파일 포맷이 무엇인지 몰라도 된다.
1. Model Registry 패턴
가장 먼저 해결해야 하는 문제는 모델 관리다.
각 모델은 다음과 같은 정보가 필요하다.
- 어떤 Provider인지
- 실제 API 모델 ID
- 지원 기능 (이미지 / 문서)
- 과금 정보
- 대체 모델
이 정보를 코드 곳곳에 흩어놓으면 유지보수가 매우 어려워진다.
그래서 모든 모델 메타데이터를 한곳에서 관리하는 레지스트리 패턴을 사용했다.
class LLMModelInfo(BaseModel):
key: LLMModelKey
service_provider: LLMProvider
model_id: str
region_name: str
image_support_type: ImageSupportType
document_support_type: DocumentSupportType
document_support_extention_list: List[str]
alter_model_for_image: Optional[LLMModelKey]
input_price_per_token: float
output_price_per_token: float
모델 등록은 이런식으로..
class LLMConfig:
def __init__(self):
self._registry = {}
self._add(LLMModelInfo(
key=LLMModelKey.CLAUDE46_SONNET,
service_provider=LLMProvider.AWS,
model_id="global.anthropic.claude-sonnet-4-6",
region_name="us-west-2",
image_support_type=ImageSupportType.BINARY
))
self._add(LLMModelInfo(
key=LLMModelKey.AZURE_GPT52,
service_provider=LLMProvider.AZURE_OPENAI,
model_id="gpt-5.2",
region_name="eastus2",
image_support_type=ImageSupportType.URL
))
...
장점
- 모델 추가 → _add() 한 줄
- 모델 비활성화 → is_in_use=False
- 기능 지원 여부 확인 → model_info 조회
즉 모델 관리가 코드가 아니라 데이터가 된다.
2. 자동 모델 Fallback
일부 모델은 이미지나 문서를 지원하지 않는다.
예를 들어
- Claude 3.5 Haiku → 이미지 미지원
- o4-mini → 이미지 미지원
사용자가 이미지를 첨부했는데 이런 모델을 선택하면 보통은 에러가 발생한다.
하지만 더 좋은 UX는 자동으로 지원 모델로 전환하는 것이다.
레지스트리에서 대체 모델을 선언할 수 있겠다.
LLMModelInfo(
key=LLMModelKey.CLAUDE35_HAIKU,
image_support_type=ImageSupportType.UNSUPPORTED,
alter_model_for_image=LLMModelKey.CLAUDE37_SONNET
)
처리 로직은 단순하다.
if has_images and model_info.image_support_type == ImageSupportType.UNSUPPORTED:
if model_info.alter_model_for_image:
model_info = self.llm_config.get(model_info.alter_model_for_image)
이렇게 하면
사용자 요청: Haiku + Image
↓
자동 전환: Sonnet
이런 식으로 처리가 될 거고, 사용자는 모델 변경을 알 필요가 없다.
3. Provider별 인증 분리
Provider마다 인증 방식이 완전히 다르다.
| Provider | 인증 방식 |
| AWS Bedrock | Access Key + Secret Key |
| Azure OpenAI | Endpoint + API Key |
| Google Vertex AI | Service Account JSON |
이 로직이 LLM 서비스에 섞이면 코드가 매우 지저분해진다.
그래서 인증 정보를 외부에서 주입하는 구조를 사용했다.
def _create_llm_client(self, model_info, credentials):
if model_info.service_provider == LLMProvider.AWS:
return ChatBedrockConverse(
model_id=model_info.model_id,
region_name=model_info.region_name,
**credentials
)
elif model_info.service_provider == LLMProvider.AZURE_OPENAI:
return AzureChatOpenAI(
azure_endpoint=credentials["endpoint"],
openai_api_key=credentials["api_key"],
deployment_name=model_info.model_id
)
elif model_info.service_provider == LLMProvider.GOOGLE:
return ChatVertexAI(
model=model_info.model_id,
location=model_info.region_name,
credentials=credentials["credentials"]
)
이 구조의 장점
- 인증 방식 변경 → 인증 모듈만 수정
- Provider 추가 → 클라이언트 생성 분기만 추가
- LLM 서비스는 인증 방식을 전혀 모른다
4. Provider별 메시지 Strategy
Provider마다 메시지 포맷도 다르다.
예를 들어
- AWS → binary image
- Azure → image URL
- Google → 자체 content 구조
그래서 Strategy 패턴을 사용해 Provider별 메시지 변환을 분리했다.
def _process_messages_by_provider(self, message_list, model_info):
for message in message_list:
if service_provider == LLMProvider.AWS:
processed = self._process_for_aws(message)
elif service_provider == LLMProvider.AZURE_OPENAI:
processed = self._process_for_azure(message)
elif service_provider == LLMProvider.GOOGLE:
processed = self._process_for_google(message)
return processed_messages
이렇게 하면
- 비즈니스 로직은 메시지를 통일된 형식으로 전달
- 내부에서 Provider별로 변환
즉 Provider 차이를 내부에서 흡수한다.
5. Provider가 파일을 직접 지원하지 않을 때
예를 들어 Azure OpenAI는 문서 파일을 직접 전달할 수 없다고 가정해 보자.
이런 경우가 있으면 서버에서 문서를 파싱 한 뒤 텍스트로 전달하는 방식으로 처리할 수 있을 것 같다.
def _parse_documents_parallel(self, files, file_service):
# 병렬처리
with ThreadPoolExecutor(max_workers=5) as executor:
futures = {
executor.submit(file_service.parse_document, f): f for f in files
}
return {
futures[f].key: f.result()
for f in as_completed(futures)
}
6. 선택적 재시도 전략
Provider API는 다양한 오류를 반환한다.
예를 들어
- 500
- 503
- Throttling
- Timeout
이 경우 지수 백오프 재시도를 적용할 수 있다
RETRYABLE_ERRORS = [
"serviceunavailableexception",
"throttlingexception",
"timeout",
"500",
"503"
]
재시도 로직
retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=1, max=10)
)
단, 모든 에러를 무작정 재시도할 필요는 없다.
예를 들어 Azure에서 내는 콘텐츠 필터 오류 같은 경우는 재시도해도 동일한 결과가 나온다.
|
Error code: 400 - {'error': {'message': "The response was filtered due to the prompt triggering Azure OpenAI's content management policy. Please modify your prompt and retry. To learn more about our content filtering policies please read our documentation: https://go.microsoft.com/fwlink/?linkid=2198766", 'type': None, 'param': 'prompt', 'code': 'content_filter', 'status': 400, 'innererror': {'code': 'ResponsibleAIPolicyViolation', 'content_filter_result': {'hate': {'filtered': False, 'severity': 'safe'}, 'jailbreak': {'filtered': False, 'detected': False}, 'self_harm': {'filtered': False, 'severity': 'safe'}, 'sexual': {'filtered': True, 'severity': 'medium'}, 'violence': {'filtered': False, 'severity': 'safe'}}}}} 201
|
그래서 에러 유형별 선택적 재시도를 적용했다.
전체 요청 흐름

정리
AI Provider를 여러 개 지원할 때 중요한 원칙은 하나다.
Provider 차이를 비즈니스 로직에 노출시키지 않는다.
이번 구조에서 사용한 패턴은 다음과 같다.
| 문제 | 해결 패턴 |
| 모델 관리 | Registry Pattern |
| 기능 미지원 | Auto Fallback |
| 인증 차이 | Auth Injection |
| 메시지 포맷 차이 | Strategy Pattern |
| API 오류 처리 | Selective Retry |
이 구조를 사용하면
- 새로운 Provider 추가
- 새로운 모델 추가
- 기능 확장
을 비즈니스 로직 수정 없이 처리할 수 있다.
멀티 Provider 환경에서 LLM 서비스를 운영해야 한다면
이런 방식의 구조화를 고려해 볼 만하다. 빠쌰👍
'Python' 카테고리의 다른 글
| fara-7b 모델 테스트 과정 끄적끄적.. (0) | 2025.12.01 |
|---|---|
| SQS 장시간 작업 처리 (0) | 2025.04.18 |
| Flask + LangGraph 환경에서 LLM Streaming 처리 구조 (0) | 2025.03.27 |
| LLM 시스템 프롬프트 캐싱 구조 (0) | 2024.03.09 |
| Claude, GPT, Gemini 이미지 입력 방식 비교 (0) | 2024.03.01 |