Files
tech-log-frontend/docs/release/2026-08-19-release-gate-verification.md
T
DongHyeonkaandClaude Opus 5 44caa477e3 docs: record the release-gate verification run against a live backend
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>
2026-08-19 16:50:42 +09:00

14 KiB
Raw Blame History

Tech Log 운영 출시 전 체크리스트 — 실측 검증 보고서

검증일 2026-08-19 · 방식 두 저장소를 로컬에서 실제 기동해 엔드포인트·브라우저 단위로 실측

대상 위치 리비전
Frontend tech-log-frontend main eb86708fix/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에 인가 검사가 없다 — 미해결

SecurityConfiganyRequest().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) — 미해결

PresentationWebConfigconfigurer.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)에서 SecurityConfigcsrf.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:140APP_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.htmlog:*·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)

다음 단계 (권장 순서)

  1. Double Prefix 해소 (P0-3) — 컨트롤러 매핑에서 /api를 제거하거나 addPathPrefix 대상에서 제외. 이걸 고치기 전에는 프론트-백엔드가 한 건도 연결되지 않으므로 최우선.
  2. 세션 인프라 (P0-4) — redis-session 배선. 백엔드 HANDOFF.md도 Plan 02보다 앞선 선행 작업으로 지목하고 있다.
  3. Studio 인가 (P0-2) — Studio 컨트롤러에 권한 검사 추가 + 권한 없는 사용자 403 회귀 테스트.
  4. prod 부팅 설정 (P0-6, P0-7) — .envddl-auto, fileserver 마이그레이션 위치.
  5. 나머지 16개 오퍼레이션 — 백엔드 HANDOFF.md가 지적한 생성 union 5종의 Jackson 파손 전략 결정이 선행.
  6. 배포 레이어 (P1-5·6·7) — nginx/CDN에 보안 헤더·캐시 정책 적용, 프론트엔드 이미지.
  7. robots.txt로 Studio 차단 (P1-8).