Runs both repositories locally -- backend on PostgreSQL 16 behind a real Keycloak realm, frontend as a production-profile build -- and records what each checklist section actually did, with the command output behind it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
14 KiB
Tech Log 운영 출시 전 체크리스트 — 실측 검증 보고서
검증일 2026-08-19 · 방식 두 저장소를 로컬에서 실제 기동해 엔드포인트·브라우저 단위로 실측
| 대상 | 위치 | 리비전 |
|---|---|---|
| Frontend | tech-log-frontend |
main eb86708 → fix/release-gate-frontend fff5e6f |
| Backend | tech-log-backend |
develop ab0447a |
| Keycloak | 로컬 컨테이너 local-keycloak |
26.7.0 (:18080) |
| PostgreSQL | 로컬 컨테이너 techlog-pg |
16.15 (:5433) |
요약 판정: 출시 보류 (P0 미충족)
체크리스트 §31의 P0 항목 중 인증 우회 불가 · 인가 우회 불가 · Studio 주요 기능 정상 · Publish 정상이 현재 충족되지 않는다. 아래 근거는 전부 실행 결과다.
가장 중요한 구조적 사실
백엔드는 계약(studio-v1.yaml)이 선언한 18개 오퍼레이션 중 2개만 구현되어 있다.
| 상태 | 오퍼레이션 |
|---|---|
| 구현됨 (2) | getStudioSession, listStudioCatalog |
| 미구현 (16) | getStudioDashboard, listStudioDocuments, createStudioDocument, getStudioDocument, saveStudioDocument, validateStudioDocument, getCurrentStudioPreview, createStudioPreview, publishStudioDocument, listStudioPublications, unpublishStudioPublication, getStudioPublicationSnapshot, listStudioAssets, uploadStudioAsset, getStudioAsset, updateStudioAsset, deleteStudioAsset |
또한 Public 읽기 엔드포인트는 계약에 아예 없다. studio-v1.yaml은 Studio 전용이고,
프론트엔드의 Public 화면(/, /explore, /projects, /releases, 문서 상세)은
src/features/tech-log/adapters/static/public-content.ts의 번들에 컴파일된 정적 콘텐츠를 읽는다.
따라서 체크리스트의 다음 절은 검증 대상 자체가 존재하지 않는다: §2(탐색·검색·프로젝트·변경기록의 백엔드 연동), §3.1~3.3(문서 작성·관계·Publish), §12(Public/Private 데이터 경계), §17(파일/Object Storage), §29(E2E 시나리오).
P0 — 출시 차단 결함
P0-1. Studio 라우트가 인증을 검사하지 않았다 — 수정 완료
TECH_LOG_ROUTE_REGISTRY가 모든 TechLog 라우트를 access: "public"으로 등록하고 있었다.
라우터에 decideRouteAccessForDefinition 가드가 존재하지만 Studio에 대해 무력화된 상태였다.
운영 프로파일 빌드(AUTH_MODE=external)로 실측한 수정 전:
/studio http=200 h1="작업 흐름" ← 비로그인 상태에서 Studio UI 렌더링
/studio/documents http=200 h1="작업본"
/studio/assets http=200 h1="Asset"
spec.layoutGroup === "STUDIO"에서 access를 유도하도록 수정한 뒤:
/studio http=200 h1="로그인 연동이 필요합니다."
/studio/documents http=200 h1="로그인 연동이 필요합니다."
/ , /explore 변화 없음
로그인 후 원래 요청 화면으로 복귀하는 것도 확인했다(/studio/documents → 로그인 → 작업본).
커밋 fff5e6f.
P0-2. 백엔드 Studio API에 인가 검사가 없다 — 미해결
SecurityConfig는 anyRequest().authenticated()로 끝나고, Studio 컨트롤러에
@RequiresPermission 계열 애노테이션이 하나도 없다.
Keycloak에 Studio 권한이 없는 사용자(plain, realm role plain-user)를 만들어 확인:
GET /api/v1/studio/catalog?type=TOPIC
studio 사용자 (studio-author) → HTTP 200
plain 사용자 (권한 없음) → HTTP 200 ← 인가 우회
체크리스트 §11 "인증된 사용자라고 해서 무조건 Studio API를 호출할 수 있지 않다", §31 P0 "인가 우회 불가" 미충족.
P0-3. 모든 Studio 경로가 /api/api/v1/...에 매핑된다 (Double Prefix) — 미해결
PresentationWebConfig가 configurer.addPathPrefix("/api", c -> true)로 전 컨트롤러에
/api를 붙이는데, Studio 컨트롤러는 @GetMapping("/api/v1/studio/...")로 이미 /api를 포함해 선언한다.
GET /api/v1/studio/catalog → 404 ROUTE_NOT_FOUND
GET /api/api/v1/studio/catalog → 200
GET /api/v1/studio/session → 404 ROUTE_NOT_FOUND
GET /api/api/v1/studio/session → 503
프론트엔드는 계약대로 /api/v1/studio/...를 호출하므로 현재 상태로는 단 한 건도 연결되지 않는다.
체크리스트 §25 "/api Prefix 처리에서 Double Prefix가 발생하지 않는다" 미충족.
P0-4. getStudioSession이 항상 503을 반환한다 — 미해결
auth-mode: jwt(저장소 기본값, src/.env:115)에서 SecurityConfig가 csrf.disable()로
CsrfFilter를 제거하므로 CsrfToken 파라미터가 항상 null이고, 컨트롤러는 이를
STUDIO_UNAVAILABLE(503)로 정직하게 보고한다.
GET /api/api/v1/studio/session (유효한 studio 토큰)
→ 503 {"code":"STUDIO_UNAVAILABLE","category":"TRANSIENT_DEPENDENCY","retryable":true}
로그: "CSRF token unavailable: CSRF protection is disabled for the active auth-mode"
프론트엔드 HTTP 모드는 getStudioSession으로 CSRF 토큰을 받아 부트스트랩하므로,
이 한 건 때문에 Studio HTTP 경로 전체가 시작조차 못 한다.
auth-mode: redis-session에 필요한 세션 빈이 저장소에 없다는 점은 백엔드 HANDOFF.md도 명시하고 있다.
P0-5. main 브랜치의 dev 부팅이 깨져 있었다 — 수정 완료
eb86708(계약 3.0.0 머지) 이후 public/release-manifest.json이 2.0.0으로 남아
부팅 시 contract-set 검증이 fail-closed → 빈 화면. 이전에 한 번 겪은 것과 같은 실패 양식이다.
setDigest drift: manifest sha256:e0da7765…, build sha256:261ac630…
package drift: manifest 2.0.0 / ce2e748 vs build 3.0.0 / b20d7a2
generate:dev-release-manifest로 재생성하고, 같은 값을 하드코딩하던
tests/runtime-schema/release-manifest.test.ts도 함께 갱신했다. 커밋 fff5e6f.
P0-6. 커밋된 .env로는 prod 프로파일이 부팅하지 않는다 — 미해결
src/.env:140이 APP_DATASOURCE_DDL_AUTO=update인데, application-prod.yml이 문서화한
JpaSchemaSafetyValidator는 prod에서 none|validate만 허용하고 위반 시 exit 71로 종료한다.
P0-7. ddl-auto=validate로는 PostgreSQL에서 부팅하지 않는다 — 미해결
SchemaManagementException: Schema-validation: missing table [fs_cleanup_item]
PostgreSqlPersistenceConfig가 Flyway 위치를 classpath:db/migration/postgresql로 고정해
db/migration/jpa/fileserver 트리가 한 번도 적용되지 않는데, 해당 JPA 엔티티는 스캔된다.
ddl-auto=update가 이 사실을 가려 온 것이고, prod가 요구하는 validate로 바꾸는 순간 드러난다.
(본 검증은 ddl-auto=none으로 우회해 진행했다.)
P1 — 출시 전 해결 권장
| # | 항목 | 실측 근거 |
|---|---|---|
| P1-1 | Keycloak realm 구성이 두 저장소 어디에도 없다 | compose에 keycloak 서비스 없음, realm export 파일 없음. 검증을 위해 ca-skeleton realm·클라이언트·audience 매퍼·테스트 사용자를 수기로 생성해야 했다. §27 "Keycloak Realm 설정을 복원할 수 있다" 미충족 |
| P1-2 | 프론트엔드에 로그인 구현이 없다 | OIDC/Keycloak 클라이언트 코드 0건. AUTH_MODE=external은 호스팅 페이지가 window.__CA_FRONTEND_AUTH_OWNER__를 주입하기를 기대하며, 없으면 createUnavailableSessionAdapter가 "로그인 연동이 필요합니다"를 띄운다. §1.4 인증 항목 전부 검증 불가 |
| P1-3 | production 런타임 설정이 플레이스홀더 | API_BASE_URL: https://api.example.com/, TELEMETRY_ENDPOINT: https://telemetry.example.com/v1/events |
| P1-4 | Rate Limit 비활성 | APP_RATE_LIMIT_ENABLED=false, APP_RATE_LIMIT_PROVIDER=disabled. 60회 연속 호출 전부 200 |
| P1-5 | 보안 헤더를 적용하는 주체가 없다 | config/hosting/security-headers.json에 CSP·HSTS·X-Frame-Options 등이 정의돼 있으나 dist/server.mjs는 하나도 적용하지 않는다. verify:hosting-headers는 기본적으로 fixture 모드로 동작해 실 서버를 검사하지 않는다 |
| P1-6 | 캐시 정책도 미적용 | cache-policy.json은 /assets/*에 public, max-age=31536000, immutable을 요구하나 실제 응답은 전부 no-cache |
| P1-7 | 프론트엔드 배포 아티팩트 부재 | Dockerfile·nginx conf·compose 없음. dist/server.mjs는 프리뷰용이지 운영 파일 서버가 아니다 |
| P1-8 | robots.txt / sitemap.xml 없음 | Studio 경로가 검색 엔진에 차단되지 않는다. §7 미충족 |
| P1-9 | Open Graph·canonical 메타데이터 없음 | dist/index.html에 og:*·canonical 없음. <title>은 라우트별로 정상 동작하나 런타임에 설정되므로 JS를 실행하지 않는 공유 미리보기 크롤러에는 "Tech Log" 고정값만 노출된다 |
| P1-10 | DB 타임아웃 30초 | APP_DATASOURCE_CONNECTION_TIMEOUT=30000. application.yml이 문서화한 D2 fail-fast 의도(기본 5s)와 어긋난다. 프론트엔드 REQUEST_TIMEOUT_MS=10000이므로 DB 장애 시 프론트가 항상 먼저 끊겨 DB_UNAVAILABLE 503을 보지 못한다 |
| P1-11 | Tech Log Asset의 Object Storage 배선 없음 | objectstorage 어댑터는 템플릿 자산으로 존재하나 techlog 참조 0건, MinIO/S3 환경변수 0건, uploadStudioAsset 엔드포인트 미구현 |
검증되어 통과한 항목
Frontend
| 항목 | 결과 |
|---|---|
| Production Build | PASS (local·production 프로파일 모두) |
| TypeScript compile | PASS (check:types 6개 프로젝트) |
| ESLint | PASS (수정 후 0 error) |
| 전체 테스트 | 1,818 passed / 16 skipped / 1 기존 flake (provider-guardian-transaction — 단독 실행 2회 모두 PASS, 부하 의존) |
| architecture / contract / dev-release-manifest / browser-security 게이트 | PASS |
| Production 번들에 dev·localhost URL 없음 | PASS (localhost·127.0.0.1 0건, .local 매치는 전부 locale/localeCompare) |
| Production 번들에 Mock API 미포함 | PASS (createMockStudioGateway 0건) |
| Source Map 비공개 | PASS (.map 0개) |
| Route 단위 Lazy Loading | PASS (30 청크, 총 954 KB / 최대 569 KB) |
| SPA 라우팅·새로고침 | PASS (열거형 allowlist 방식. 존재하지 않는 문서 경로는 의도적으로 404) |
Route별 <title> |
PASS (탐색 · Tech Log, 프로젝트 · Tech Log …) |
| 반응형 | PASS — 360/414/768/1440 × 6개 Public 라우트 24개 조합 전부 가로 스크롤 없음 |
| 접근성 | PASS — axe(wcag2a/2aa/21a/21aa) serious+critical 0건 (Public 6 + Studio 4 라우트). h1 정확히 1개, heading 건너뜀 없음, alt 누락 0, 레이블 없는 icon button 0 |
| 로그인 흐름 | PASS (게이트 → 로그인 → 원래 화면 복귀) |
Backend
| 항목 | 결과 |
|---|---|
| Production Profile Build | PASS — :app-bootstrap:bootJar 성공 |
| 전체 테스트 | PASS — 3,530 tests / 0 failures / 7 skipped (BUILD SUCCESSFUL 8m 9s). app-bootstrap 797 · application-core 568 · cache-redis 423 · fileserver 398 · inbound-web 341 · httpclient 283 · shared-contract 224 · objectstorage 140 · persistence-jpa 122 · 그 외 |
| Docker Image Build | PASS — 623MB. BUILD_VERSION/GIT_SHA/SOURCE_URL build-arg를 강제하는 provenance 게이트가 있어 인자 없이는 의도적으로 실패한다 |
| Production Image 실제 실행 | PASS — 컨테이너에서 14.5초 기동, healthcheck 200 · readiness 200 · catalog 200(실데이터 2건) |
| Flyway 마이그레이션 (신규 DB, 처음부터) | PASS — 6개 적용, V7 techlog core 포함, 테이블 33개 생성 |
| 응답 봉투 일관성 | PASS — {success,data,error,meta} 전 경로 동일 |
| HTTP 상태 코드 | PASS — 401 / 404 / 405 / 422 / 500 / 503 모두 적절 |
| 인증 오류 코드 분리 | PASS — AUTH_TOKEN_MISSING / AUTH_TOKEN_MALFORMED / AUTH_TOKEN_INVALID_SIGNATURE / AUTH_TOKEN_EXPIRED |
| Validation | PASS — 잘못된 enum·필수 누락은 422 + fieldErrors, limit 상·하한 강제 |
| SQL Injection | PASS — ' OR 1=1-- 파라미터 바인딩되어 빈 결과 |
| Visibility 필터 | PASS — ARCHIVED 토픽이 catalog 결과에서 제외됨 |
| CORS | PASS — 허용 origin 200 + Allow-Credentials: true, 미허용 origin 403, 와일드카드 없음 |
| 보안 헤더 | PASS — X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Cache-Control: no-store |
| 오류 정보 노출 | PASS — Stack trace·SQL·내부 클래스명 모두 미노출 (details: null) |
| 로그 위생 | PASS — 토큰·Authorization·쿠키·비밀번호 0건. user=는 가명화 해시 |
| 추적성 | PASS — 모든 요청 로그에 req=·trace=, http_request method= uri_template= status= duration_ms= |
| Metrics | PASS — Prometheus 127개 메트릭 패밀리 (http_server_requests_seconds_bucket, jvm_gc_*, jvm_memory_*, hikaricp_connections_*) |
| Liveness / Readiness 분리 | PASS — DB 중단 시 readiness 503 DOWN, liveness 200 UP 유지 |
| 의존성 장애 대응 | PASS(동작) — DB 중단 시 무한 대기 없이 503 DB_UNAVAILABLE (retryable) 반환, DB 복구 후 27ms/정상 데이터로 자동 회복. 단 응답까지 30초 소요(P1-10) |
다음 단계 (권장 순서)
- Double Prefix 해소 (P0-3) — 컨트롤러 매핑에서
/api를 제거하거나addPathPrefix대상에서 제외. 이걸 고치기 전에는 프론트-백엔드가 한 건도 연결되지 않으므로 최우선. - 세션 인프라 (P0-4) —
redis-session배선. 백엔드 HANDOFF.md도 Plan 02보다 앞선 선행 작업으로 지목하고 있다. - Studio 인가 (P0-2) — Studio 컨트롤러에 권한 검사 추가 + 권한 없는 사용자 403 회귀 테스트.
- prod 부팅 설정 (P0-6, P0-7) —
.env의ddl-auto, fileserver 마이그레이션 위치. - 나머지 16개 오퍼레이션 — 백엔드 HANDOFF.md가 지적한 생성 union 5종의 Jackson 파손 전략 결정이 선행.
- 배포 레이어 (P1-5·6·7) — nginx/CDN에 보안 헤더·캐시 정책 적용, 프론트엔드 이미지.
- robots.txt로 Studio 차단 (P1-8).