# engine.py → engine_v2 봇 마이그레이션 계획서

> 작성: task-686.1 Phase 5 (G14) | 2026-03-18

## 현황 (engine.py 호출처 전수조사)

```
claude_bot.py:14:from engine import call_claude
codex_bot.py:14:from engine import call_codex
conversation_memory.py:75:    from engine import call_claude  # type: ignore[import-not-found]
gemini_bot.py:14:from engine import call_gemini
main_bot.py:20:from engine import call_claude, call_codex, call_gemini
party_bot.py:17:from engine import call_gemini, call_codex, call_claude
tests/test_engine.py:101:        import engine
tests/test_engine.py:138:        import engine
tests/test_engine.py:154:                result = await engine.call_gemini("테스트 질문입니다.")
tests/test_engine.py:161:        import engine
tests/test_engine.py:180:                result = await engine.call_gemini("타임아웃 테스트", timeout=1)
tests/test_engine.py:189:        import engine
tests/test_engine.py:205:                result = await engine.call_gemini("403 에러 테스트")
tests/test_engine.py:215:        import engine
tests/test_engine.py:231:                result = await engine.call_gemini("500 에러 테스트")
tests/test_engine.py:240:        import engine
tests/test_engine.py:257:                result = await engine.call_gemini("빈 응답 테스트")
tests/test_engine.py:265:        import engine
tests/test_engine.py:281:                result = await engine.call_gemini("빈 텍스트 테스트")
tests/test_engine.py:289:        import engine
tests/test_engine.py:308:                    result = await engine.call_gemini("토큰 refresh 테스트")
tests/test_engine.py:317:        import engine
tests/test_engine.py:336:                result = await engine.call_gemini("캐시 테스트")
tests/test_engine.py:347:        import engine
tests/test_engine.py:372:                result = await engine.call_gemini("project id 조회 테스트")
tests/test_engine.py:382:        import engine
tests/test_engine.py:390:                result = await engine.call_gemini("예외 테스트")
tests/test_engine.py:414:            from engine import call_codex
tests/test_engine.py:430:            from engine import call_codex
tests/test_engine.py:453:            from engine import call_codex
tests/test_engine.py:472:            from engine import call_codex
tests/test_engine.py:484:            from engine import call_codex
tests/test_engine.py:497:            from engine import call_codex
tests/test_engine.py:516:                from engine import call_codex
tests/test_engine.py:534:            from engine import call_codex
tests/test_engine.py:546:            from engine import call_codex
tests/test_engine.py:563:            from engine import call_codex
tests/test_engine.py:579:            from engine import call_codex
tests/test_engine.py:596:            from engine import call_codex
tests/test_engine.py:614:            from engine import call_codex
tests/test_engine.py:632:            from engine import call_codex
tests/test_engine.py:650:            from engine import call_codex
tests/test_engine.py:670:            from engine import call_codex
tests/test_engine.py:683:            from engine import call_codex
tests/test_engine.py:697:            from engine import call_codex
tests/test_engine.py:711:            from engine import call_codex
tests/test_engine.py:731:            from engine import call_claude
tests/test_engine.py:741:            from engine import call_claude
tests/test_engine.py:752:            from engine import call_claude
tests/test_engine.py:765:            from engine import call_claude
tests/test_engine.py:776:            from engine import call_claude
tests/test_engine.py:787:            from engine import call_claude
tests/test_engine.py:802:            from engine import call_claude
tests/test_engine.py:816:                from engine import call_claude
tests/test_engine.py:836:            from engine import call_claude
tests/test_engine.py:854:            from engine import call_claude
tests/test_engine.py:870:            from engine import call_claude
tests/test_engine_v2_phase2.py:343:        import engine_v2
tools/test_insight_runner.py:245:        from engine import call_claude  # type: ignore[import]  # noqa: F401
tools/test_insight_runner.py:247:        step("call_claude import", True, "engine.call_claude 사용 가능")
```

### 호출처 요약

| 파일 | 호출 함수 | 비고 |
|------|-----------|------|
| `claude_bot.py` | `call_claude` | 프로덕션 봇 |
| `codex_bot.py` | `call_codex` | 프로덕션 봇 |
| `gemini_bot.py` | `call_gemini` | 프로덕션 봇 |
| `main_bot.py` | `call_claude`, `call_codex`, `call_gemini` | 프로덕션 봇 (3개 모두) |
| `party_bot.py` | `call_gemini`, `call_codex`, `call_claude` | 프로덕션 봇 (3개 모두) |
| `conversation_memory.py` | `call_claude` | 유틸리티 (조건부 import) |
| `tests/test_engine.py` | `engine.call_gemini`, `call_codex`, `call_claude` | 테스트 파일 (다수 라인) |
| `tools/test_insight_runner.py` | `call_claude` | 도구 스크립트 |

프로덕션 파일 6개 + 테스트 1개 + 도구 1개 = **총 8개 파일**에서 engine.py 의존성 존재

## 마이그레이션 단계별 로드맵

### Phase M1: 호환 레이어 (1일)
- engine.py의 call_gemini/call_codex/call_claude를 engine_v2 CLIRunner 래퍼로 교체
- 기존 호출 코드 변경 없이 동작 보장

### Phase M2: 직접 마이그레이션 (2-3일)
- 각 봇 파일에서 `from engine import call_xxx` → `from engine_v2 import CLIRunner` 교체
- 반환값 EngineResult 활용으로 에러 처리 개선

### Phase M3: engine.py 제거 (1일)
- 모든 호출처 전환 확인
- engine.py 삭제 (또는 빈 redirect 유지)
- import 오류 발생 시 CI에서 즉시 감지

## 위험 요소
1. OAuth 토큰 refresh race condition (engine.py + engine_v2 동시 호출 시)
2. 봇 재시작 시 환경변수 미로드 가능성
3. 테스트 커버리지 부족 구간

## 롤백 계획
- engine.py는 삭제하지 않고 DEPRECATED 유지
- Phase M2까지 engine.py 동작 보장
- 문제 발생 시 git revert로 즉시 복구
