# 백엔드 아키텍처 리뷰

## 전체 판단

현재 백엔드는 "방향은 맞지만 계약이 아직 하나로 닫히지 않은 과도기 모놀리스"에 가깝다. `analysis_service.py` 제거, `checkpoint_sync` 분해, `step_catalog` 도입은 실제 진전이다. 반면 런타임 소비자, 공개 API, startup/lifecycle은 여전히 과거 계약을 같이 끌고 간다. 문제의 핵심은 새 구조가 없다는 것이 아니라, 새 구조와 옛 진입점이 동시에 살아 있어서 운영 계약이 아직 단일화되지 않았다는 점이다.

이번 문서는 2026-04-21 checkout과 실제 소스/pytest 실행 결과를 기준으로 다시 정리했다.

## 현재 구조 요약

```mermaid
flowchart TD
    FE1[Frontend EpisodeDetail] --> API1[/steps/run-all]
    FE2[Legacy client / Episodes] --> API2[/episodes/{id}/analyze]
    API1 --> DISPATCH[analysis_dispatch_service]
    API2 --> DISPATCH
    DISPATCH --> RUNNER[StepRunner]
    RUNNER --> CP[checkpoint manifest.json]
    DISPATCH --> SYNC[checkpoint_sync orchestrator]
    SYNC --> DB[(PostgreSQL / step_run / entity / still / image tables)]
```

핵심 관찰은 다음과 같다.

- 내부 실행 수렴점은 `analysis_dispatch_service` + `StepRunner`다.
- 하지만 외부 계약은 아직 `/analyze`, `/reanalyze-scenes`, `/steps/run-all`로 분산돼 있다.
- DB projection은 별도 sync 경로에 남아 있어, "checkpoint canonical + DB projection" 모델이 아직 아키텍처의 사실상 중심이다.

## 실제로 정리된 부분

### 1. 실행 엔진 이원화는 많이 줄었다

- `backend/app/services/analysis_service.py`는 현재 없다.
- `backend/app/api/v1/episodes.py:145-149`는 `/analyze`가 더 이상 독자 엔진이 아니라 `dispatch_category_run(category="analysis")`으로 위임된다고 명시한다.
- `backend/app/api/v1/steps.py:168-175`도 `/steps/run-all`이 같은 dispatch 경로를 사용한다고 설명한다.

판단:

- 예전의 "엔드포인트마다 다른 실행기" 문제는 상당 부분 정리됐다.
- 다만 아래에서 보듯, 제품 계약 단일화는 아직 끝나지 않았다.

### 2. checkpoint sync 분해는 실제 구조로 반영됐다

- `backend/app/api/v1/steps.py:365-372`의 legacy wrapper는 실제 로직을 `app.services.checkpoint_sync.orchestrate_full_sync`로 넘긴다.
- 즉, sync 거대 함수가 최소한 서비스 오케스트레이터 경유 구조로 바뀐 것은 사실이다.

판단:

- 분해 방향은 맞다.
- 하지만 route/step 쪽에서 아직 wrapper를 계속 붙들고 있어서, 구조 개선의 이익이 경계면에서 희석된다.

### 3. step 메타데이터는 설계 수준까지 올라왔다

- `backend/app/core/step_manifest.py:21-30`은 `step_type`, `lifecycle`, `resume_sensitive`, `modifies_checkpoints`, `replaced_by`를 정의한다.
- `backend/app/core/step_catalog.py:1-18`은 manifest + runner class binding을 합친 "소비자 전용 view"라고 스스로 규정한다.

판단:

- "Step을 메타 기반으로 운영"한다는 리팩터링 목표는 문서가 아니라 코드로 들어왔다.
- 지금의 문제는 메타가 없어서가 아니라, 모든 런타임 소비자가 그 메타만 보도록 아직 강제되지 않았다는 점이다.

## 구조적으로 아직 미완료인 지점

### 1. `step_catalog` 채택이 선언만큼 완결되지 않았다

근거:

