docs: archive superseded roadmap/WBS docs, fix broken install command, retire stale files
Validators (Pushes and Pull Requests) / UI & Storage Validation (push) Failing after 18s
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (push) Failing after 26s
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) / Database & Schema Validation (push) Successful in 12s
Validators (Pushes and Pull Requests) / Security & Secrets (push) Successful in 12s
Validators (Pushes and Pull Requests) / CI Workflow Lint (push) Failing after 9s
Validators (Pushes and Pull Requests) / Notify PR Results (push) Has been skipped
Frontend CI Pipeline / ci-frontend-8-steps (push) Failing after 1m56s
Validators (Pushes and Pull Requests) / UI & Storage Validation (push) Failing after 18s
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (push) Failing after 26s
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) / Database & Schema Validation (push) Successful in 12s
Validators (Pushes and Pull Requests) / Security & Secrets (push) Successful in 12s
Validators (Pushes and Pull Requests) / CI Workflow Lint (push) Failing after 9s
Validators (Pushes and Pull Requests) / Notify PR Results (push) Has been skipped
Frontend CI Pipeline / ci-frontend-8-steps (push) Failing after 1m56s
Archives 6 more superseded planning docs to docs/archive/ (ROADMAP_WBS.md, MODERNIZATION_ROADMAP_VISUAL.md, MODERNIZATION_STRATEGY_ROADMAP_2026-2027.md, ROADMAP_ENTERPRISE_TEMPLATES_WBS.md, EXECUTION_PLAN_PHASE0_CLOSEOUT_AND_PHASE1_KICKOFF.md, CICD_ROADMAP.md), each replaced by a current source of truth (CLAUDE.md, docs/MIGRATION_STATUS.md, or the OMS·WMS·ERP spec/playbook), and repoints 4 files that linked to the pre-archive path. Fixes README's top-of-file install instructions and package.json's "ops:dev" script, both of which pointed at core_satellite_collector.js - a file that has never existed anywhere in this repo's git history. The real, working entry point (tools/run_kis_data_collection_v1.py / npm run ops:data-collect) was already correctly documented further down the same README. Also finishes retiring pre-existing stale state that predates this session: removes the superseded deploy-prod.yml.backup, completes the already-in-progress removal of the old src/client/ Vue+AG-Grid prototype (superseded by src/frontend/), and untracks test-results/.last-run.json (Playwright's own run-metadata file, regenerated every test run - shouldn't be version controlled). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user