Files
sodalive-backend-spring-boot/docs/20261110_자막Phase1연동/deployment-gates.md
T

142 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 자막 Phase 1 배포 게이트
**현재 판정: 배포 BLOCKED.** 이 문서는 로컬 저장 계약과 배포 전 확인 항목이며 DB 적용·AWS 배포 증거가 아니다.
기준일은 2026-11-10이며 [PRD](prd.md), [구현 계획](plan-task.md), [DDL](subtitle-schema.sql)을 함께 본다.
## 1. 검토 기준과 파일 경계
- persistence 기준 경로: `src/main/kotlin/kr/co/vividnext/sodalive/v2/subtitle/adapter/persistence/`.
- 실제 읽은 파일: `SubtitleJob.kt`, `Subtitle.kt`, `SubtitleCallbackReceipt.kt` 및 세 repository.
- 콘텐츠 기준: `src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContent.kt`, `common/BaseEntity.kt`.
- `src/main/resources/application.yml`의 `MySQL8Dialect`·`ddl-auto: validate`는 로컬 설정 근거일 뿐 운영 DB 증거가 아니다.
- 문서 담당은 이 문서와 DDL만 작성한다. 병렬 Kotlin 구현의 최종 엔티티 대조는 상위 구현 게이트에서 다시 수행한다.
## 2. 저장 계약 대조
물리명은 기존 Spring snake_case 매핑 기준이다. 길이 미지정 `String`은 JPA 기본 `VARCHAR(255)`를 따른다.
`jobId`는 UUID **문자열**이며 `BINARY(16)`으로 바꾸지 않는다. `N`은 NOT NULL, `Y`는 NULL 허용이다.
| `SubtitleJob.kt` 속성 | DDL 컬럼·타입 | NULL |
|---|---|---|
| `jobId` | `job_id VARCHAR(36)` PK | N |
| `contentId` | `content_id BIGINT` signed | N |
| `sourceBucket`, `sourceKey` | `source_bucket`, `source_key VARCHAR(255)`; 전체 두 컬럼 unique | N |
| `outputKey` | `output_key VARCHAR(255)` | N |
| `status` | `status VARCHAR(32)`; enum STRING | N |
| `errorCode` | `error_code VARCHAR(255)` | Y |
| `errorMessage` | `error_message TEXT` | Y |
| `sourceUploadErrorCode` | `source_upload_error_code VARCHAR(255)` | Y |
| `sourceUploadErrorAt` | `source_upload_error_at TIMESTAMP`; 명시적 JPA TIMESTAMP | Y |
| `createdAt`, `updatedAt` | `created_at`, `updated_at TIMESTAMP` | N |
| `Subtitle.kt` 속성 | DDL 컬럼·타입 | NULL |
|---|---|---|
| `id` | `id BIGINT` signed PK AUTO_INCREMENT | N |
| `jobId` | `job_id VARCHAR(36)` FK → `subtitle_job.job_id` | N |
| `language` | `language VARCHAR(35)`; `(job_id, language)` unique | N |
| `format` | `format VARCHAR(16)` | N |
| `s3Bucket`, `s3Key` | `s3_bucket`, `s3_key VARCHAR(255)` | N |
| 기존 엔티티 속성 | DDL 컬럼·타입 | NULL |
|---|---|---|
| `SubtitleCallbackReceipt.eventId` | `event_id VARCHAR(255)` PK; UUID 제한 없음 | N |
| `SubtitleCallbackReceipt.jobId` | `job_id VARCHAR(36)` | N |
| `SubtitleCallbackReceipt.bodyHash` | `body_hash VARCHAR(64)` | N |
| `SubtitleCallbackReceipt.createdAt` | `created_at TIMESTAMP` | N |
| `AudioContent.latestSubtitleJobId` | `content.latest_subtitle_job_id VARCHAR(36)` | Y |
### 미확정 차이와 적용 전 대조
1. 마지막 정적 대조에서 `Subtitle.kt`의 테이블명·6개 컬럼·길이·nullable·IDENTITY PK·unique가 합의 계약과 일치했다.
`SubtitleJob.kt`에서도 `koKey`/`jaKey` 제거를 확인했다. `Subtitle.jobId`는 scalar이고 JPA 관계/FK 선언은 없다.
DDL의 `subtitle.job_id` FK는 합의 계약에 따른 DB 제약이며 ORM 관계 선언 확인으로 보고하지 않는다.
`subtitle_job` CREATE에 `ko_key`/`ja_key`는 넣지 않는다. 미출시 초안 교체이며 기존 데이터 이관·컬럼 삭제 SQL은 없다.
2. 최초 대조에서는 `sourceUploadErrorAt`에 `columnDefinition`이 없어 차이로 기록했다.
최종 구현은 `@Column(columnDefinition = "TIMESTAMP")`와 초 절삭을 명시했고 부모가 DDL과 다시 대조했다.
이 로컬 mapping 차이는 해소됐지만 실제 MySQL schema 적용·왕복 검증은 여전히 운영 게이트다.
3. 나머지 시각은 엔티티의 명시적 `TIMESTAMP`와 초 절삭을 따른다. DDL 소수초 정밀도는 모두 0이다.
`created_at`은 `DEFAULT CURRENT_TIMESTAMP`, job의 `updated_at`은 같은 기본값과 `ON UPDATE CURRENT_TIMESTAMP`다.
`Subtitle.kt`에는 시각이나 `BaseEntity` 상속이 없으므로 결과 행에 임의 시각 컬럼을 추가하지 않는다.
UTC 생성값과 JDBC/session timezone 및 실제 MySQL 왕복은 미검증이다.
4. `AudioContent`는 `content` 테이블이며 `BaseEntity.id: Long?`의 IDENTITY를 상속한다.
신규 `content_id`는 signed BIGINT로 맞췄지만 운영 `content.id`의 실제 타입·부호·엔진은 확인하지 않았다.
합의된 `subtitle.job_id` FK만 둔다. receipt의 jobId 및 콘텐츠 연결은 현재 scalar 매핑이므로 FK를 임의 추가하지 않는다.
FK cascade는 없으며 기본 RESTRICT다. `(job_id, language)` unique가 결과 FK와 작업별 조회 인덱스를 겸한다.
5. 현재 callback 활성화는 `ko-KR`/`ja-JP`, 형식은 `SRT`, 결과 2/1/0개의 상태 판정은 그대로다.
저장 구조만 언어별 행으로 바꾼다. 새 언어 활성화·재생성·새 관리 API·recovery 상태/테이블은 추가하지 않는다.
### 식별자와 인덱스 전제
- 세 신규 테이블과 최신 job 컬럼은 `utf8mb4_0900_bin`을 쓴다. 대소문자·후행 공백을 구별하는 NO PAD 비교다.
eventId 및 bucket/key의 정확한 식별에 case-insensitive collation이나 PAD SPACE인 `utf8mb4_bin`을 대신 쓰지 않는다.
- MySQL 8.0.17 이상 및 해당 collation의 binary/NO PAD 속성, InnoDB DYNAMIC·3072-byte 키 지원을 운영자가 확인한다.
16 KiB 이상 InnoDB page를 전제로 하며 4/8 KiB page 환경의 적합성을 주장하지 않는다.
- source unique 최대 키 데이터는 `(255 + 255) × 4 = 2040` bytes다. eventId PK는 `255 × 4 = 1020` bytes다.
결과 unique는 `(36 + 35) × 4 = 284` bytes다. 보조 인덱스의 PK 포함분도 3072 bytes 미만이다.
prefix unique나 해시 식별자로 원래 bucket/key를 대체하지 않는다.
- 경로 컬럼은 현재 UUID 기반 계약에 맞는 `VARCHAR(255)`이며 일반 S3 최대 길이 전체 지원을 뜻하지 않는다.
job의 `(content_id, created_at)` 인덱스는 현재 `findFirstByContentIdOrderByCreatedAtAsc` 조회에 대응한다.
## 3. Lambda handoff와 실제 환경 구분
아래 SHA-256은 **PRD §2의 기존 조사 기록 인용**이다. 문서 담당이 외부 저장소·ZIP·AWS를 다시 조회한 결과가 아니다.
원본 보고서 참조는 PRD에 적힌 `aws-subtitle-service/docs/FINAL_REPORT.md`다.
| 정확한 artifact | SHA-256 |
|---|---|
| `start-subtitle-transcription.zip` | `a0d0b19b2ecb24c0f89caf528bf09fd039558563a99063f082ac64418704abfd` |
| `generate-subtitle.zip` | `bfa8742ca436bace853ec2540fc5f70daa24cbab49e75315565d980189fbf209` |
로컬 artifact 일치 기록은 운영 함수 버전/alias·event routing·consumer·in-flight·Scheduler·DB 적용 증거가 아니다.
자격 증명/JWT/raw body 값은 이 문서나 증거 보고서에 기록하지 않는다.
## 4. 운영 차단 항목
| 게이트 | 현재 | 해제에 필요한 증거 |
|---|---|---|
| 최종 엔티티·DB | BLOCKED | 위 차이 해소, 운영 기존 스키마·버전·collation·인덱스 확인, 별도 DDL 승인·적용 기록 |
| consumer·조정 배포 | BLOCKED | 운영자 inventory, 실행 중일 때만 pause/drain, backend·정확한 두 Lambda의 조정 배포 기록 |
| MediaConvert·CloudFront | BLOCKED | UUID 입력의 ID 추출/출력 확장자 및 private origin·서명 URL 실환경 검증 |
| controlled live | BLOCKED | ko/ja/mixed의 실제 S3 → callback → 콘텐츠/앱 결과와 word/item 언어 증거 |
| 대표 2시간 입력 | BLOCKED·미측정 | 신뢰 가능한 duration 근거 및 대표 입력의 전체 처리 예산 실측 |
### 운영자 실행 순서: 별도 승인 후에만
1. 운영 consumer·event routing·함수 버전/alias·in-flight·기존 DB 상태를 inventory로 확정한다.
실행 중인 consumer가 있을 때만 신규 유입을 pause하고 기존 작업·callback을 drain하는 전환 구간을 조율한다.
실행 중인 대상이 없으면 그 증거와 함께 pause/drain을 생략한다. 미확인 상태를 비가동으로 간주하지 않는다.
2. 최종 JPA와 실제 `SHOW CREATE TABLE`·collation·인덱스 전제를 대조하고 DB 변경을 별도 승인받는다.
기존 동명 테이블/컬럼이 있으면 중단하고 차이를 검토한다. `IF NOT EXISTS`로 불일치를 숨기지 않는다.
additive DDL 승인·적용 확인과 Phase 1 backend/두 Lambda의 배포를 같은 전환 계획으로 조율한다.
구·신 consumer의 중복 실행이나 호환성 미확인 in-flight를 남긴 채 재개하지 않는다.
3. UUID 원본 `input/{contentId}/{UUID filename}`으로 외부 MediaConvert의 contentId 추출 규칙을 확인한다.
등록은 안전한 실제 파일 확장자를 UUID 뒤에 보존한다. 실제 START ZIP은 지원 확장자를 검사하므로
누락/미지원 확장자의 자막 성공을 주장하지 않는다. 이를 위해 기존 업로드를 거부하거나 파일을 허위 변환하지 않는다.
출력 경로·실제 확장자·완료 callback·기존 preview/알림 흐름의 호환 증거가 필요하다.
로컬 `matchesOutput`의 허용 확장자는 `aac`/`m4a`/`mp3`이나 실제 외부 출력 계약이 확인된 것은 아니다.
자막 bucket/key가 기존 private CloudFront origin/path에 정확히 매핑되고 권한 있는 사용자만 서명 URL로
열람하는지 확인한다. 두 항목 모두 현재 미검증이며 임의 prefix 허용이나 공개 S3 URL로 우회하지 않는다.
현재 Spring의 자막 bucket allowlist는 기존 `cloud.aws.s3.content-bucket` 한 개다.
Lambda `MEDIA_BUCKET`과 `SUBTITLE_BUCKET`도 이 값에 맞춰야 하며, 별도 자막 bucket을 쓰려면
allowlist와 CloudFront origin 계약을 먼저 변경·검증해야 한다. 현재 구현이 다중 bucket을 지원한다고 보지 않는다.
4. 일반 유입을 재개하기 전에 controlled ko/ja/mixed live를 수행한다.
S3 source binding, Transcribe 실제 word/item 언어, 결과 SRT, BOT callback, 언어별 콘텐츠/앱 표시를 연결해 기록한다.
mixed는 실제 word/item 언어 증거가 필수이며 synthetic fixture나 HTTP metadata 검증으로 대체할 수 없다.
유료 preview-only 사용자의 전체 자막 차단과 private 접근도 확인한다.
5. 신뢰 가능한 기존 duration의 가용성과 입력 검증 경계를 확인하고 대표 2시간 입력의 처리 예산을 실측한다.
구형/검증 불가능 원본은 수동 게이트로 남기며 임의 client duration을 신뢰하지 않는다.
2시간은 입력 목표이지 SLA/지원 보장이 아니다. 미측정 상태에서 지원 또는 재개 승인을 하지 않는다.
모든 차단 항목의 증거와 승인자를 기록한 뒤에만 일반 유입을 재개한다.
purge·데이터 정리·적용 migration 수정·Scheduler fallback·자동 복구·Translate 50-call 정책은 이 절차에 없다.
재생성 전체와 새 언어 활성화 역시 제외한다. 미해결 작업을 삭제하거나 성공 처리해 drain을 가장하지 않는다.
## 5. 문서 담당 검증 경계
문서 간 계약·실제 엔티티 속성·컬럼별 COMMENT·테이블 COMMENT·SQL 종류·공백을 read/rg로 정적으로 대조했다.
DDL은 CREATE TABLE 3개와 ALTER TABLE ADD COLUMN 1개뿐이며 23개 신규 컬럼·3개 신규 테이블에 COMMENT가 있다.
후행 공백·탭·CR·130자 초과 행은 발견하지 않았다. SQL 파싱/실행이나 물리 스키마 호환성을 검증한 결과가 아니다.
이 담당은 테스트·빌드·DB 실행·AWS 접근·외부 저장소 조회를 하지 않으며 성공 결과를 주장하지 않는다.
상위 구현 검증도 이번 작업에서 새로 작성한 테스트만 실행하도록 승인되어 있다. 기존 테스트 실행은 승인 범위가 아니다.
로컬 HTTP·새 테스트·빌드 결과는 구현 담당의 별도 증거이며, 그 성공도 위 운영 게이트를 해제하지 않는다.