1) 지능형 매칭 파이프라인

💡 쉽게 말하면: 사용자가 검색 버튼을 누른 순간부터 화면에 국악 3곡이 뜰 때까지, 요청이 어떤 계층을 어떤 순서로 지나가는지를 한 줄기로 그린 문서입니다. 핵심은 “AI가 하는 일”과 “계산이 하는 일”이 명확히 나뉘어 있다는 점입니다.

7-1. 요청 하나의 전 구간

sequenceDiagram
    participant U as 브라우저
    participant R as Router<br/>(Inbound Adapter)
    participant I as TrackDiscoverInteractor<br/>(Application)
    participant G as GeminiAdapter<br/>(Outbound)
    participant P as PgRepository<br/>(Outbound)
    participant C as CreatePresetInteractor

    U->>R: POST /api/soundbridge/discover<br/>{input, lang}
    R->>I: DiscoverCommand
    I->>I: Redis 캐시 조회
    I->>G: analyze_emotion(input, lang)
    G-->>I: EmotionAnalysisResult
    I->>P: embed_text(embed_text)
    P-->>I: float[1536]
    I->>P: find_similar_tracks(vec, top_k=3)
    P-->>I: UUID[3]
    I->>P: find_by_ids(ids) + joinedload
    P-->>I: GugakTrack[3]
    I->>G: explain_match(input, tracks, lang)
    G-->>I: MatchExplanation[3]
    I->>P: save_match_log() × 3
    I->>I: Redis SETEX 3600
    I-->>R: DiscoverResult
    R->>C: build_preset_url() × 3
    C-->>R: preset_url
    R-->>U: DiscoverResponseSchema

7-2. 계층별 책임

계층 파일 하는 일 하지 않는 일
Inbound Router track_discover_router.py HTTP 파싱, 예외 → 상태코드 변환, 프리셋 부착 DB 쿼리, 비즈니스 판단
Application Interactor track_discover_interactor.py 순서 조율, 캐시, 결과 조립 ORM 접근, HTTP 상태코드
Outbound Gemini gemini_adapter.py 프롬프트 조립, JSON 파싱 어떤 트랙을 고를지 결정
Outbound PG track_discover_pg_repository.py 쿼리, joinedload, 벡터 검색 응답 형식 결정
Domain track_entity.py, *_vo.py 불변식, 파생 값(loop_unit_beats, is_commercial) 외부 패키지 import

7-3. AI와 계산의 경계

이 파이프라인의 설계 핵심은 LLM이 사실을 만들지 않는다는 것입니다.

산출물 만드는 주체 성격
감성 키워드 · 임베딩용 텍스트 Gemini 해석 (비결정론적, 허용)
어떤 트랙이 매칭되는가 pgvector 계산 (결정론적)
유사도 점수 pgvector 계산 (결정론적)
BPM · 장단 · 루프 단위 · 라이선스 PostgreSQL 사실 (DB 값)
왜 닮았는지 설명 Gemini 서술 (이미 확정된 결과에 대한 설명)
Gemini가 할 수 있는 것 :  번역 · 해석 · 설명
Gemini가 할 수 없는 것 :  선택 · 계산 · 사실 주장

만약 Gemini에게 “이 500곡 중 뭐가 어울릴까?”를 직접 물었다면, 존재하지 않는 곡명을 만들어 내거나 매번 다른 답을 낼 수 있습니다. 매칭을 벡터 연산으로 고정했기 때문에 같은 입력에는 같은 트랙이 나옵니다.

7-4. 응답 스키마

class TrackResponseSchema(BaseModel):
    id: UUID
    title: str
    artist: str
    instrument: str
    jangdan: str
    emotion_tags: list[str]
    bpm: int
    loop_unit_beats: int          # 도메인 프로퍼티에서 계산
    cue_points: list[CuePointSchema]
    audio_url: str
    license_type: str             # KOGL_1 | KOGL_2
    license_label_en: str         # CC-BY (Commercial OK) | CC-BY-NC …
    description_ko: str
    description_en: str
    score: float | None = None
    explanation: str | None = None
    preset_url: str | None = None  # 브릿지

