모델 카탈로그: 멀티 모델 AI 앱에서 누락된 제품 계약 (Product Contract)
요약
멀티 모델 AI 애플리케이션 구축 시 제공자 어댑터만으로는 해결할 수 없는 제품 계층의 복잡성을 다룹니다. 모델별 입력 모드, 파라미터, 비용 구조 등을 통합 관리하기 위한 '모델 카탈로그'와 제품 중심의 계약(Product Contract) 설계의 필요성을 강조합니다.
핵심 포인트
- 제공자 어댑터는 호출 방식의 정규화에 집중하며 제품 규칙은 다루지 못함
- 모델별 입력 제한, 해상도, 비용 산정 방식 등은 제품 계층에서 관리해야 함
- 제품 소유의 단일 계약(Product-owned contract)인 모델 카탈로그가 필요함
- 기능을 데이터로 기술하여 모델 통합 변경에 유연하게 대응해야 함
제품에 처음 도입되는 AI 모델은 아키텍처 문제를 거의 일으키지 않습니다.
폼을 만들고, API를 호출하고, 결과를 표시하면 끝입니다.
두 번째 모델은 드롭다운과 몇 가지 조건을 추가합니다. 세 번째 모델은 또 다른 입력 모드를 추가합니다. 머지않아 제품은 text-to-image, image-to-image, text-to-video, image-to-video, 참조 기반 생성 (reference-based generation), 비디오 편집, 배경 제거, 그리고 업스케일링 (upscaling)을 지원하게 됩니다.
그 시점이 되면, 제공자 (provider)를 호출하는 것은 더 이상 가장 어려운 부분이 아닙니다.
어려운 질문들은 제품에 관한 질문들입니다:
- 각 모델은 어떤 모드를 지원하는가?
- 폼에 어떤 컨트롤을 렌더링해야 하는가?
- 사용자가 얼마나 많은 참조 에셋 (reference assets)을 업로드할 수 있는가?
- 모델이 이미지, 비디오, 오디오 또는 이들의 조합을 수용하는가?
- 어떤 파라미터 (parameter) 조합이 유효한가?
- 해상도, 재생 시간 또는 소스 미디어가 크레딧 비용에 어떤 영향을 미치는가?
- 제공자가 공개 URL이나 저장된 작업을 깨뜨리지 않고 모델의 이름을 변경할 수 있는가?
AI Image Editor를 구축하면서, 저는 제공자 어댑터 (provider adapter)가 이 문제의 일부만 해결한다는 것을 발견했습니다. 애플리케이션에는 어댑터 상위에 모델 카탈로그 (model catalog)가 필요합니다. 즉, 제품 내부에서 모든 모델이 무엇을 의미하는지를 설명하는, 제품 소유의 단일 계약 (product-owned contract)이 필요합니다.
제공자 어댑터는 잘못된 질문에 답하고 있다
제공자 어댑터는 여전히 유용합니다. 다음과 같은 작업들을 정규화 (normalize)할 수 있습니다:
interface GenerationProvider {
submit(input: ProviderInput): Promise<ProviderJob>
getStatus(jobId: string): Promise<ProviderResult>
...
애플리케이션의 나머지 부분은 한 제공자가 식별자를 taskId라고 부르고 다른 제공자가 predictionId라고 부르는지 여부를 신경 쓸 필요가 없습니다.
하지만 어댑터는 "이 서비스를 어떻게 호출하는가?"라는 질문에 답할 뿐입니다.
"이 제품이 사용자에게 무엇을 허용해야 하는가?"라는 질문에는 답하지 못합니다.
하나의 제공자 (Provider)는 완전히 다른 제품 규칙 (Product rules)을 가진 여러 모델을 노출할 수 있습니다. 어떤 이미지 모델은 16개의 참조 이미지를 허용하는 반면, 다른 모델은 4개만 허용할 수 있습니다. 어떤 비디오 모델은 비디오 입력을 허용하지만, 다른 모델은 시작 이미지(Starting image)만 허용할 수도 있습니다. 어떤 모델은 해상도(Resolution)에 따라 비용을 부과하고, 다른 모델은 출력 시간(Output duration)이나 입력 및 출력 미디어 모두에 따라 비용을 부과합니다.
이러한 차이점들은 제품 계층 (Product layer)에 속합니다.
기능을 데이터로 기술하기
유용한 카탈로그 항목은 다음과 같은 형태를 가질 수 있습니다:
type ModelCatalogEntry = {
id: string
kind: 'image' | 'video'
...
이 객체는 제공자 응답 (Provider response)의 복사본이 아닙니다. 이는 애플리케이션이 정의한 모델 자체의 정의입니다.
내부 id와 공개 slug는 제품에 속합니다. 제공자별 모델 이름은 providerRoutes 내부에 위치해야 합니다. 나중에 통합 (Integration) 방식이 변경되더라도, 애플리케이션이 데이터베이스 기록, 공개 페이지 URL, 프론트엔드 상태 (Frontend state)를 다시 작성할 필요가 없어야 하기 때문입니다.
제품 정체성 (Product identity)은 안정적이어야 합니다. 제공자 정체성 (Provider identity)은 교체 가능해야 합니다.
기능을 기반으로 폼(Form) 생성하기
카탈로그가 없다면, 모델 폼은 종종 늘어나는 조건문들의 집합이 되어버립니다:
if (model === 'model-a') {
showResolution()
}
...
카탈로그 규모가 작을 때는 이런 방식이 직관적으로 느껴질 수 있습니다. 하지만 모델의 이름이 변경되거나, 업그레이드되거나, 일부 기능만 공유하게 될 경우 시스템은 취약해집니다.
기능 중심의 폼 (Capability-driven form)은 대신 항목을 읽어 들입니다:
modes: 텍스트-투-이미지 (Text-to-image), 이미지-투-이미지 (Image-to-image), 또는 비디오 편집 (Video-edit) 탭을 렌더링합니다.fields: 해상도 (Resolution), 종횡비 (Aspect ratio), 지속 시간 (Duration), 오디오 컨트롤을 렌더링합니다.defaults: 폼을 초기화합니다.maxReferenceAssets: 업로더를 제어합니다.sourceAssetAccept: 미디어 유형을 제한합니다.
모델을 추가하는 작업은 모델 이름을 알고 있는 모든 컴포넌트를 찾아다니는 대신, 주로 데이터 변경과 그에 따른 계약 테스트 (Contract tests)를 수행하는 과정이 됩니다.
프론트엔드 또한 의도적으로 단순함을 유지합니다. 제공자의 용어를 학습하지 않고도 제품의 기능들을 렌더링할 뿐입니다.
서버에서도 동일한 계약 강제하기
카탈로그로부터 UI를 생성하는 것만으로는 충분하지 않습니다. 만약 서버가 동일한 규칙을 사용하지 않는다면, 카탈로그는 계약 (Contract)이라기보다는 단순한 표시 설정 (Display configuration)에 불과하게 됩니다.
요청을 받은 후, 서버는 다음 사항들을 확인해야 합니다:
- 모델이 존재하며 요청된 미디어 종류 (Media kind)와 일치하는지 여부.
- 요청된 모드 (Mode)가 해당 모델에 속해 있는지 여부.
- 선택된 모드가 제공된 참조 (References)를 수용할 수 있는지 여부.
- 에셋 (Assets)의 개수와 종류가 허용되는지 여부.
- 동적 필드 (Dynamic fields)에 선언된 값만 포함되어 있는지 여부.
- 필드 간 조합 (Cross-field combinations)이 호환되는지 여부.
서버는 렌더링된 폼 (Rendered form)을 신뢰할 수 없습니다. 요청은 오래된 브라우저 탭, 이전 버전의 배포 환경, 또는 엔드포인트 (Endpoint)를 직접 호출하는 클라이언트로부터 올 수 있기 때문입니다.
더 안전한 흐름은 다음과 같습니다:
request schema
|
v
...
그러면 워커 (Worker)는 가공되지 않은 폼 상태 (Raw form state) 대신 정규화된 제품 입력 (Normalized product input)을 받게 됩니다.
가격 책정 또한 계약에 포함되어야 합니다
"생성당 10 크레딧"과 같은 단순한 규칙은 멀티 모델 제품에서 유지되기 어렵습니다.
이미지 비용은 해상도 (Resolution)에 따라 달라질 수 있습니다. 비디오 비용은 재생 시간, 품질, 오디오, 그리고 사용자가 비디오 입력을 제공했는지 여부에 따라 달라질 수 있습니다. 또한 제공자 (Provider)의 가격 책정은 시간이 지남에 따라 변합니다.
카탈로그 가격 책정은 버전 관리되는 매칭 규칙 (Versioned matching rules)으로 표현될 수 있습니다:
type CreditCostTier = {
credits: number
effectiveAt: string
...
리졸버 (Resolver)는 먼저 작업 생성 시점에 유효한 최신 가격 버전을 선택합니다. 그 다음, 정규화된 입력과 일치하는 가장 구체적인 티어 (Tier)를 선택합니다.
여기서 두 가지 세부 사항이 중요합니다.
첫째, 작업이 생성 파이프라인 (Generation pipeline)에 진입하기 전에 비용을 계산하고 영구 저장 (Persist)해야 합니다. 작업이 대기하는 동안 가격이 업데이트되더라도 사용자가 이미 확인한 내용은 변경되지 않아야 합니다.
둘째, UI 추정치와 서버 청구 금액은 동일한 리졸버를 사용해야 합니다. 버튼에 표시된 가격과 결제 시스템에 기록된 가격이 다르면 신뢰를 빠르게 잃게 됩니다.
타입뿐만 아니라 카탈로그 시맨틱을 테스트하세요
TypeScript는 엔트리 (Entry)의 형태 (Shape)를 검증할 수 있습니다. 하지만 해당 엔트리가 논리적으로 타당한지는 증명할 수 없습니다.
A correctly typed configuration can still contain:
- 옵션에서 누락된 기본값 (default value)
- 중복된 퍼블릭 슬러그 (public slug)
- 커버되지 않은 해상도-기간 가격 조합 (resolution-duration price combination)
- 참조 제한이 0인 참조 활성화 모드 (reference-enabled mode)
- 잘못된 미디어 종류 (media kind)를 가리키는 프로바이더 경로 (provider route)
- 의도치 않게 과거 계산을 변경하는 새로운 가격 버전 (price version)
카탈로그는 집중적인 테스트가 필요합니다:
모든 기본값은 허용된 옵션이어야 함
모든 퍼블릭 슬러그는 고유해야 함
모든 지원되는 모드는 성공적으로 정규화 (normalization) 되어야 함
...
이러한 테스트는 리뷰를 더 명확하게 만들어 줍니다. 모델 통합은 관련 없는 파일들에 흩어진 특수 사례 (special cases)의 흔적이 아니라, 기능(capabilities)에 대한 가시적인 선언이 됩니다.
모든 것을 JSON으로 만들지 마세요
카탈로그가 있다고 해서 모든 차이점을 정적 설정 (static configuration)으로 강제해야 한다는 의미는 아닙니다.
어떤 모델들은 필드 간의 실제 제약 조건 (cross-field constraints)을 가지고 있습니다. 어떤 비디오 가격은 업로드된 소스 에셋 (source assets)의 측정된 길이에 따라 달라집니다. 이러한 규칙은 카탈로그의 퍼블릭 API (public API)를 통해 호출되는 명시적인 순수 함수 (pure functions)로 표현하는 것이 더 쉽습니다.
실질적인 경계는 다음과 같습니다:
- 기능 (capabilities), 옵션 (options), 기본값 (defaults), 그리고 경로 (routes)는 데이터입니다.
- 정규화 (normalization), 필드 간 검증 (cross-field validation), 그리고 동적 비용 (dynamic cost)은 순수 함수 (pure functions)입니다.
- 요청 변환 (request translation) 및 응답 파싱 (response parsing)은 프로바이더 어댑터 (provider adapters)에 머뭅니다.
- 재시도 (retries) 및 최종 결제 (final settlement)는 작업 워크플로우 (job workflow)에 머뭅니다.
목표는 코드를 제거하는 것이 아닙니다. 모든 차이점에 대해 하나의 권위 있는 거처를 제공하는 것입니다.
보상은 예측 가능성입니다
멀티 모델 AI 제품의 복잡성은 API 클라이언트의 수에서 오는 것이 아닙니다. 동일한 제품 흐름을 가로지르는 기능 조합의 수에서 옵니다.
프로바이더 어댑터 (provider adapters)는 통합의 차이점을 숨깁니다. 모델 카탈로그는 애플리케이션의 나머지 부분에 이러한 차이점에 대한 일관된 해석을 제공합니다.
카탈로그가 실제 제품 계약 (product contract)이 되면, 모델을 추가하는 것은 더 이상 "모든 곳에 또 다른 예외를 추가하는 것"을 의미하지 않습니다. 그것은 "우리가 이미 이해하고 있는 경계 내에서 새로운 기능 세트를 선언하는 것"이 됩니다.
그러한 예측 가능성은 다음 통합 (integration) 과정에서 몇 분을 절약하는 것보다 더 가치 있는 일입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기