- `backend/app/core/step_catalog.py:8-10`은 "소비자는 STEP_CATALOG만 본다"고 못 박는다.
- 그런데 `backend/app/core/step_runner.py:21-23`은 여전히 `STEP_MANIFEST`, `get_depends_on`, `get_all_downstream_recursive`를 `step_manifest`에서 직접 가져온다.
- 같은 파일 `backend/app/core/step_runner.py:44-52`는 초기화 시점 검증과 `self.manifest` 바인딩도 `STEP_MANIFEST`를 직접 사용한다.
- `backend/app/api/v1/steps.py:18-23`은 `step_catalog`와 `step_manifest`를 동시에 import한다.
- `backend/app/api/v1/steps.py:71-126`의 `get_all_steps()`는 `get_ordered_steps()`를 manifest에서 가져와 순회하고, `backend/app/api/v1/steps.py:142-143`, `266-267`은 step 존재 여부를 `STEP_MANIFEST`로 검사한다.

판단:

- `step_catalog`는 "새 계약"이지만, `StepRunner`와 `steps API`는 아직 "manifest 직접 참조"를 유지한다.
- 그 결과 ordering, gate, applicability, lifecycle 해석이 계층마다 다시 갈라질 수 있다.
- 즉, step metadata 리팩터링의 핵심 과제인 "single runtime contract"는 아직 완료되지 않았다.

### 2. pre-sync 정책이 메타가 아니라 route 내부 상수다

근거:

- `backend/app/api/v1/steps.py:307-324`의 nested worker `_run_in_background`는 `_needs_presync` 튜플을 직접 선언한다.
- 목록에는 `scene_director`, `outlook_extraction`, `outlook_phase1/2/3`, `scene_detail`, `scene_verify`가 하드코딩돼 있다.
- 반면 `backend/app/core/step_manifest.py:21-30`의 메타 필드에는 "이 step은 실행 전에 projection이 필요하다"는 계약이 없다.

판단:

- projection 필요 여부는 현재 step metadata가 아니라 route 구현 세부사항이다.
- 새 step을 추가할 때 manifest/catalog만 맞춰도 충분하지 않고, `steps.py` 내부 상수까지 함께 수정해야 한다.
- 이건 단순 중복이 아니라 런타임 장애 후보군이다. 메타 누락 시 테스트보다 운영에서 먼저 드러날 수 있다.

### 3. 공개 분석 API는 아직 이중화 상태다

근거:

- `backend/app/api/v1/episodes.py:135-207`에 `/episodes/{id}/analyze`가 deprecated 상태로 남아 있다.
- `backend/app/api/v1/episodes.py:210-260`에는 `/episodes/{id}/reanalyze-scenes`도 남아 있다.
- 동시에 `backend/app/api/v1/steps.py:159-175`는 `/steps/run-all`을 별도 공식 경로로 노출한다.

판단:

- 내부 dispatch는 하나로 모였지만, 외부 계약은 여전히 둘 이상이다.
- 문서상 "공식 경로는 `/steps/run-all`"이라 해도, 코드 레벨에서는 `/analyze`가 여전히 작동한다.
- 제품/자동화/테스트가 서로 다른 URL을 계속 학습할 수 있다는 뜻이다.

### 4. 앱 라이프사이클이 import/startup 부수효과에 강하게 묶여 있다

근거:

- `backend/app/core/database.py:15-16`은 `engine`과 `SessionLocal`을 import 시점에 고정한다.
- `backend/app/main.py:39`는 모듈 import 시점에 바로 `init_db()`를 호출한다.
- `backend/app/main.py:45-93`는 `@app.on_event("startup")` 안에서 로깅 초기화, 기본 사용자 생성, stale progress 복구를 수행한다.
- `backend/tests/test_pipeline_e2e.py:10`은 전역 `app`를 import하고, `backend/tests/test_pipeline_e2e.py:15`는 별도 schema 준비 없이 `TestClient(app)`를 연다.
- 실제로 전체 실행 `backend/.venv/bin/python -m pytest backend/tests -q`에서는 `test_pipeline_e2e` setup이 startup 중 `_ensure_default_user()`에서 `sqlite3.OperationalError: no such table: user_account`로 실패했다.
- 같은 파일 단독 실행 `backend/.venv/bin/python -m pytest backend/tests/test_pipeline_e2e.py -q`는 `2 passed, 1 skipped`였다.