loop_unit_beats는 DB 컬럼이 아니라 도메인 프로퍼티입니다(3NF 분리, 정규화 설계). license_label_en도 컬럼이 아니라 LICENSE_EN_LABEL 딕셔너리에서 나옵니다. DB에 없는 값이 응답에 있다는 것 자체가 도메인 레이어가 살아 있다는 증거입니다.

7-5. 엔드포인트 전체 목록

[DISCOVER]  /api/soundbridge/discover                              [MVP]
  POST   /                       감성 매칭 검색 (preset_url 포함)
  GET    /popular                인기 트랙 목록
  GET    /{track_id}             트랙 상세                          [v1.2]

[CREATE]    /api/soundbridge/create                                [MVP]
  GET    /samples                샘플 목록 (필터 쿼리)
  POST   /download-log           다운로드 이력 저장                 [v1.1]
  GET    /download-logs          내 다운로드 이력                   [v1.1]

[AUTH]      /api/soundbridge/auth                                  [v1.1]
  POST   /register · /login · /login/google
  POST   /verify-email · /resend-verification
  POST   /forgot-password · /reset-password
  GET    /me · PATCH /me · DELETE /me

[SAVED]     /api/soundbridge/saved                                 [v1.1]
  GET    /tracks · POST /{track_id} · DELETE /{track_id}

[CUE]       /api/soundbridge/cue                                   [v1.1]
  POST   /extract/{track_id}     CUE 마커 추출 (관리자용)

[EVENTS]    /api/soundbridge/events                                [v1.1]
  GET    /performances           KOPIS 공연 목록
  GET    /places                 관광 체험 장소

/health는 prefix 밖에 별도로 있습니다 (main.py).

7-6. 구현 순서 (Phase)

명세서 13장의 순서를 그대로 따랐습니다. 외부 IO가 없는 것부터 만듭니다.

Phase 1 — 기반
  디렉터리, 환경변수, DB·Redis 연결, 예외, ORM Base, db_init
  jangdan_orm · track_emotion_tag_orm 생성 + 장단 시드 6건

Phase 2 — Domain + App Core
  Value Objects, Track Entity, Ports, DTOs
  (프레임워크 없이 단위 테스트 가능)

Phase 3 — CREATE 프리셋 유스케이스
  외부 IO 없음 — 순수 로직. 프론트 연동 전에 단독 테스트 완료

Phase 4 — DISCOVER 핵심
  Interactor · Router(preset_url 포함) · PgRepository · GeminiAdapter
  → POST /discover + preset_url 포함 응답 확인

Phase 5 — CREATE 샘플 필터
  emotions 필터 JOIN, loop_unit 필터 jangdan JOIN
  → GET /samples 동작 확인 → 프론트 FilterPanel 연동

v1.1 이후는 인증(Phase 6), 저장·다운로드 이력(Phase 7), 외부 API 연동(Phase 8), 최적화(Phase 9 — HNSW 인덱스·Redis 전면 적용)로 이어집니다.

7-7. 성능 특성

구간 지연 비고
Redis 캐시 HIT ~수 ms Gemini 호출 4회를 통째로 회피
Gemini analyze_emotion 1회 호출 파이프라인 최대 지연 구간
Gemini explain_match 3회 순차 호출 트랙별 개별 호출
pgvector 검색 500건 순차 스캔 현 규모에서 체감 지연 없음
find_by_ids 1회 쿼리 joinedload로 N+1 제거

개선 여지: explain_match가 트랙마다 순차 호출이라 캐시 MISS 시 지연이 3배로 붙습니다. asyncio.gather로 병렬화하면 체감 응답이 크게 줄어듭니다. 데이터가 수천 건으로 늘면 pgvector에 HNSW 인덱스도 필요합니다(명세서 Phase 9).


SoundBridge — 당신의 음악 언어로 국악을 만나세요. 본 사이트는 개발 문서 자료입니다.

This site uses Just the Docs, a documentation theme for Jekyll.