# 보정된 설계 원칙

> 작성: 2026-04-17
> 입력: Claude 초안의 7원칙 + Codex 검토 보정
> 출력: 현재 코드 구조에 맞게 조정된 9원칙

---

## 0. 개요

Claude 초안(`docs/architecture-refactor/01-target-design.md`)은 다음 7원칙을 제안했다:

1. DB Primary, 체크포인트 Backup
2. 명시적 계약
3. 단방향 의존성 (API → Service → Core → Steps → Model)
4. Step은 Pure Transformation
5. 중앙 레지스트리 (Manifest)
6. 선언적 > 명령적
7. 테스트 가능성 우선

이 중 **1, 4, 5** 세 원칙은 현재 코드베이스 현실과 어긋나 있다 (Codex 검토 지적). 본 문서는 세 원칙을 **실행 가능한 형태**로 보정하고, 2개 원칙을 신규 추가한다.

---

## 1. 원칙 목록 (최종)

| # | 원칙 | 초안 | 최종 | 변경 이유 |
|---|---|---|---|---|
| 1 | **3-tier 진실원** | DB Primary | 데이터 종류별 3-tier | 실행 엔진이 checkpoint-first |
| 2 | **명시적 계약** | 명시적 계약 | (유지) | - |
| 3 | **단방향 의존성** | API → Service → Core → Steps → Model | (유지, 강화) | API private 역의존 제거 |
| 4 | **Step 유형별 규칙** | Step은 Pure Transformation | 4-type 분류 + 유형별 규칙 | Image/Editorial step 현실 |
| 5 | **Step Catalog** | 중앙 레지스트리 (Manifest) | Step Catalog (Manifest + Class + Applicability + UI) | registry 분산 현실 |
| 6 | 선언적 > 명령적 | (유지) | (유지) | - |
| 7 | 테스트 가능성 우선 | (유지) | (유지) | - |
| 8 | **Step 경계 명시성** 🆕 | — | 한 step의 책임은 자기 step만 | Image step 경계 붕괴 대응 |
| 9 | **공식 경로 단일화** 🆕 | — | 분석 진입점은 StepRunner만 | AnalysisService 폐기 |

---

## 2. 원칙 1 — 3-tier 진실원

### 2.1 배경

초안은 "DB Primary, 체크포인트 Backup"을 제안했다. Codex 지적대로 현재 `StepRunner.run()`(`step_runner.py:243-290`)의 실행 순서는:

1. `_update_step_run("running")` — DB 상태 갱신
2. `_execute()` — LLM 호출
3. `save_checkpoint()` — **체크포인트 파일 저장** (canonical 결과)
4. `_update_step_run("completed")` — DB status 갱신

그리고 LLM 분석 산출물의 실제 저장은 체크포인트 파일이며, DB는 `_sync_checkpoints_to_db` 시점의 projection에 불과. 이 구조를 뒤집으려면 전체 StepRunner 엔진을 재작성해야 하며, 리팩토링 범위를 벗어난다.

### 2.2 3-tier 정의

| 티어 | 데이터 종류 | 진실원 | 쓰기 책임 | 소비자 |
|---|---|---|---|---|
| **T1: 실행 상태** | `step_run.status`, `scene_still.is_selected`, `scene_still.image_generated`, `episode.status`, `t2i_appearance_count`, progress row | **DB primary** | `StepRunner._update_step_run`, `toggle_shot_selection` 등 전용 API | UI, 게이트, 대시보드 |
| **T2: 분석 산출물** | `entity_t2i/manifest.json`, `scene_detail/manifest.json`, `outlook_phase3/manifest.json` 등 체크포인트 JSON | **체크포인트 canonical + DB projection** | Step `save_checkpoint` → 이후 `CheckpointSyncService.sync_*` | Step 하위 consumer, Image Service, UI (DB 경유) |
| **T3: 이미지 자산** | 씬/참조/합성 이미지 파일 + `ImageAsset` DB row | **DB 메타 + FS 파일 동시 소유** | `ImageService` 내부 | 프론트 (DB), 이미지 빌드 (FS) |

### 2.3 티어별 계약

