Files
QuantEngineByItz/docs/EXECUTION_PLAN_PHASE0_CLOSEOUT_AND_PHASE1_KICKOFF.md
kjh2064 1c48c45a45
Validators (Pushes and Pull Requests) / Database & Schema Validation (push) Failing after 6s
Validators (Pushes and Pull Requests) / UI & Storage Validation (push) Failing after 11s
Validators (Pushes and Pull Requests) / WBS & Audit Validations (push) Has been skipped
Validators (Pushes and Pull Requests) / .NET Contracts (push) Has been skipped
Validators (Pushes and Pull Requests) / Calibration & Performance (push) Has been skipped
Validators (Pushes and Pull Requests) / Operational Report & Decision Packet (push) Has been skipped
Validators (Pushes and Pull Requests) / Notify PR Results (push) Has been skipped
Validators (Pushes and Pull Requests) / Security & Secrets (push) Failing after 6s
Validators (Pushes and Pull Requests) / CI Workflow Lint (push) Failing after 5s
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (push) Failing after 20s
docs: add Phase 0 closeout & Phase 1 kickoff execution plan
Strategic execution roadmap for 2026-07-24 ~ 2026-09-30 (9 weeks):

PART 1: Phase 0 Validation (Jul 24 - Aug 31, 4 weeks)
- Task 1.1.1: CI performance baseline (expect 15-20min actual)
- Task 1.1.2: CI reproducibility validation (3x same commit → same result)
- Task 1.2.1: PostgreSQL audit trail tables (kis_*_audit)
- Task 1.2.2: Daily data consistency validation (completeness, freshness, consistency, outliers)
- Task 1.3.1: Deployment e2e testing (prepare-release + deploy-prod scenarios)

PART 2: Phase 1 Preparation (Sep 1-30, 5 weeks)
- Task 2.1.1: 3NF schema design & validation (stocks, quotes, order_book, fundamentals)
- Task 2.1.2: Blue-green migration strategy (5-phase parallel run, zero downtime)
- Task 2.2.1: Repository ISP refactoring (IQuoteRepository, IRunRepository, IErrorRepository)
- Task 2.2.2: Dependency inversion implementation (DI container, Strategy pattern)
- Task 2.3.1: Architecture Decision Records (5+ ADRs: normalization, DI, audit, fallback)
- Task 2.3.2: Code style guide (C#, Python, SQL, naming conventions)

PART 3: Integrated Progress Tracking
- Weekly tracking table (11-week timeline)
- Risk matrix & mitigation plans
- Success criteria for Phase 0 & 1

Key Principles Applied:
✓ SOLID (Single Responsibility, Interface Segregation, Dependency Inversion)
✓ YAGNI (No over-engineering, necessities only)
✓ Data consistency (100% audit trail, reproducibility)
✓ Blue-green deployment (zero downtime, easy rollback)
✓ Pattern standardization (Repository, Strategy, Adapter, Factory)
✓ Code quality (tests, coverage, technical debt reduction)

Success criteria by 2026-09-30:
- Phase 0 validation: CI 15-20min confirmed, 3x reproducibility pass
- Phase 1 ready: Schema designed, migration tested, SOLID refactor designed

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-07-24 13:49:06 +09:00

30 KiB

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분이 실제 달성되는지 확인

구체적 작업:

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 실행 결과가 항상 동일한지 확인

구체적 작업:

# 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) 기초 마련

구체적 작업:

-- 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):

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: 데이터 정합성 검증 자동화

목표: 매일 자동으로 데이터 품질 점검

구체적 작업:

# 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 게이트로 추가:

# .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: 배포 프로세스 엔드-투-엔드 테스트

목표: 실제 배포까지 자동화 검증

구체적 작업:

# 시나리오 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 스크립트 준비
   - 테스트 환경에서 실행

체크리스트 작성:

# 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로 재설계

구체적 작업:

-- 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)
);

정규화 검증:

# 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)

목표: 무중단 데이터 마이그레이션 계획

구체적 작업:

# 마이그레이션 전략: 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);  // 신규
  1. 두 테이블 데이터 정합성 비교
    • SELECT COUNT(*) 일치 확인
    • Aggregates (SUM, AVG) 일치 확인
  2. 한 주일 운영: 모든 쿼리가 일관된 결과 반환하는지 확인