판단:

- 지금의 startup은 "앱 구동"과 "환경/bootstrap"을 분리하지 않는다.
- 그래서 테스트에서 어떤 DB/session state가 먼저 깔렸는지에 따라 `TestClient(app)`의 성공 여부가 바뀐다.
- 단일 파일에서는 통과하고 전체 suite에서만 깨지는 것은, 테스트 flaky 그 자체라기보다 lifecycle coupling이 이미 환경 의존성을 만들고 있다는 증거다.

### 5. 설정 계층은 여전히 deprecated 스타일과 insecure default를 함께 가진다

근거:

- `backend/app/core/config.py:10`은 `secret_key = "dev-secret-key"`를 기본값으로 둔다.
- `backend/app/core/config.py:15-17`은 `admin123`, `creator123`를 기본 비밀번호로 둔다.
- `backend/app/core/config.py:59-61`은 여전히 Pydantic v2에서 deprecated된 `class Config` 스타일을 사용한다.
- `backend/app/main.py:53-65`는 startup 때 이 insecure defaults를 감지해 warning을 찍는다.
- `backend/app/main.py:69-82`는 바로 그 기본 비밀번호들로 default user bootstrap을 수행한다.

판단:

- 코드는 이미 "이 기본값이 위험하다"는 사실을 알고 있다.
- 그런데 그 기본값을 경고만 하고, 같은 startup에서 계속 seed까지 하기 때문에 안전장치가 아니라 "위험을 알고도 실행하는 구조"에 가깝다.

## 테스트에서 드러난 구조적 후폭풍

전체 backend suite 실행 결과는 `56 failed, 637 passed, 2 errors, 1 skipped`였다. 이 중 가장 구조적인 실패는 다음 둘이다.

- `test_pipeline_e2e`의 startup/schema coupling:
  `app.main` import + `TestClient(app)`만으로 startup이 돌고, default-user bootstrap이 스키마 준비 여부에 의존한다.
- 다수의 legacy-contract drift:
  `test_pipeline_v3_e2e`, `test_sync_v3`, `test_scene_*_v2/v3`, `test_outlook_v2`가 과거 step order, dependency count, registry key, checkpoint schema를 전제로 하고 현재 구현과 충돌한다.

이것은 "테스트가 낡았다"는 한 줄로 끝낼 문제가 아니다. 현재 아키텍처가 새 계약과 옛 계약을 동시에 유지하고 있다는 뜻이기 때문이다.

## 프로젝트 목적 대비 영향

이 프로젝트의 목적은 "대본을 넣으면 안정적으로 분석하고, 그 결과를 이미지와 웹북 생산까지 연결하는 것"이다. 그 기준에서 현재 백엔드의 가장 큰 리스크는 모델 정확도보다 재실행 일관성과 계약 일관성이다.

문제가 되는 이유:

- 사용자는 `/analyze`와 `/steps/run-all` 중 어느 것을 호출해야 하는지 아직 코드만 봐서는 단일 답을 얻지 못한다.
- step metadata를 바꿔도 `StepRunner`, `steps.py`, legacy wrapper가 같은 계약을 자동으로 공유하지 않는다.
- startup/bootstrap이 앱 import에 묶여 있어 테스트와 운영 모두에서 환경 민감도가 커진다.

## 우선 보강 항목

1. `StepRunner`와 `steps API`를 `step_catalog`만 보도록 바꾸고, `step_manifest` 직접 참조를 내부 구현으로 숨긴다.
2. `_needs_presync`를 없애고 `needs_projection_before_run` 같은 메타를 manifest/catalog에 올린다.
3. `/episodes/{id}/analyze`와 `/reanalyze-scenes`를 호환 shim 이상으로 남기지 말고, 공개 계약을 `/steps/*`로 확정한다.
4. `init_db()` import side effect와 default-user bootstrap을 app lifespan 밖의 명시적 init 경로로 분리한다.
5. `config.py`를 `ConfigDict`로 옮기고 insecure defaults를 "경고 후 실행"이 아니라 "개발 전용 opt-in"으로 낮춘다.
