Next.js에서 전환 가능한 AI 프로바이더 (Gemini, GPT-4, Claude, 하나의 워크스페이스 설정) 시스템을 구축한 방법
요약
특정 AI 모델에 종속되지 않고 Gemini, GPT-4, Claude 등 다양한 프로바이더를 유연하게 교체할 수 있는 Next.js 기반의 아키텍처 설계 방법을 소개합니다. 인터페이스와 팩토리 패턴을 활용하여 코드 수정 없이 워크스페이스 설정만으로 AI 모델을 전환하는 구조를 구축합니다.
핵심 포인트
- 단일 AIClient 인터페이스 정의를 통한 추상화
- 팩토리 패턴을 활용한 프로바이더 동적 생성
- 워크스페이스 설정 기반의 모델 전환 구현
- 특정 SDK에 종속되지 않는 Provider-agnostic 코드 작성
제 코드베이스의 모든 AI 함수에는 Gemini가 하드코딩되어 있었습니다. 그러던 중 한 베타 사용자가 자신의 클라이언트들이 Google에 관한 데이터 정책을 가지고 있다고 말해주었습니다. 저는 이틀 만에 전체 AI 레이어를 다시 구축했습니다. 그 아키텍처를 소개합니다.
잘못된 방식 (기존 방식)
import { GoogleGenerativeAI } from "@google/generative-ai";
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);
export async function summarizeProject(projectId: string) {
const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" });
const result = await model.generateContent(prompt);
return result.response.text();
}
다른 프로바이더 (provider)를 지원해야 할 때까지는 잘 작동합니다. 하지만 그 시점이 되면 모든 함수를 일일이 수정해야 합니다.
패턴: 하나의 팩토리 (factory), 하나의 인터페이스 (interface)
저는 모든 프로바이더가 구현해야 하는 단일 AIClient 인터페이스를 정의했습니다:
export interface AIClient {
complete: (prompt: string, systemPrompt?: string) => Promise;
}
하나의 팩토리 함수가 워크스페이스 (workspace) 설정을 읽어 적절한 클라이언트 (client)를 반환합니다:
export async function getAIClient(workspaceId: string): Promise {
const { aiProvider } = await getWorkspaceSettings(workspaceId);
switch (aiProvider) {
case "openai": return createOpenAIClient();
case "anthropic": return createAnthropicClient();
default: return createGeminiClient();
}
}
각 팩토리는 프로바이더 SDK를 동일한 인터페이스 뒤로 래핑 (wrap) 합니다:
function createAnthropicClient(): AIClient {
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });
return {
complete: async (prompt, systemPrompt) => {
const res = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 2048,
...(systemPrompt ? { system: systemPrompt } : {}),
messages: [{ role: "user", content: prompt }],
});
return res.content[0].type === "text" ? res.content[0].text : "";
},
};
}
이제 모든 AI 작업은 다음과 같이 프로바이더에 구애받지 않는 (provider-agnostic) 형태를 띱니다:
export async function summarizeProject(projectId: string, workspaceId: string) {
const ai = await getAIClient(workspaceId);
const data = await getProjectContext(projectId);
return ai.complete(buildPrompt(data), "You are a project analyst.");
}
설명할 가치가 있는 세 가지 결정 사항
사용자 수준이 아닌 워크스페이스 (Workspace) 수준. 프로젝트 데이터는 워크스페이스에 속합니다. 팀 전체가 "우리 데이터를 어떤 AI 프로바이더가 처리하나요?"라는 질문에 대해 "누가 로그인했느냐에 따라 다릅니다"가 아닌 동일한 답변을 가질 수 있어야 합니다.
조용한 폴백 (Silent fallback) 금지. 선택된 프로바이더가 실패하면 에러가 전파됩니다. 저는 다른 프로바이더로 재시도하지 않습니다. 만약 누군가 규정 준수 (Compliance)를 이유로 Claude를 선택했다면, 조용히 Gemini로 폴백하는 것은 명시적인 에러를 내는 것보다 더 나쁜 상황입니다.
모듈 수준의 캐시가 아닌 요청당 팩토리 호출 (Per-request factory call). 워크스페이스 설정은 변경될 수 있습니다. 모듈 수준의 캐싱을 사용하면 함수가 콜드 스타트 (Cold-start)될 때까지 이전 프로바이더가 계속 실행됩니다. AI 호출당 발생하는 추가적인 DB 읽기 비용은 실제 추론 (Inference) 비용에 비하면 무시할 수 있는 수준입니다.
이 패턴이 해결하지 못하는 한 가지
동일한 프롬프트에 대해 모델마다 다르게 동작합니다. Claude는 시스템 프롬프트 (System prompt)를 매우 엄격하게 따릅니다. GPT-4는 명시적인 길이 지침이 없으면 장황해지는 경향이 있습니다. 저는 이를 팩토리 내에서 프로바이더별로 얇은 프롬프트 조정 (Thin per-provider prompt adjustments)을 수행함으로써 처리합니다. 우아한 방식은 아니지만 효과적입니다.
만약 "동일한 프롬프트, 다른 모델 동작" 문제를 더 깔끔하게 해결하셨다면, 어떻게 하셨는지 궁금합니다.
Melororium 구축 중 — 프리랜서를 위한 일회성 결제 프로젝트 관리 도구. 7월 30일 출시 예정입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기