Phase 3: Read Cutover (1주)

  1. 읽기(SELECT) 쿼리를 새 테이블에서 수행 시작
    // 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<SnapshotDto> 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 → 작은 책임의 인터페이스로 분리

구체적 작업:

// BEFORE (ISP 위반)
public interface ICollectionRepository
{
    Task<SnapshotDto> GetSnapshotAsync(string ticker);
    Task<RunDto> GetRunAsync(Guid runId);
    Task<ErrorDto> GetErrorAsync(Guid errorId);
    Task SaveSnapshotAsync(SnapshotDto snapshot);
    Task SaveRunAsync(RunDto run);
    Task DeleteErrorAsync(Guid errorId);
}

// AFTER (ISP 준수)
public interface IQuoteRepository
{
    Task<QuoteDto> GetLatestAsync(string ticker);
    Task<IEnumerable<QuoteDto>> GetHistoryAsync(string ticker, DateRange range);
    Task SaveAsync(QuoteDto quote);
}

public interface ICollectionRunRepository
{
    Task<RunDto> GetAsync(Guid runId);
    Task<IEnumerable<RunDto>> GetRecentAsync(int limit);
    Task SaveAsync(RunDto run);
}

public interface ICollectionErrorRepository
{
    Task<ErrorDto> GetAsync(Guid errorId);
    Task<IEnumerable<ErrorDto>> GetByRunAsync(Guid runId);
    Task SaveAsync(ErrorDto error);
}

public interface IStockRepository
{
    Task<StockDto> GetByTickerAsync(string ticker);
    Task<IEnumerable<StockDto>> 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)

목표: 고수준 모듈이 저수준 모듈에 의존하지 않기

구체적 작업:

// Program.cs (DI 설정)
services
    // Repository abstraction
    .AddScoped<IQuoteRepository>(sp => 
        new AuditedQuoteRepository(
            new QuoteRepository(sp.GetRequiredService<DbContext>()),
            sp.GetRequiredService<IAuditLogger>()))
    
    // Data source abstraction (Strategy pattern)
    .AddScoped<IDataSourceFactory>(sp =>
        new DataSourceFactory(
            sp.GetRequiredService<IKisApiClient>(),
            sp.GetRequiredService<INaverFinanceClient>(),
            sp.GetRequiredService<IYahooFinanceClient>()))
    
    // Fallback chain
    .AddScoped<IQuotationService>(sp =>
        new FallbackQuotationService(
            new KisQuotationService(sp.GetRequiredService<IKisApiClient>()),
            new NaverQuotationService(sp.GetRequiredService<INaverFinanceClient>()),
            new YahooQuotationService(sp.GetRequiredService<IYahooFinanceClient>())))
    
    // Validation
    .AddScoped<IDataQualityValidator>(sp =>
        new DataQualityValidator(sp.GetRequiredService<DbContext>()))
    
    .AddScoped<CollectionService>();

// 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) 작성

목표: 왜 이런 선택을 했는가? 의사결정 기록

구체적 작업:

# 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 작성

목표: "이 프로젝트에서는 이렇게 코딩한다"

구체적 작업:

# CODING_STANDARDS.md

## C# Guidelines

### Repository Pattern
```csharp
// DO
public interface IQuoteRepository
{
    Task<QuoteDto> GetByTickerAsync(string ticker);
    Task SaveAsync(QuoteDto quote);
}

// DON'T
public interface IRepository
{
    T Get<T>(object id);
    void Save<T>(T entity);
}

Error Handling

// 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

// DON'T: 무엇을 하는지 설명 (코드가 이미 말함)
// 가격을 저장한다
await _repository.SaveAsync(quote);

// DO: 왜 이렇게 하는지 설명
// KIS API는 대체로 가격을 30분 지연해서 보고하므로,
// 최신 3시간 데이터만 보관하여 조회 성능 향상
const int RETENTION_HOURS = 3;

Python Guidelines

Data Validation

# 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

# DO: seed 고정 (재현성)
np.random.seed(42)
test_data = np.random.normal(100, 15, 1000)

# DON'T: 시간에 따른 변화
test_timestamp = datetime.now()  # ❌ 매번 다름

SQL Guidelines

-- 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)