**T1 (DB primary)**:
- DB가 먼저 커밋된 후 다른 쪽(체크포인트)에 반영
- 예: `toggle_shot_selection`(`steps.py:334-401`)은 DB commit 후 체크포인트 원자 쓰기
- 파일 쓰기 실패 시 DB는 이미 정합. 다음 `_sync` 또는 scene_detail 실행이 파일 복원

**T2 (체크포인트 canonical)**:
- Step 실행이 체크포인트 파일에 쓰는 것이 canonical
- `CheckpointSyncService`가 파일을 읽어 DB에 UPSERT projection
- `_sync` 실패 시 체크포인트는 유지 — 재실행으로 복원
- UI는 DB 읽기, 직접 파일 읽지 않음

**T3 (DB 메타 + FS 동시)**:
- 파일 경로는 DB `image_asset.file_path`에 저장
- 생성 시: 파일 쓰기 성공 → DB insert
- 삭제 시: DB delete 후 파일 삭제 (실패 허용)
- 스냅샷/복원 시: DB 복원 + FS 검증, 파일 없으면 stale 마킹

### 2.4 장기 목표 (Phase 외)

T2의 "체크포인트 canonical"은 아키텍처 숙성을 위한 **과도기 계약**이다. 장기적으로는:
- T2 데이터를 DB 전용으로 점진 이전 (예: `scene_detail` → `scene_still.t2i_prompt_cinematic`에 이미 projection됨)
- 체크포인트는 snapshot/복원 전용 archive로 격하
- 이 전환은 **Phase 5 이후 별도 프로젝트**로 평가 (Phase 4까지는 유지)

---

## 3. 원칙 3 — 단방향 의존성 (강화)

### 3.1 레이어

```
[Frontend]
    ↓ HTTP
[API Layer] ──────────────────────────────┐
    ↓                                      │
[Service Layer]                            │  (의존 허용)
    ↓                                      │
[Core] (step_runner, step_catalog,         │
        applicability, checkpoint_io)      │
    ↓                                      │
[Steps] (analysis_steps, image_steps, ...) │
    ↓                                      │
[Models] (SQLAlchemy ORM)                  │
    ↓                                      │
[DB / Filesystem]                          │
                                           │
                                           │
금지: 위쪽 레이어 → 아래쪽 import만 허용    │
                                           │
역의존 현재 위반:                           │
  - image_steps.py:58                      │
    from app.api.v1.steps import          │
      _sync_checkpoints_to_db              │ ← Phase 2에서 해소
  - api/v1/entities.py                    │
    from app.api.v1.steps import          │
      _sync_t2i_appearance_counts          │ ← Phase 2에서 해소
```

### 3.2 판정 규칙

다음은 레이어 위반:
- **Step이 API를 import**: 절대 금지 (현재 2건 → Phase 2에서 해소)
- **Service가 API를 import**: 금지 (현재 확인 안 됨)
- **Step이 Service를 import**: 허용 (예: `image_steps`가 `ImageService` 호출)
- **Service가 Service를 import**: 허용 (예: `ReferenceImageService`가 `PromptService` 호출)
- **Step이 Step을 import**: 허용 (예: `t2i_review_step`이 `_DetailStepMixin` 상속)

### 3.3 Phase 2 작업

`_sync_checkpoints_to_db`를 `app/services/checkpoint_sync_service.py`로 이전하면:
- `image_steps.py:58`은 `from app.services.checkpoint_sync_service import CheckpointSyncService`로 교체 → 합법 의존
- `api/v1/steps.py::_sync_checkpoints_to_db`는 얇은 wrapper (Service 호출)

---

## 4. 원칙 4 — Step 유형별 규칙

### 4.1 배경

초안은 "Step은 Pure Transformation"을 모든 step에 적용하려 했다. Codex 지적대로 다음 반례가 존재:

- `WorldGuideStep`(`image_steps.py:243-305`): DB에서 직접 읽고 `WorldGuide` insert/commit
- `SetDesignStep`(`set_design_step.py:80-228`): 이미지 파일 생성 + `ImageAsset` DB 등록
- `T2iReviewStep`(`t2i_review_step.py:51-92`): **자기 step이 아닌** `entity_t2i`, `scene_detail` 체크포인트 덮어쓰기
- `RefImageGenStep`(`image_steps.py:311-375`): 파일 생성 + DB update + `composite_image_gen` step 완료 처리

