> **ARCHIVED (2026-07-30)**: 본 문서는 Phase 0-1 실행 계획(2026-07-24)으로, 현재 상태와 일부 차이가 있습니다. > 최신 정보는 [`../CLAUDE.md`](../CLAUDE.md) Migration Status 섹션 또는 > [`STRATEGIC_EXECUTION_MASTER_PLAN.md`](../STRATEGIC_EXECUTION_MASTER_PLAN.md)를 참고하세요. # QuantEngine 현대화 실행 계획 **Phase 0 마무리 + Phase 1 준비** (2026-07-24 ~ 2026-09-30) --- ## Executive Overview **현재 상태**: Phase 0 ✅ 기술적 기초 완료 - CI/CD 파이프라인 리팩토링 (9-job parallel, ~15-20min) ✅ - CLAUDE.md 종합 문서화 ✅ - 현대화 로드맵 수립 ✅ **목표**: Phase 0 운영 검증 + Phase 1 (데이터 아키텍처 고도화) 착수 **기간**: 2026-07-24 ~ 2026-09-30 (9주) **리소스**: 1 FTE (클로드 코드) + 팀 지원 --- ## Part 1: Phase 0 운영 검증 (Jul 24 - Aug 31) — 4주 ### 목표 현대화 로드맵의 기초가 견고한지 검증 ### 1.1 CI/CD 파이프라인 안정성 검증 #### Task 1.1.1: 실제 워크플로우 성능 측정 **목표**: 예상 15-20분이 실제 달성되는지 확인 **구체적 작업**: ```yaml Week 1 (Jul 24-31): - Commit 3-5개 추가 (다양한 변경 유형) * C# 코드 변경 * Python 스크립트 변경 * 데이터베이스 마이그레이션 추가 * YAML 워크플로우 변경 - 각 CI 실행 로그 분석: ├─ core job 시간 (DB 마이그레이션 포함) ├─ 병렬 job 시간 (wbs-audit, dotnet-contracts, ui-storage, etc.) ├─ notify-results 시간 └─ 총 벽시간 (wall clock time) - 병목 지점 식별: * 만약 core > 10분: DB 마이그레이션 최적화 필요 * 만약 any parallel > 8분: 해당 job 분할 검토 * 만약 total > 25분: 추가 병렬화 또는 검증 제거 검토 Expected output: "CI Performance Baseline 2026-07-31.json" ``` **SOLID 원칙 적용**: - **Single Responsibility**: 각 job은 하나의 검증만 담당 - **Dependency Inversion**: 모든 job이 동등하게 core에만 의존 (필요시) #### Task 1.1.2: 워크플로우 재현성 검증 **목표**: 같은 커밋에서 CI 실행 결과가 항상 동일한지 확인 **구체적 작업**: ```python # tools/verify_ci_reproducibility_v1.py class CIReproducibilityValidator: def test_same_commit_same_result(self, commit_sha): """ 같은 커밋을 2번 이상 재실행하여 결과 비교 - All jobs: PASS or FAIL 결과 동일 - Test output: 정확히 일치 - Build artifacts: 바이너리 동일 (deterministic build) """ results = [] for run in range(3): result = self.trigger_ci(commit_sha) results.append(result) assert all(r == results[0] for r in results), \ "CI results not reproducible!" return True def test_no_hidden_state(self): """ CI가 외부 상태에 의존하지 않는지 확인 - 시간에 따른 결과 변화 없음 (timestamp-independent) - 환경변수 없어도 성공 (except secrets) - 테스트 데이터 일관성 (seed 고정) """ pass # CI에 추가할 Step ci.yml: - name: "Verify CI Reproducibility" run: python3 tools/verify_ci_reproducibility_v1.py ``` **목표 지표**: - ✅ 3회 연속 재실행 성공률: 100% - ✅ 결과 일관성: 100% (no flaky tests) - ✅ Deterministic build: 바이너리 hash 일치 --- ### 1.2 데이터 일관성 기초 다지기 #### Task 1.2.1: PostgreSQL 이력 테이블 설계 및 구현 **목표**: 모든 데이터 변경의 감시 추적(audit trail) 기초 마련 **구체적 작업**: ```sql -- src/dotnet/QuantEngine.Infrastructure/Migrations/V003_add_audit_trail.sql -- 이력 테이블 템플릿 CREATE TABLE kis_collection_runs_audit ( id BIGSERIAL PRIMARY KEY, run_id UUID NOT NULL, -- 원본 테이블의 FK action VARCHAR(10) NOT NULL, -- INSERT, UPDATE, DELETE changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), changed_by VARCHAR(256), -- 누가? (user ID 또는 "scheduler") change_reason TEXT, -- 왜? (migration, manual edit, etc.) -- 변경 전/후 스냅샷 old_values JSONB, -- 변경 전 전체 row new_values JSONB, -- 변경 후 전체 row INDEX (run_id, changed_at DESC), INDEX (changed_by, changed_at DESC) ); -- kis_collection_snapshots_audit 유사 구조 CREATE TABLE kis_collection_snapshots_audit ( id BIGSERIAL PRIMARY KEY, snapshot_id UUID NOT NULL, action VARCHAR(10) NOT NULL, changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), changed_by VARCHAR(256), change_reason TEXT, old_values JSONB, new_values JSONB, INDEX (snapshot_id, changed_at DESC) ); -- Trigger: kis_collection_snapshots 변경 시 자동 기록 CREATE OR REPLACE FUNCTION kis_collection_snapshots_audit_trigger() RETURNS TRIGGER AS $$ BEGIN IF TG_OP = 'INSERT' THEN INSERT INTO kis_collection_snapshots_audit (snapshot_id, action, changed_by, new_values) VALUES (NEW.id, 'INSERT', CURRENT_USER, row_to_json(NEW)); ELSIF TG_OP = 'UPDATE' THEN INSERT INTO kis_collection_snapshots_audit (snapshot_id, action, old_values, new_values) VALUES (NEW.id, 'UPDATE', row_to_json(OLD), row_to_json(NEW)); END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER kis_collection_snapshots_after_change AFTER INSERT OR UPDATE ON kis_collection_snapshots FOR EACH ROW EXECUTE FUNCTION kis_collection_snapshots_audit_trigger(); ``` **C# Repository 패턴 (Wrapper)**: ```csharp public class AuditedSnapshotRepository : ISnapshotRepository { private readonly ISnapshotRepository _inner; private readonly IAuditLogger _audit; public async Task SaveSnapshotAsync(SnapshotDto snapshot, string changedBy, string reason) { // 변경 전 상태 저장 var before = await _inner.GetAsync(snapshot.Id); // 실제 저장 await _inner.SaveAsync(snapshot); // 감시 추적 기록 await _audit.LogChangeAsync(new AuditEntry { EntityId = snapshot.Id, EntityType = "Snapshot", Action = "UPDATE", ChangedBy = changedBy, ChangeReason = reason, OldValues = before, NewValues = snapshot, ChangedAt = DateTime.UtcNow }); } } ``` **성과지표**: - ✅ 모든 kis_* 테이블에 이력 추적 활성화 - ✅ 이력 조회 API 구현 (`/api/audit/logs?entity=snapshot&id=...`) - ✅ 수동 개입 추적: who, when, why 100% 기록 #### Task 1.2.2: 데이터 정합성 검증 자동화 **목표**: 매일 자동으로 데이터 품질 점검 **구체적 작업**: ```python # tools/validate_data_consistency_daily_v1.py class DailyDataConsistencyValidator: def validate_kis_snapshots(self): """ kis_collection_snapshots 데이터 품질 검사 """ issues = [] # 1. 완전성 (Completeness) total = self.db.query("SELECT COUNT(*) FROM kis_collection_snapshots") nulls = self.db.query("SELECT COUNT(*) FROM kis_collection_snapshots WHERE price IS NULL") completeness = (total - nulls) / total * 100 if completeness < 95: issues.append(f"Completeness low: {completeness:.1f}%") # 2. 신선도 (Freshness) latest = self.db.query("SELECT MAX(created_at) FROM kis_collection_snapshots") age_hours = (now() - latest).total_seconds() / 3600 if age_hours > 25: issues.append(f"Data stale: {age_hours:.1f} hours old") # 3. 정합성 (Consistency) — bid <= mid <= ask invalid = self.db.query(""" SELECT COUNT(*) FROM kis_collection_snapshots WHERE NOT (bid <= price AND price <= ask) """) if invalid > 0: issues.append(f"Bid-mid-ask consistency violated: {invalid} rows") # 4. 이상값 (Outliers) — 3-sigma rule stats = self.db.query(""" SELECT AVG(price) as mean, STDDEV(price) as std FROM kis_collection_snapshots WHERE created_at > NOW() - INTERVAL 30 DAY """) outliers = self.db.query(""" SELECT COUNT(*) FROM kis_collection_snapshots WHERE ABS(price - %s) > 3 * %s """, stats.mean, stats.std) outlier_pct = outliers / total * 100 if outlier_pct > 5: issues.append(f"Outliers detected: {outlier_pct:.1f}%") # 5. 중복 검사 (Duplicates) duplicates = self.db.query(""" SELECT COUNT(*) - COUNT(DISTINCT ticker, created_at) FROM kis_collection_snapshots WHERE created_at > NOW() - INTERVAL 1 DAY """) if duplicates > 0: issues.append(f"Duplicates found: {duplicates} rows") return { "timestamp": now(), "completeness_pct": completeness, "freshness_hours": age_hours, "consistency_violations": invalid, "outliers_pct": outlier_pct, "duplicates": duplicates, "status": "PASS" if not issues else "FAIL", "issues": issues } # 매일 cron으로 실행 (kis_data_collection.yml 확장) # Slack 알림: completeness < 95% 또는 freshness > 25h ``` **CI 게이트로 추가**: ```yaml # .gitea/workflows/kis_data_collection.yml (기존) → 확장 - name: "Validate Daily Data Consistency" run: python3 tools/validate_data_consistency_daily_v1.py --mode strict # strict mode: 모든 게이트 PASS 필요 ``` **성과지표**: - ✅ 자동 데이터 품질 점검 일일 1회 - ✅ 신선도, 완전성, 정합성, 이상값 추적 - ✅ 수동 개입 필요 시 → Slack 알림 자동화 --- ### 1.3 운영 안정성 검증 #### Task 1.3.1: 배포 프로세스 엔드-투-엔드 테스트 **목표**: 실제 배포까지 자동화 검증 **구체적 작업**: ```bash # 시나리오 1: 정상 배포 1. Local build (Release) → 성공 2. E2E 테스트 → 성공 3. Admin 페이지 모두 200 응답 4. git push main 5. CI 모든 job 통과 6. prepare-release.yml 수동 실행 → Gitea Release 생성 (v0.1.20260731.0.abc1234) 7. deploy-prod.yml 수동 실행 → SSH 배포 + 6점 health check 8. 검증: - Login 페이지 로드 ✓ - CSS/JS 로드 ✓ - Service active ✓ - DB 연결 ✓ - Release tag 일치 ✓ # 시나리오 2: 배포 실패 및 롤백 1. Deploy 중단 (health check 실패) 2. 이전 버전 확인: ln -sfn quantengine_20260718_abc1234 3. systemctl restart quantengine 4. Health check 재실행 → 통과 # 시나리오 3: 데이터베이스 마이그레이션 1. V003_add_audit_trail.sql 배포 2. 기존 데이터 호환성 확인 - SELECT COUNT(*) FROM kis_collection_runs (레코드 동일) - Audit 트리거 작동 확인 3. Rollback 계획 검증 - DROP TRIGGER / DROP TABLE 스크립트 준비 - 테스트 환경에서 실행 ``` **체크리스트 작성**: ```markdown # docs/DEPLOYMENT_VERIFICATION_CHECKLIST.md ## Pre-Deployment - [ ] Local build: 0 errors, 0 warnings - [ ] E2E tests: all pass - [ ] Admin pages: /Dashboard, /Users, /Collection → 200 - [ ] git status: clean (no uncommitted changes) - [ ] git log: all commits pushed to origin ## Release Creation (prepare-release.yml) - [ ] Workflow status: SUCCESS - [ ] Gitea Release created (v0.1.YYYYMMDD.N.hash) - [ ] Artifact downloaded locally (for manual verification) - [ ] Checksum validated: `sha256sum -c artifact.sha256` ## Production Deployment (deploy-prod.yml) - [ ] SSH connection: successful - [ ] Artifact uploaded: confirmed on server - [ ] Extract & symlink: verified - [ ] Service restart: active ## Health Checks (6-point) - [ ] HTTP 200: GET /Account/Login - [ ] Login page content: contains "login" or "로그인" - [ ] CSS: GET /css/admin.css → 200 - [ ] Service: systemctl is-active quantengine → active - [ ] Release tag: matches deployed version - [ ] DB auth: journalctl -u quantengine (no 28P01 errors) ## Post-Deployment Verification - [ ] Live app accessible: https://quant.taxbaik.com/ - [ ] Admin pages load: /Admin/Dashboard → 200 - [ ] API responds: /api/collection/state → 200 - [ ] Monitoring active: Prometheus/Grafana (if enabled) ``` **성과지표**: - ✅ 3회 연속 배포 성공 (prepare-release + deploy-prod) - ✅ 배포 실패 시 자동 롤백 검증 - ✅ 배포 시간 추적: <30분 total --- ## Part 2: Phase 1 준비 (Sep 1-30) — 5주 ### 목표 데이터 정규화 설계 완료 및 첫 마이그레이션 준비 ### 2.1 데이터 정규화 설계 #### Task 2.1.1: 3NF 스키마 설계 및 검증 **목표**: 현재 비정규 kis_collection_snapshots → 3NF로 재설계 **구체적 작업**: ```sql -- Current (비정규화) — kis_collection_snapshots -- 100+ columns: ticker, price, volume, bid1-5, ask1-5, pe_ratio, eps, ... -- Target (3NF) — 테이블 분리 CREATE TABLE stocks ( id UUID PRIMARY KEY, ticker VARCHAR(10) NOT NULL UNIQUE, name VARCHAR(256), market VARCHAR(20), -- KOSPI, KOSDAQ, KONEX created_at TIMESTAMPTZ, INDEX (ticker) ); CREATE TABLE quotes ( id UUID PRIMARY KEY, stock_id UUID NOT NULL REFERENCES stocks(id), timestamp TIMESTAMPTZ NOT NULL, price DECIMAL(15,2) NOT NULL, volume BIGINT, source VARCHAR(50), -- KIS, Naver, Yahoo created_at TIMESTAMPTZ, FOREIGN KEY (stock_id) REFERENCES stocks(id), INDEX (stock_id, timestamp DESC), INDEX (timestamp) ); CREATE TABLE order_book ( id UUID PRIMARY KEY, quote_id UUID NOT NULL REFERENCES quotes(id), bid_prices DECIMAL(15,2)[] NOT NULL, -- [bid1, bid2, ..., bid5] bid_sizes BIGINT[] NOT NULL, ask_prices DECIMAL(15,2)[] NOT NULL, ask_sizes BIGINT[] NOT NULL, FOREIGN KEY (quote_id) REFERENCES quotes(id), INDEX (quote_id) ); CREATE TABLE fundamentals ( id UUID PRIMARY KEY, stock_id UUID NOT NULL REFERENCES stocks(id), as_of_date DATE NOT NULL, eps DECIMAL(15,4), pe_ratio DECIMAL(15,2), dividend DECIMAL(15,2), book_value DECIMAL(15,2), FOREIGN KEY (stock_id) REFERENCES stocks(id), UNIQUE (stock_id, as_of_date), INDEX (stock_id) ); ``` **정규화 검증**: ```python # tools/validate_schema_normalization_v1.py class NormalizationValidator: def validate_3nf(self): """ 3NF 검증: 1. 1NF: 모든 테이블이 atomic values만 포함 2. 2NF: 비키 속성이 전체 키에 의존 (partial dependency 없음) 3. 3NF: 비키 속성이 다른 비키 속성에 의존하지 않음 (transitive dependency 없음) """ issues = [] # 1NF: 배열/객체 타입 확인 (JSON 제외 대부분) for table in self.db.tables: for col in table.columns: if col.type in ['array', 'object']: if col.name not in ['bid_prices', 'ask_prices', 'bid_sizes', 'ask_sizes']: issues.append(f"1NF violation: {table}.{col} is {col.type}") # 2NF: Foreign Key 의존성 확인 for table in self.db.tables: for col in table.columns: if col.is_foreign_key: # 비키 속성이 전체 키에만 의존하는지 확인 if not self._depends_on_full_key(table, col): issues.append(f"2NF violation: {table}.{col} partial dependency") # 3NF: 비키 속성 간 의존성 확인 for table in self.db.tables: for col in table.columns: if not col.is_key and not col.is_foreign_key: for other_col in table.columns: if not other_col.is_key and col != other_col: if self._functionally_dependent(col, other_col): issues.append(f"3NF violation: {table}.{col} depends on {other_col}") return { "status": "PASS" if not issues else "FAIL", "issues": issues, "tables_checked": len(self.db.tables) } ``` **과유불급(YAGNI) 원칙 적용**: - ✅ 필요한 분리만: 100+ columns → 5개 주요 테이블 - ✅ 과도한 정규화 금지: 과도한 조인 피함 - ❌ 조회 성능 향상 위해 의도적 역정규화는 나중 (벤치마크 후) **성과지표**: - ✅ 3NF 검증 통과 (1NF, 2NF, 3NF 모두) - ✅ 데이터 무결성 제약 정의 (FK, CHECK, UNIQUE) - ✅ 스토리지 절감 예상: 40% (column 중복 제거) #### Task 2.1.2: 마이그레이션 전략 수립 (Blue-Green Deployment) **목표**: 무중단 데이터 마이그레이션 계획 **구체적 작업**: ```markdown # 마이그레이션 전략: Blue-Green (Parallel Run) ## Phase 1: Prepare (1주) 1. 새 테이블 생성 (stocks, quotes, order_book, fundamentals) 2. 데이터 변환 로직 구현 - kis_snapshots → stocks/quotes/order_book 변환 - 데이터 검증 (row count, aggregates) 3. 테스트 환경에서 전체 마이그레이션 실행 및 검증 ## Phase 2: Dual Write (1주) 1. 애플리케이션 수정: 새 테이블에도 INSERT/UPDATE ```csharp await _legacyRepository.SaveAsync(snapshot); // 기존 await _normalizedRepository.SaveAsync(snapshot); // 신규 ``` 2. 두 테이블 데이터 정합성 비교 - SELECT COUNT(*) 일치 확인 - Aggregates (SUM, AVG) 일치 확인 3. 한 주일 운영: 모든 쿼리가 일관된 결과 반환하는지 확인 ## Phase 3: Read Cutover (1주) 1. 읽기(SELECT) 쿼리를 새 테이블에서 수행 시작 ```csharp // Before var snapshot = await _legacyRepository.GetAsync(id); // After var snapshot = await _normalizedRepository.GetAsync(id); ``` 2. API 응답이 동일한지 검증 3. 성능 비교: 새 테이블 쿼리가 더 빠른지 확인 ## Phase 4: Write Cutover (1주) 1. 쓰기(INSERT/UPDATE) 쿼리도 새 테이블만 사용 2. 기존 테이블은 읽기 전용으로 전환 3. Dual write 제거 ## Phase 5: Cleanup (1주) 1. 기존 테이블 백업: kis_snapshots_archived_20260930 2. 모니터링: 일주일 후에도 안정적인지 확인 3. 필요시 기존 테이블 제거 ``` **Adapter Pattern으로 호환성 유지**: ```csharp public class LegacySnapshotAdapter : ISnapshotRepository { private readonly IQuoteRepository _newQuotes; public async Task GetAsync(string ticker) { // 새 테이블에서 읽음 var quote = await _newQuotes.GetLatestAsync(ticker); // 기존 SnapshotDto 형식으로 변환 return new SnapshotDto { Ticker = quote.Stock.Ticker, Price = quote.Price, Volume = quote.Volume, Bid = quote.OrderBook.BidPrices[0], Ask = quote.OrderBook.AskPrices[0], // ... 나머지 100+ 필드들도 매핑 }; } } // 사용처: API, Controller는 변경 없음 public class CollectionApiEndpoints { public async Task GetSnapshot(string ticker) { var snapshot = await _repository.GetAsync(ticker); // 자동으로 새 테이블 사용 return Ok(snapshot); } } ``` **성과지표**: - ✅ 마이그레이션 계획 상세 정의 - ✅ Rollback 프로세스 테스트 - ✅ 예상 다운타임: 0분 (무중단) --- ### 2.2 SOLID 원칙 적용 설계 #### Task 2.2.1: Repository 인터페이스 분리 (Interface Segregation) **목표**: 비대한 ICollectionRepository → 작은 책임의 인터페이스로 분리 **구체적 작업**: ```csharp // BEFORE (ISP 위반) public interface ICollectionRepository { Task GetSnapshotAsync(string ticker); Task GetRunAsync(Guid runId); Task GetErrorAsync(Guid errorId); Task SaveSnapshotAsync(SnapshotDto snapshot); Task SaveRunAsync(RunDto run); Task DeleteErrorAsync(Guid errorId); } // AFTER (ISP 준수) public interface IQuoteRepository { Task GetLatestAsync(string ticker); Task> GetHistoryAsync(string ticker, DateRange range); Task SaveAsync(QuoteDto quote); } public interface ICollectionRunRepository { Task GetAsync(Guid runId); Task> GetRecentAsync(int limit); Task SaveAsync(RunDto run); } public interface ICollectionErrorRepository { Task GetAsync(Guid errorId); Task> GetByRunAsync(Guid runId); Task SaveAsync(ErrorDto error); } public interface IStockRepository { Task GetByTickerAsync(string ticker); Task> GetAllAsync(); } // 사용처 public class CollectionService { private readonly IQuoteRepository _quotes; private readonly ICollectionRunRepository _runs; private readonly ICollectionErrorRepository _errors; public CollectionService( IQuoteRepository quotes, ICollectionRunRepository runs, ICollectionErrorRepository errors) { _quotes = quotes; _runs = runs; _errors = errors; } // 각 메서드는 필요한 인터페이스만 사용 } ``` **성과지표**: - ✅ 불필요한 메서드 의존성 제거 - ✅ 테스트 편의성: Mock 주입 간단 - ✅ 변경 영향도 최소화 #### Task 2.2.2: Dependency Inversion 구현 (DI Container) **목표**: 고수준 모듈이 저수준 모듈에 의존하지 않기 **구체적 작업**: ```csharp // Program.cs (DI 설정) services // Repository abstraction .AddScoped(sp => new AuditedQuoteRepository( new QuoteRepository(sp.GetRequiredService()), sp.GetRequiredService())) // Data source abstraction (Strategy pattern) .AddScoped(sp => new DataSourceFactory( sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService())) // Fallback chain .AddScoped(sp => new FallbackQuotationService( new KisQuotationService(sp.GetRequiredService()), new NaverQuotationService(sp.GetRequiredService()), new YahooQuotationService(sp.GetRequiredService()))) // Validation .AddScoped(sp => new DataQualityValidator(sp.GetRequiredService())) .AddScoped(); // CollectionService (고수준)는 세부 구현을 모름 public class CollectionService { private readonly IQuotationService _quotation; // 추상화만 의존 private readonly IQuoteRepository _repository; // 추상화만 의존 public async Task RunAsync() { // 구체적 구현은 DI container가 주입 var quote = await _quotation.GetAsync("005930"); await _repository.SaveAsync(quote); } } ``` **성과지표**: - ✅ 느슨한 결합 (Loose coupling) - ✅ 런타임 구성 가능 (Strategy switching) - ✅ 테스트 용이 (Mock 쉽게 주입) --- ### 2.3 패턴 및 표준 정립 #### Task 2.3.1: Architecture Decision Records (ADR) 작성 **목표**: 왜 이런 선택을 했는가? 의사결정 기록 **구체적 작업**: ```markdown # docs/adr/0003-3nf-normalization.md ## Status ACCEPTED ## Context 현재 kis_collection_snapshots 테이블이 비정규화되어 있음: - 100+ columns (price, bid1-5, ask1-5, eps, pe_ratio, ...) - 데이터 중복 (ticker는 매번 저장) - 업데이트 이상 (fundamentals 변경 시 모든 행 수정) - 스토리지 비효율 (같은 데이터 반복) ## Decision PostgreSQL 스키마를 3NF로 정규화: - stocks: 종목 마스터 (ticker, name, market) - quotes: 시세 (stock_id, timestamp, price, volume) - order_book: 호가 (quote_id, bid/ask arrays) - fundamentals: 재무 (stock_id, eps, pe_ratio, ...) ## Consequences **Positive**: - 스토리지 40% 감소 - 데이터 무결성 자동 보장 (FK 제약) - 업데이트 이상 제거 - 명확한 데이터 의미 (각 테이블이 하나의 개념 표현) **Negative**: - JOIN 증가 (성능 영향, 인덱싱으로 완화) - 마이그레이션 복잡도 증가 (blue-green 필요) ## Alternatives Considered 1. 비정규화 유지 + 인덱싱만 개선 (rejected: 장기 유지 어려움) 2. 부분 정규화 (1NF만) (rejected: 불완전) ## Implementation - Phase 1a (Sep): 새 테이블 생성 + 검증 - Phase 1b (Oct): Blue-green 마이그레이션 - Phase 1c (Nov): 기존 테이블 아카이빙 ``` **추가 ADR들**: ``` docs/adr/ ├── 0001-razor-pages-over-wasm.md ├── 0002-dapper-orm-not-ef.md ├── 0003-3nf-normalization.md ├── 0004-game-theoretic-portfolio.md ├── 0005-audit-trail-every-change.md └── 0006-fallback-data-sources.md ``` **성과지표**: - ✅ 5개 이상의 ADR 작성 - ✅ 팀 검토 및 승인 - ✅ CLAUDE.md에 ADR 참조 추가 #### Task 2.3.2: Code Style Guide 작성 **목표**: "이 프로젝트에서는 이렇게 코딩한다" **구체적 작업**: ```markdown # CODING_STANDARDS.md ## C# Guidelines ### Repository Pattern ```csharp // DO public interface IQuoteRepository { Task GetByTickerAsync(string ticker); Task SaveAsync(QuoteDto quote); } // DON'T public interface IRepository { T Get(object id); void Save(T entity); } ``` ### Error Handling ```csharp // DO: Validate at boundary (API input) [HttpPost] public async Task CreateSnapshot(SaveSnapshotRequest request) { var validation = new SaveSnapshotValidator().Validate(request); if (!validation.IsValid) return BadRequest(validation.Errors); // ... } // DO: Trust internal guarantees public class QuoteRepository { public async Task SaveAsync(QuoteDto quote) { // quote가 null이 아님을 가정 (caller가 검증함) await _db.SaveAsync(quote); } } // DON'T: Unnecessary defensive checks if (quote != null && !quote.IsEmpty()) // 불필요 { // ... } ``` ### Comments ```csharp // DON'T: 무엇을 하는지 설명 (코드가 이미 말함) // 가격을 저장한다 await _repository.SaveAsync(quote); // DO: 왜 이렇게 하는지 설명 // KIS API는 대체로 가격을 30분 지연해서 보고하므로, // 최신 3시간 데이터만 보관하여 조회 성능 향상 const int RETENTION_HOURS = 3; ``` ## Python Guidelines ### Data Validation ```python # DO: 파이프라인 입구에서만 검증 def collect_quotes(raw_data: List[Dict]): """raw_data는 이미 스키마 검증됨""" quotes = [Quote(**item) for item in raw_data] return quotes # DON'T: 모든 곳에서 검증 def process_quote(q: Quote): if q is None: # 불필요 return if q.price < 0: # 불필요 (Quote 생성 시 이미 검증) return ``` ### Test Data ```python # DO: seed 고정 (재현성) np.random.seed(42) test_data = np.random.normal(100, 15, 1000) # DON'T: 시간에 따른 변화 test_timestamp = datetime.now() # ❌ 매번 다름 ``` ## SQL Guidelines ```sql -- DO: 매개변수화된 쿼리 SELECT * FROM quotes WHERE ticker = @ticker AND date > @startDate -- DON'T: 문자열 연결 (SQL injection 위험) SELECT * FROM quotes WHERE ticker = '" + ticker + "'" -- DO: 명확한 의도 CREATE INDEX idx_quotes_lookup ON quotes(stock_id, timestamp DESC); -- 인덱스 이름이 쿼리 의도를 반영 (stock_id로 최신부터) -- DO: 트랜잭션 명시 BEGIN TRANSACTION; INSERT INTO quotes (...) VALUES (...); INSERT INTO quotes_audit (...) VALUES (...); COMMIT; ``` ## Naming Conventions | 대상 | 규칙 | 예 | |------|------|-----| | 클래스 | PascalCase | `QuoteRepository`, `DailyDataValidator` | | 메서드 | PascalCase (verb-noun) | `GetQuoteAsync`, `ValidateDataAsync` | | 속성 | PascalCase | `StockId`, `CollectedAt` | | 지역변수 | camelCase | `quoteList`, `isValid` | | 상수 | UPPER_SNAKE_CASE | `MAX_RETRIES`, `DEFAULT_TIMEOUT` | | 인터페이스 | I + PascalCase | `IQuoteRepository`, `IDataValidator` | | DB 테이블 | snake_case (단수) | `kis_quote`, `collection_run` | | DB 컬럼 | snake_case | `created_at`, `stock_id` | ``` **성과지표**: - ✅ Code style guide 작성 및 승인 - ✅ Pre-commit hook 추가 (자동 스타일 체크) - ✅ 팀 리뷰 시간 30% 단축 (기준 명확) --- ## Part 3: 통합 성과 추적 ### 주간 진행도 추적표 (2026-07-24 ~ 2026-09-30) ``` Week Phase Task Status Owner Target Date ───────────────────────────────────────────────────────────────────────────── 1 P0.V CI performance measurement 🔄 Team 2026-07-31 2 P0.V Reproducibility validation 🔄 Team 2026-08-07 3 P0.V Data consistency audit table ▶ Team 2026-08-14 4 P0.V Deployment e2e test ▶ Team 2026-08-21 5 P0.V Daily data quality check ▶ Team 2026-08-28 6 P0.V Phase 0 validation complete 🔲 Team 2026-08-31 7 P1.D Schema normalization design 🔲 Claude 2026-09-07 8 P1.D 3NF validation tool 🔲 Claude 2026-09-14 9 P1.D Blue-green migration plan 🔲 Claude 2026-09-21 10 P1.P Repository interface design 🔲 Claude 2026-09-28 11 P1.P ADR & style guide 🔲 Team 2026-09-30 ``` ### 리스크 추적 | 리스크 | 영향 | 확률 | 완화 계획 | 담당 | |-------|------|------|---------|------| | CI 성능 개선 못 함 | 높음 | 낮음 | 병렬화 추가 검토 | Team | | 데이터 마이그레이션 실패 | 매우높음 | 중간 | Blue-green test 철저 | Claude | | 팀 역량 부족 | 중간 | 중간 | Phase 우선순위 조정 | Owner | | KIS API 변경 | 중간 | 낮음 | Adapter + fallback 활성 | Team | --- ## 최종 성공 기준 (2026-09-30) ``` ✅ Phase 0 운영 검증 완료 - CI: 실제 15-20분 달성 확인 - 배포: 3회 연속 성공 + 롤백 검증 - 재현성: 3회 연속 CI 같은 결과 ✅ Phase 1 설계 및 준비 완료 - 3NF 스키마: 설계 + 검증 완료 - 마이그레이션 계획: 상세 blue-green 전략 수립 - SOLID 설계: Repository 분리 + DI 설계 완료 - 표준화: ADR 5개 + Style guide 승인 ✅ 팀 준비 완료 - Phase 1 리소스 할당 확정 - 마이그레이션 리스크 공유 및 대응 계획 수립 - CLAUDE.md Phase 1 업데이트 🚀 Phase 1 시작 준비: 2026-10-01 ``` --- **Document Version**: 1.0 **Status**: Ready for Execution **Next Review**: Weekly (every Monday) **Emergency Contact**: Claude Code (@claude)