Architecture Audit — Nougat DevPay
기준 Commit: aa884e73 (feat/pg-magician HEAD). 모든 항목은 실제 코드·테스트 재검토 기준. 상태 구분: P0 = 정합성·보안·실행 차단급, P1 = 베타 품질 필수, P2 = 운영 개선.
1. 실행/배포 (Phase 1)
| # | 우선순위 | 문제 | 증거 | 해결 조건 |
|---|---|---|---|---|
| A1 | P0 | docker-compose.yml은 BIND_ADDR=0.0.0.0:8080 + NOUGAT_AUTH_MODE=dev — BE가 dev-auth 외부 바인드를 거부해 컨테이너가 즉시 panic | infrastructure/docker-compose.yml, be/src/main.rs:24 | prod/dev compose 분리; dev는 별도 override에서만 |
| A2 | P0 | NUXT_PUBLIC_API_BASE=http://be:8080는 브라우저에서 해석 불가 (Docker 내부 DNS) | infrastructure/docker-compose.yml:24, fe/Dockerfile | same-origin /api/v1 프록시 (Nitro route) 도입 |
| A3 | P1 | Docker 데몬 미기동 — 이미지 빌드·기동 미검증 | 로컬 docker info 실패 | CI docker smoke job + 로컬 재검증 |
| A4 | P1 | graceful shutdown 없음 — axum::serve에 shutdown signal 미연결 | be/src/main.rs:39 | SIGTERM 핸들러 + 종료 시 outbox flush |
| A5 | P2 | 컨테이너 재시작/헬스/시크릿 주입 절차 문서 없음 | — | 배포 문서 |
2. FE 상태 관리 (Phase 2·3)
| # | 우선순위 | 문제 | 증거 | 해결 조건 |
|---|---|---|---|---|
| B1 | P0 | pullAll이 상위 리소스를 store에 먼저 쓰고 하위 리소스(items/players/fulfillments)를 나중에 조회 — 후반 실패 시 부분 갱신 | fe/lib/api/sync.ts:20-38 | 임시 스냅샷 → 전수 성공 시 단일 커밋 |
| B2 | P0 | persist.client.ts가 api 모드에서도 LocalStorage에 비즈니스 데이터 복원·기록 — 서버 권위 위반, 시드 오염 가능 | fe/plugins/persist.client.ts | api 모드 비즈니스 데이터 영속화 금지 |
| B3 | P1 | 동시 pullAll 경쟁 없음 보장 없음 — mutation마다 pullAll 호출, 응답 순서 역전 가능 | fe/stores/db.ts remote() | request generation id + 최신만 커밋 |
| B4 | P1 | API 모드에서 로컬만 변경하는 기능 다수: 구독 renew/fail/expire/cancel, 라이선스 발급·상태·만료·activation·verify, 정산 실행, settings 저장, 상품 삭제 | fe/stores/db.ts (apiMode 분기 없음) | BE 엔드포인트 + remote() 연결 |
| B5 | P1 | checkout에서 구독 상품 결제 시 BE가 subscription을 생성하지 않음 — 구독 목록이 영구히 비어있음 | be/src/handlers.rs:369-395 | subscription 상품 → subscription+license 생성 |
| B6 | P1 | pendingPaymentId가 위젯·금액·작성자 문맥과 무관하게 재사용됨 — 위젯 변경 후 이전 결제 ID 제출 시 402만 발생 | fe/pages/feedback/widgets.vue:96 | 결제 컨텍스트 바인딩 + 무효 시 초기화 |
| B7 | P2 | create_feedback_payment에 멱등키 없음 — 네트워크 단절 후 재시도 시 중복 결제 생성 | be/src/handlers.rs:591 | idempotency_key + GET /payments/:id 상태 조회 |
| B8 | P2 | push()의 .catch(() => {}) — mock 모드 best-effort이지만 침묵 실패 | fe/stores/db.ts:344 | 근거 주석 명시 (현 상태 기록) |
| B9 | P2 | mock 모드에서도 서버 push 시도 — apiMode 분기 이후라도 backendOnline이면 전송 | fe/stores/db.ts | mock 모드는 로컬 전용 문서화 |
3. DB/트랜잭션 (Phase 4·5)
| # | 우선순위 | 문제 | 증거 | 해결 조건 |
|---|---|---|---|---|
| C1 | P1 | 마이그레이션 러너가 마커 존재만 확인 — 적용된 SQL 변경 여부 식별 불가, schema_version에 checksum 없음 | be/src/db.rs:84 | checksum 컬럼 + 드리프트 감지 |
| C2 | P1 | Store::tx가 FnMut(&Repo) — 오류 반환 불가, panic만이 롤백 수단. 핸들러는 여전히 Arc<Mutex<Connection>> 직접 사용 | be/src/store.rs:15 | 오류 반환 계약 + 핸들러 점진 이전 |
| C3 | P1 | update_seller_field(col)이 format!으로 컬럼명 삽입 — 현재 호출부는 고정 목록이지만 포트 계약상 위험 패턴 | repository.rs:333 | 허용 컬럼 allowlist를 repo 내부로 |
| C4 | P2 | get_seller/get_verification은 LIMIT 1/id=1 — 단일 판매자 고정 | repository.rs | seller_id 스코핑(Phase 12와 연동) |
| C5 | P1 | admin_backup VACUUM INTO 결과 검증 없음(파일 크기·테이블 카운트), 복구 절차·테스트 없음 | handlers.rs:747 | 백업 검증 + 복원 테스트 |
| C6 | P2 | now()는 초 단위 — 감사 순서가 동일 초 내 불명확 | db.rs:7 | rowid/monotonic 보조 정렬(이미 일부 사용) |
| C7 | P1 | Tokio 런타임에서 동기 rusqlite 호출이 워커 스레드를 장시간 점유 — spawn_blocking 미사용 | handlers.rs lock() | blocking pool 분리 검토·문서화 |
| C8 | P2 | PostgreSQL 어댑터 미연결 — pg_store.rs(771줄)·PgStore/PgRepo 구현은 있으나 main.rs가 비-sqlite 시 panic, build_state가 SqliteStore 하드코딩, pg_schema.sql은 users/login_attempts(010/011)·display_name/token_version(011)·integrations/donations(012)까지 커버 — 실제 누락은 counters+outbox.delivered_at/lease_owner(009)와 seller_memberships(011)뿐 | main.rs:8-12, lib.rs:121-124, pg_store.rs | build_state 드라이버 분기 + pg_schema 누락분(counters·outbox lease·seller_memberships) 보강 + 전환 절차 검증 |
4. Codec/Protobuf (Phase 6)
| # | 우선순위 | 문제 | 증거 | 해결 조건 |
|---|---|---|---|---|
| D1 | P1 | Feedback.payment_id=12, AuditEvent.actor_role=8, PaymentList), 와이어 타입은 be/build.rs가 proto에서 자동 생성해 구조적 drift 불가 | nougat.proto, be/build.rs | (해소 — CI buf lint/breaking 유지) |
| D2 | P1 | be/build.rs(protox+prost-build)가 contracts/proto에서 자동 생성(models.rs re-export) — 해소됨. 잔여: FE ES codegen 미연결 (contracts/buf.gen.yaml은 dead config, buf generate 미실행) | be/build.rs, contracts/buf.gen.yaml | FE codegen 연결 또는 buf.gen.yaml 정리(현재 주석으로 표기) |
| D3 | P2 | FE longs: Number — int64 > 2^53 정밀도 손실 가능 (현재 KRW 금액·ID 문자열만 사용해 실해당 없음) | fe/lib/api/proto.ts:22 | 금액 필드 문자열/소수점 규약 문서화 |
| D4 | P2 | codec matrix 대상 API가 일부 엔드포인트만 | codec-matrix.spec.ts | checkout/feedback/fulfillment/error 확대 |
5. Origin 004 (Phase 7)
| # | 우선순위 | 문제 | 증거 | 해결 조건 |
|---|---|---|---|---|
| E1 | P2 | useOverlayShell — [data-overlay-shell] 첫 요소만 탐색해 중첩 오버레이 시 상위만 trap | useOverlayNav.ts:139 | topmost 셸 기준으로 스코프 |
| E2 | P2 | ephemeral 소비 목록 consumed가 영구 누적 (메모리 누수, 페이지 수명 내 한정) | useOverlayNav.ts:59 | bounded set |
| E3 | P2 | 외부 위젯 iframe/postMessage 격리·origin 검증 시연 없음 | — | embed 페이지 + origin 검증 코드 |
| E4 | P2 | 중첩 오버레이 E2E 없음 | e2e | Playwright 테스트 |
6. Audit/Ledger/Outbox (Phase 8·9·10)
| # | 우선순위 | 문제 | 증거 | 해결 조건 |
|---|---|---|---|---|
| F1 | P0 | FNV-1a 64bit 해시 체인 — 비암호학적, 충돌 임의 생성 가능 | db.rs:113 | SHA-256(+HMAC 옵션) 교체 |
| F2 | P1 | 감사 detail에 비즈니스 객체 전체 JSON — PII·계정 정보(payout_account) 포함 | handlers.rs 곳곳 serde_json::to_string(&p) | 허용 메타데이터 화이트리스트 |
| F3 | P1 | 해시 체인 검증 함수 없음 — 변조 감지 불가 | db.rs | verify_audit_chain + 테스트 |
| F4 | P1 | 원장이 이벤트 로그 수준 — direction/asset/quantity/ref/idempotency 없음, 금전·재화 구분이 meta 문자열 의존 | 006_ledger.sql, db.rs:131 | 구조화 컬럼 + 합계 정합성 테스트 |
| F5 | P1 | outbox 디스패처가 consumer 호출 동안 DB 전역 lock 보유 — 모든 요청 블로킹; 백오프는 "주기" 주석뿐 실제 지연 없음 | outbox.rs:82 | claim/lease + next_attempt_at + lock 외 호출 |
| F6 | P1 | outbox 동일 이벤트 중복 처리·소유권 관리 없음 (pending→completed 경쟁) | outbox.rs | claimed 상태 + lease 만료 회수 |
| F7 | P2 | Exactly-once 미보장인데 문서상 명시 부족 | — | at-least-once 명시 + consumer 멱등 계약 |
7. 보안 (Phase 11·12)
| # | 우선순위 | 문제 | 증거 | 해결 조건 |
|---|---|---|---|---|
| G1 | P1 | metrics label에 원시 path — /orders/<id> 등 cardinality 폭증 | metrics.rs:58 | MatchedPath 라우트 패턴 |
| G2 | P1 | 운영 인증이 X-Admin-Key 하나뿐 — seller/buyer는 prod에서 접근 불가 → 사실상 prod 비가동 | auth.rs:85 | JWT Bearer 검증 제공자 (표준 라이브러리) |
| G3 | P1 | 단일 판매자 가정 — projects/products에 소유자 스코핑 없음, 누구든 타인 리소스 접근 가능 | repository.rs 전역 | seller_id 소유권 + 서버측 필터 |
| G4 | P2 | 시드 seller_settings에 평문 API key·webhook secret — dev 시드와 운영 secret 경계 흐림 | db.rs:161 | 시드 마킹 + 설정 API에서 secret 마스킹 |
| G5 | P2 | rate limit이 IP당 분단위 윈도우만 — 결제 등 민감 경로 별도 한도 없음 | ratelimit.rs | 경로별 한도(선택) |
| G6 | P2 | IDOR 테스트 없음 | tests | 소유권·역할 상승 테스트 |
8. 테스트 거짓성 검토
| # | 우선순위 | 문제 | 증거 |
|---|---|---|---|
| H1 | P2 | page.goBack 이후 오버레이 detach를 .catch(()=>{})로 삼킴 | server-authoritative.spec.ts 마지막 줄 |
| H2 | P2 | "라이선스·구독 — 서버 데이터 로드"는 페이지 렌더+API 200만 확인 — 실제 데이터 바인딩 미검증 | server-authoritative.spec.ts |
9. 로컬 툴체인 참고
- Windows GNU 타겟에서
dlltool부재로 cargo 빌드 실패 →stable-x86_64-pc-windows-msvc사용으로 해결 (이번 세션 검증 기준). - buf/protoc 미설치 → prost-build + vendored protoc 또는 CI buf action 경로 검토.
- Docker Desktop 데몬 정지 → compose 변경은 코드상 수정 + CI smoke job으로 보완.