→ Pure Transformation은 **일부 step에만 적용 가능**. Codex 제안 4-type 분류를 수용.

### 4.2 4-type 분류

| 유형 | 정의 | 예시 | 규칙 |
|---|---|---|---|
| **transform** | 체크포인트 입력 → 체크포인트 출력 | `scene_summary`, `beat_extract`, `shot_extract`, `shot_selection`, `scene_director`, `shot_director`, `scene_camera_flow`, `scene_consistency`, `scene_detail`, `shot_staging`, `shot_dependency`, `entity_*`, `outlook_phase*`, `visual_world_rules`, `text_cleanup`, `planning_doc_analysis` | DB 쓰기 금지, 외부 side effect 금지, closure 공유 금지 |
| **projection** | 체크포인트 → DB projection | `_sync_checkpoints_to_db` 내부 (`EntitySyncService`, `RelationSyncService`, `SceneStillSyncService`, `OutlookSyncService`, `EpisodeProjectionService`) | DB 쓰기 전담. 입력은 체크포인트. idempotent 필수 |
| **asset** | 이미지 파일 + DB 메타 생성 | `ref_image_gen`, `composite_image_gen`, `set_design`, `character_state_variant`, `scene_image_pipeline`, `world_guide` | 파일/DB side effect 허용. 단, 자기 step 외부 상태(`composite_image_gen.status`) 변경 금지 |
| **editorial** | 타 step 체크포인트 수정 | `t2i_review` | 덮어쓰는 타 step 체크포인트 명시 (manifest 필드). 원자 쓰기 + archive 필수 |

### 4.3 manifest 필드 확장

```python
"scene_detail": {
    ...,
    "step_type": "transform",
    ...
},
"ref_image_gen": {
    ...,
    "step_type": "asset",
    ...
},
"t2i_review": {
    ...,
    "step_type": "editorial",
    "modifies_checkpoints": ["entity_t2i", "scene_detail"],  # 명시
    ...
},
"composite_image_gen": {
    ...,
    "step_type": "asset",
    ...
},
```

### 4.4 유형별 리팩토링 규칙

- **transform step**: Phase 3에서 DTO 전환 가능. 단위 테스트 용이 (mock 입력 → assert 출력).
- **projection step**: Phase 2에서 Service로 분해. DB fixture로 테스트.
- **asset step**: Phase 3에서 경계 재정의. 파일 I/O mock 테스트.
- **editorial step**: 덮어쓰는 대상을 manifest에 명시. invalidation 시 정방향으로 재실행 (역순 무효화 금지).

---

## 5. 원칙 5 — Step Catalog

### 5.1 배경

초안의 "Manifest가 중앙 레지스트리" 주장은 부족하다. 현재 step 관련 정보는 4곳에 분산:

| 관심사 | 현재 위치 | 소비자 |
|---|---|---|
| 메타 (label, order, depends_on, applicability) | `step_manifest.py::STEP_MANIFEST` | 모두 |
| Class binding | `core/steps/__init__.py::STEP_CLASSES` | `_get_step_runner` |
| Applicability resolver | `StepRunner.check_applicability` + `steps.py:191-204` + `steps.py:79-126` + UI 각각 | 4개 해석기 |
| UI visibility/lifecycle | `PipelineStepsPanel.tsx:173-185` | 프론트 |

→ Codex 지적대로 **catalog 분산**이 실제 문제. manifest만 정리해도 resolver drift가 남음.

### 5.2 Step Catalog 단일 계약

