어디살지 백엔드 종합 진단
TL;DR — 4축 핵심 결함
- DB: Alembic head 7개로 분기 미머지 · JSONB GIN 인덱스 0개 · PostGIS 추가됐지만 검색 경로는 여전히 Haversine 풀스캔
- 임베딩:
text-embedding-3-small1536d 단일 — 한국어 부동산 도메인 검증 X - 리랭커: cross-encoder 없음. 코드의 "rerank" 는 payload feature 가중 합계뿐
- 아키텍처: hex 경계 위반 6곳 · 관측성 0 (Sentry/OTel/Prometheus 미설치) · boto3 sync 가 async loop block · APScheduler 중복 트리거
7
Alembic head 분기
160 / 0
Index / JSONB GIN
1536d
단일 임베딩 차원
0
Cross-encoder 리랭커
6
Hex 경계 위반
0
관측성 라이브러리
5,348
models.py 라인
7
중복 cron (APScheduler)
축 1 · DB 구조 5 결함
| # | 항목 · 위치 | 리스크 | 권장 |
|---|---|---|---|
| 1 | Alembic head 7개 분기 미머지 backend/migrations/versions/ (90 파일) · heads: c0d1e2f3a4b5, d6e7f8a9b0c1, a1t2u3v4w5x6, c4d5e6f7h8i9, b3v4w5x6y7z8, f2a3b4c5d6e7, e5f6g7h8i9j0 | CRIT alembic upgrade head 시 multiple heads 에러 |
단일 merge 리비전 추가 + CI 가드 (alembic heads | wc -l == 1) |
| 2 | 동일 revision ID 충돌 a1t2u3v4w5x6_*.py — contract_documents / notification_settings / oauth_tokens 3 파일이 같은 down_revision="z0s1t2u3v4w5" | HIGH suffix _cd/_ns만으로 구분, head 추가 분기 위험 |
리비전 ID 네이밍 정책 (YYYYMMDD_HHMM_slug) |
| 3 |
PostGIS 미활용 (검색 경로)
src/app/adapters/database/property_repository.py:1187-1217 (find_candidate_ids) — func.acos Haversine 풀스캔
|
HIGH geom + GIST 만들었으나 핵심 후보 추출 미사용 → 매물 늘면 선형 저하 |
ST_DWithin(geom, ST_MakePoint(..)::geography, radius_m) 교체 |
| 4 |
JSONB GIN 인덱스 0개
models.py:1346 property_details / :1411 vision_summary 모두 JSONB 인덱스 없음. property_details["area_m2"].astext.cast(Numeric) 풀스캔
|
MED 면적·방수 필터마다 전체 cast 비용 | 자주 쓰는 키는 generated column + B-tree, 또는 GIN (property_details jsonb_path_ops) |
| 5 |
ORM relationship eager loading 0회
models.py relationship 12회 — selectinload/joinedload 전 코드베이스 0회. lazy 기본값
|
MED raw query 위주라 현재 미표출. relationship 접근 시 자동 N+1 | relationship 정의에 lazy="raise" → lazy access 시 즉시 에러로 N+1 차단 |
축 2 · 임베딩 모델 4 결함
| # | 항목 · 위치 | 리스크 | 권장 |
|---|---|---|---|
| 1 | 단일 모델 의존 src/app/adapters/ai/openai_embedding_adapter.py:34 MODEL=text-embedding-3-small · DIMENSIONS=1536 · qdrant/bootstrap.py:46 VECTOR_SIZE=1536 | HIGH OpenAI 영어 중심. 한국어 부동산 토큰(반지하·풀옵션·역세권) 검증 X | A/B: text-embedding-3-large(3072) vs bge-m3(1024) vs Gemini embedding-001(768) — POC 어댑터 골격 이미 존재 |
| 2 | 도메인 동의어 보강 부재 search_document.py:526-625 _build_embedding_text — 단순 [유형][주소][설명][태그] concat | MED 한국어 약어/동의어("강남"="강남구·강남역") 정규화 없이 임베딩 → recall 저하 | 도메인 사전 기반 query/document 양쪽 동의어 확장 |
| 3 | Qdrant 단일 dense vector qdrant/bootstrap.py:155-161 VectorParams(size=1536, distance=COSINE) | MED dense-only 라 "강남역 도보 3분" 같은 키워드 정확 매칭은 BM25 보다 약함 | properties_v2 dense+sparse(BM25/SPLADE) hybrid + alias zero-downtime 전환 (alias_manager.py 존재) |
| 4 | description 1,000자 truncate search_document.py:577 desc_text = description.strip()[:1000] | LOW text-embedding-3-small 8,191 토큰 한도. 후반부(주변 인프라·인테리어) 손실 | 8,000자(≈3,000토큰) 또는 청크별 임베딩 후 mean pooling |
축 3 · 리랭커 3 결함
| # | 항목 · 위치 | 리스크 | 권장 |
|---|---|---|---|
| 1 | Cross-encoder 리랭커 미존재 pyproject.toml:43-58 cohere·sentence-transformers·bge 의존성 없음. cohere/cross_encoder/reranker_model 매치 0건 | HIGH "rerank"는 search_properties.py:531 payload-feature 가중 합계(vector α=0.75 + prefer 0.25). 의미적 재정렬 X | 2-stage: ANN top-50 → Cohere rerank-multilingual-v3 또는 BAAI/bge-reranker-v2-m3 top-10. POC 어댑터 존재 |
| 2 | per-search 추가 count RT src/app/adapters/qdrant/qdrant_search_repository.py:242 | MED 매 검색마다 Qdrant count() 한 번 더 — 리랭커 도입 시 latency 누적 |
total 필요한 path만 count, 무한 스크롤은 has_next |
| 3 | 검색 결과 캐시 부재 services/embedding_cache.py Redis 24h TTL — query vector만 캐싱 | LOW "강남역 원룸" 반복 쿼리도 Qdrant 매번 호출. 리랭커 추가 시 cost 폭증 | (query_hash + filter_hash) → top-K id 짧은 TTL(5~10분) 캐시 |
축 4 · 아키텍처 5 결함
| # | 항목 · 위치 | 리스크 | 권장 |
|---|---|---|---|
| 1 | Hex Port/Adapter 경계 위반 6곳 search/ports/search_properties_port.py:21-22 / auth/use_cases/request_email_verification.py:24 / contract_documents/use_cases/generate_{hwpx,pdf}.py / admin/user_management/use_cases/admin_user_management_service.py | HIGH import-linter는 core → adapters만 차단. domains → adapters 무방비 |
import-linter contract에 app.domains → app.adapters 금지 추가 + adapter port 분리 리팩토 |
| 2 | 관측성 도구 0 pyproject.toml 전체에 sentry-sdk / opentelemetry / prometheus-client 없음. sentry 매치 0건 (conversation_memory.py:397 주석뿐) | CRIT 프로덕션 에러·latency 추적 수단이 구조화 로그뿐. APM·alerting·trace 없음 | sentry-sdk[fastapi] + OTel auto-instrument(asyncpg/httpx/redis/qdrant) + CF Workers trace export |
| 3 | boto3 sync 호출이 async loop block src/app/core/storage.py:67,86 boto3.client + put_object. 호출처 domains/messaging/use_cases/upload_chat_image.py:143,173 async def | HIGH 동기 S3 I/O가 event loop block — 업로드 트래픽 동시성 저하 | aioboto3 전환 또는 asyncio.to_thread 일괄 래핑 (email_notification_adapter.py:107이 선례) |
| 4 | Dramatiq + APScheduler 중복 트리거 src/app/workers/broker.py:100-204 BackgroundScheduler(in-process) 7개 cron · Redis 락 없음 | HIGH worker N개면 동일 cron N회 trigger — webhook 재시도·outbox 중복 처리 위험 | dramatiq-periodiq 또는 단일 leader 패턴 (Redis distributed lock + heartbeat) |
| 5 | 거대 단일 파일 + eager 0회 models.py 5,348줄 / property_repository.py 1,540줄 / routers/properties.py 3,019줄. selectinload·joinedload 매치 0건 | MED 변경 영향 추적 곤란 + relationship 사용 시 자동 lazy → N+1 위험 | 도메인별 model 분할 + lazy="raise" 기본 + 명시적 eager 전략 |
우선순위 매트릭스 (P0 → P3)
| 우선 | 항목 | 영향 | 예상 작업량 |
|---|---|---|---|
| P0 | Alembic head 7개 → 단일 merge 리비전 | 배포 실패 회복 | S (~40 LOC) |
| P0 | Sentry + OTel 최소 도입 | 프로덕션 가시성 | M (~150 LOC + config) |
| P1 | PostGIS ST_DWithin 으로 검색 후보 추출 교체 | 지리 검색 지연 ↓ | M (~80 LOC) |
| P1 | import-linter contract 강화 (도메인 → 어댑터 금지) | 아키텍처 회귀 차단 | S + 6 파일 리팩토 |
| P1 | APScheduler → Redis-lock 또는 dramatiq-periodiq | 중복 webhook/outbox 차단 | M (~100 LOC) |
| P1 | boto3 → aioboto3 또는 to_thread 일괄 래핑 | 업로드 동시성 ↑ | M |
| P2 | Cross-encoder 리랭커 도입 (Cohere rerank-multilingual-v3) | 검색 정확도 MRR ↑ | M (~120 LOC, POC 어댑터 재활용) |
| P2 | 임베딩 A/B (bge-m3 vs gemini vs text-embedding-3-large) | 한국어 recall ↑ | L (offline eval 셋 필요) |
| P2 | JSONB jsonb_path_ops GIN 인덱스 |
면적·방수 필터 지연 ↓ | S (alembic 1 리비전) |
| P3 | 거대 단일 파일 분할 + lazy="raise" | 유지보수 ↑ | L (단계적) |
참고 — 이미 존재하지만 활용 안 된 자산
poc-chat-search/src/adapters/embedding/openrouter_gemini_adapter.py— Gemini Embedding 2 골격poc-chat-search/src/adapters/reranker/openrouter_cohere_adapter.py— Cohere rerank 어댑터qdrant/alias_manager.py— zero-downtime alias 전환e1f2a3b4c5d6_*.py— PostGIS geom + GIST 마이그레이션 (검색 경로 미연결)cached_building_registry_adapter.py·cached_nearby_facility_adapter.py·cached_geocoding_adapter.py— Redis 캐시 래퍼 패턴email_notification_adapter.py:107—asyncio.to_threadsync → async 변환 선례