docs: add Phase 0 closeout & Phase 1 kickoff execution plan
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

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>
This commit is contained in:
2026-07-24 13:49:06 +09:00
parent 852848e69b
commit 1c48c45a45
@@ -0,0 +1,968 @@
# 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<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 → 작은 책임의 인터페이스로 분리
**구체적 작업**:
```csharp
// 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)
**목표**: 고수준 모듈이 저수준 모듈에 의존하지 않기
**구체적 작업**:
```csharp
// 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) 작성
**목표**: 왜 이런 선택을 했는가? 의사결정 기록
**구체적 작업**:
```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<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
```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)