```python
# backend/app/core/step_catalog.py (신규)

from dataclasses import dataclass
from typing import Callable, Dict, List, Optional

@dataclass(frozen=True)
class StepEntry:
    step_id: str
    label: str
    category: str           # analysis | image | auxiliary
    order: float
    default_model: str
    provider: str
    depends_on: List[str]
    fan_out: bool
    applicability: str      # always | on_demand | disabled | if_*
    step_type: str          # transform | projection | asset | editorial
    lifecycle: str          # active | deprecated | removed
    resume_sensitive: bool = False
    modifies_checkpoints: List[str] = None  # editorial only
    runner_cls: Optional[type] = None       # Class binding


STEP_CATALOG: Dict[str, StepEntry] = _build_from_manifest_and_classes()


def resolve_applicability(entry: StepEntry, ctx: "ApplicabilityContext") -> bool:
    """모든 호출 경로가 같은 resolver 사용."""
    ...


def get_active_steps(ctx) -> List[StepEntry]:
    """UI + run_all_steps + get_all_steps 공용."""
    ...


def get_all_downstream_recursive(step_id: str) -> List[str]:
    """기존 함수와 동일 (manifest 시절 그대로)."""
    ...
```

### 5.3 소비자 통합

| 소비자 | Phase 0 이전 | Phase 1 이후 |
|---|---|---|
| `StepRunner.check_applicability` | manifest + 서브클래스 override | `resolve_applicability(entry, ctx)` |
| `GET /steps` 응답 | manifest + 수동 필터 | `get_active_steps(ctx)` |
| `POST /steps/run-all` step 선택 | manifest + applicability 일부 | `get_active_steps(ctx, category=...)` |
| UI `PipelineStepsPanel` 필터 | 문자열 match | API가 결정, UI는 렌더만 |
| `toggle_shot_selection` downstream | 하드코딩 | `get_all_downstream_recursive("shot_selection")` |

### 5.4 manifest는 catalog의 데이터 소스

manifest 파일(`step_manifest.py`)은 **catalog의 선언적 입력**으로 남는다. Class binding은 `STEP_CLASSES`에서 자동 merge. Runtime resolver만 catalog에서 제공.

---

## 6. 원칙 8 🆕 — Step 경계 명시성

### 6.1 배경

Codex 지적한 `RefImageGenStep`이 `composite_image_gen` 상태를 완료 처리하는 현상은 **step 경계의 모호함**의 사례. 한 step이 다른 step의 상태를 기록하면:
- Snapshot/restore 시 두 step이 한 묶음으로 처리되어야 함 (현재는 따로)
- invalidation 시 의도 못 한 상태 변화 발생
- step_run 테이블 기록과 실제 작업 분리 불가능

### 6.2 원칙

**한 step의 `_execute()`는 다음 3종 중 하나만 수행**:

1. 자기 step의 체크포인트 쓰기 (`save_checkpoint`)
2. 자기 step의 `step_run` 갱신 (StepRunner가 자동 처리)
3. 자기 step의 도메인 DB 객체 생성/수정 (asset step만)

**금지**:
- 다른 step의 `step_run.status` 직접 변경
- 다른 step의 체크포인트 덮어쓰기 (**editorial step은 예외, manifest에 명시 시**)
- 파이프라인 흐름 자체를 변경 (예: 다음 step invalidation을 step이 직접 호출 — StepRunner가 담당)

### 6.3 Phase 3 작업

- `RefImageGenStep._mark_composite_done` 제거
- `CompositeImageGenStep._execute`가 **자기 책임만** 수행 (composite 이미지 생성)
- `ImageService.generate_reference_images_only` → 두 개 메서드로 분리 (`generate_base_references`, `generate_composites`)
- 각 step이 해당 메서드만 호출

### 6.4 `t2i_review` 예외

`t2i_review`는 editorial step. 자기 체크포인트는 리뷰 로그, **타 step 체크포인트(`entity_t2i`, `scene_detail`) 덮어쓰기**가 본질. 이는 manifest에 명시:

```python
"t2i_review": {
    ...,
    "step_type": "editorial",
    "modifies_checkpoints": ["entity_t2i", "scene_detail"],
    ...
}
```

덮어쓰기 시 `_save_checkpoint_data`(`t2i_review_step.py:68-92`)가 archive + 원자 쓰기를 수행 — 이 패턴은 유지.

---

## 7. 원칙 9 🆕 — 공식 경로 단일화

### 7.1 배경

