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

12 KiB
Raw Blame History

자막 Phase 1 배포 게이트

현재 판정: 배포 BLOCKED. 이 문서는 로컬 저장 계약과 배포 전 확인 항목이며 DB 적용·AWS 배포 증거가 아니다. 기준일은 2026-11-10이며 PRD, 구현 계획, DDL을 함께 본다.

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

2026-11-11 정정: 이전 정적 대조는 s3Bucket의 숫자 경계 명명 규칙을 놓쳤다. 기본 naming strategy가 s3bucket/s3key로 추론하므로 두 필드에 명시적 @Column(name = "s3_bucket"/"s3_key")가 필요하다. P3-R1에서 실제 validate 실패를 재현한 뒤 두 이름을 명시해 H2 validation/저장/재조회를 통과했다. 독립 리뷰도 승인했다. 운영 DB에 s3bucket 컬럼을 추가한 것이 아니며 실 MySQL 기동은 별도 확인 대상이다.

기존 엔티티 속성 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·새 테스트·빌드 결과는 구현 담당의 별도 증거이며, 그 성공도 위 운영 게이트를 해제하지 않는다.