`AnalysisService`(`analysis_service.py`, 1,137줄)와 `StepRunner` 경로가 병행 운영 중이지만, Frontend는 이미 StepRunner만 호출 (`EpisodeDetail.tsx:821`, `PipelineStepsPanel.tsx:125`).

### 7.2 원칙

**공식 분석 경로는 `StepRunner`**. 근거:
1. Frontend 실사용 현실
2. 기능 우위 (snapshot, fan-out 병렬, 부분 실행, 재개)
3. v4 beat→shot 네이티브 지원 (AnalysisService는 v3 entity_extractor_v3 기반)

### 7.3 Phase 4 수렴 작업

- `AnalysisService` 제거 (1,137줄 제거)
- `POST /episodes/{id}/analyze`: 내부를 `run_all_steps(category="analysis")` 래퍼로 교체
- `POST /episodes/{id}/reanalyze-scenes`: 관련 step만 force 실행하는 partial run-all 래퍼
- `_run_analysis_in_background` 제거

### 7.4 하위 호환

외부 integrations가 `/episodes/{id}/analyze`를 직접 호출하는 경우를 대비해 엔드포인트는 유지. 단, 내부는 StepRunner로 완전 교체. Response schema 호환 유지.

---

## 8. 원칙 2·6·7 (유지)

### 8.1 원칙 2 — 명시적 계약

- 모든 체크포인트는 schema 문서화 (`docs/architecture/06-data-contracts.md` 이미 존재, Phase 0에서 갱신)
- Step의 입력(depends_on의 체크포인트 구조) 및 출력(data 필드) 명시
- Service 메서드의 입력/출력 type hint 필수

### 8.2 원칙 6 — 선언적 > 명령적

- manifest가 의존성 그래프의 single source → 하드코딩 downstream 제거
- applicability rule은 선언, resolver가 해석 → override 최소화
- frontend filter는 backend 응답 기반 → 하드코딩 step_id 제거

### 8.3 원칙 7 — 테스트 가능성 우선

- Phase 0에서 test baseline 복구
- Phase 2~5에서 Service별 단위 테스트 추가
- DTO 전환 시 테스트 fixture 쉽게 작성 가능

---

## 9. 원칙 간 우선순위

의사결정 시 원칙 간 상충 해결:

1. **원칙 3 (단방향) > 원칙 4 (유형별 규칙)**: 유형별 예외도 레이어 위반은 금지
2. **원칙 8 (경계) > 원칙 4 (유형별 규칙)**: 같은 asset step이라도 타 step 상태 변경 금지
3. **원칙 9 (단일 경로) > 원칙 1 (3-tier)**: 경로 단일화가 먼저, 티어 세분화는 이후
4. **원칙 2 (계약) > 원칙 6 (선언적)**: 선언적 해결이 불가능하면 계약 명시로 대체
5. **원칙 7 (테스트) > 모든 원칙**: Phase 0에서 테스트가 선행되지 않으면 리팩토링 실행 금지

---

## 10. 원칙 → Phase 매핑

| 원칙 | Phase 0 | Phase 1 | Phase 2 | Phase 3 | Phase 4 | Phase 5 |
|---|---|---|---|---|---|---|
| 1. 3-tier 진실원 | 문서화 | catalog 도입 | sync 분해 | — | — | FE 캐시 |
| 2. 명시적 계약 | 문서 갱신 | catalog schema | Service API | DTO | — | query schema |
| 3. 단방향 의존성 | — | — | 역의존 해소 | Service 분할 | legacy 제거 | — |
| 4. Step 유형 | — | manifest 필드 | projection Service | step 경계 | — | — |
| 5. Step Catalog | — | 신설 | resolver 통합 | — | — | — |
| 6. 선언적 | 문서 일치 | 하드코딩 제거 | — | — | — | — |
| 7. 테스트 | **baseline 복구** | validator 테스트 | Service 테스트 | DTO 테스트 | — | FE E2E |
| 8. Step 경계 | — | step_type 필드 | — | image 경계 재정의 | — | — |
| 9. 단일 경로 | 선언 | — | — | — | **AnalysisService 제거** | — |

다음 문서 `02-final-roadmap.md`에서 Phase별 구체 작업과 검증을 다룬다.
