Files
sodalive-backend-spring-boot/docs/20261110_자막Phase1연동/schema-validation-report.md
T

5.2 KiB

Subtitle 컬럼명 schema-validation 회귀

원인과 범위

사용자가 보고한 Schema-validation: missing column [s3bucket] in table [subtitle]를 실제 Hibernate로 재현했다. Subtitle.s3Bucket/s3Key에 물리 컬럼 이름이 없었고, 기본 naming strategy는 숫자 3 다음 대문자 앞에 underscore를 넣지 않아 s3bucket/s3key로 변환한다. 기존 DDL은 s3_bucket/s3_key이므로 불일치했다.

production 수정은 Subtitle.kt의 두 annotation뿐이다:

@Column(name = "s3_bucket", nullable = false, length = 255)
val s3Bucket: String,
@Column(name = "s3_key", nullable = false, length = 255)
val s3Key: String

Kotlin/JSON 필드명, 기존 DDL, 전역 naming strategy, 운영 DB는 변경하지 않는다. 잘못 추론된 이름의 DB 컬럼을 새로 만들거나 ddl-auto를 update/none으로 바꿔 오류를 숨기지 않는다.

기존 검증의 공백

기존 HTTP 테스트는 ddl-auto:create-drop으로 엔티티 기준 테이블을 자동 생성했으므로 잘못된 컬럼명으로 테이블과 ORM이 동시에 일치했다. 앞선 정적 DDL/JPA 대조도 숫자-대문자 경계의 이름 변환을 놓쳤다. 해당 완료 이력은 보존하되 이번 재현으로 그 검증 한계를 명확히 정정한다.

신규 회귀 테스트

SubtitleSchemaValidationTest.shouldValidateAndUseExistingS3Columns 한 개만 실행한다.

  1. 엔티티와 독립적으로 H2에 s3_bucket/s3_key를 포함한 테이블을 직접 생성한다.
  2. HibernateProperties/HibernateSettings로 Boot 기본 설정을 얻고 실제 Hibernate의 hbm2ddl.auto=validate를 실행한다.
  3. Subtitle을 저장하고 새 Session 및 직접 JDBC SELECT로 두 컬럼의 값을 확인한다.

전체 Spring context/Redis/AWS 및 mock 없이 ORM 저장 표면을 검증한다. DB는 테스트 전용 메모리 H2이며 실제 MySQL collation/FK/운영 기동 전부를 검증하는 테스트는 아니다.

실행 기록

  • RED: 새 테스트1개/실패1개/오류0/skip0, XML UTC 2026-11-11T01:42:01.
  • 실제 실패: SchemaManagementException: Schema-validation: missing column [s3bucket] in table [subtitle].
  • RED 빌드 결과: BUILD FAILED in 3m 49s. 컴파일 오류가 아닌 보고된 운영 오류와 동일한 validation 실패다.
  • GREEN: 새 테스트1개/실패0/오류0/skip0, XML UTC 2026-11-11T01:46:49.
  • 실제 Hibernate validation 성공 후 저장/new Session 조회/직접 JDBC 두 컬럼 조회가 통과했다.
  • bootJar 및 변경 엔티티/신규 테스트에 한정한 ktlintCheck 성공, BUILD SUCCESSFUL in 3m 29s.
  • LSP 도구 미제공으로 Kotlin 컴파일·변경 파일 lint를 사용했다.
  • 독립 한정 리뷰 ses_e37b4db2bffeUyLw5vO2EtHHuG: APPROVE. 두 annotation만 변경됐고 독립 DDL/실제 ORM roundtrip 테스트가 원인에 맞는 것을 확인했다. 신규 blocker 없음.

일반 환경의 신규 테스트 실행 명령:

./gradlew test --tests 'kr.co.vividnext.sodalive.v2.subtitle.SubtitleSchemaValidationTest'

격리 실행은 작업 소유 root subtitle-schema-fix.XcftjN의 init script로 build/project-cache와 테스트 compile/lint 입력을 이 변경에 한정한다. production source는 필터링하지 않는다. 기존 테스트와 전체 테스트는 실행하지 않는다.

실제 격리 실행 명령(RED는 test까지만, GREEN은 아래 전체):

export VERIFY_ROOT=/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/subtitle-schema-fix.XcftjN
export TMPDIR="$VERIFY_ROOT" NODE_COMPILE_CACHE="$VERIFY_ROOT/node-compile-cache"
export BUN_RUNTIME_TRANSPILER_CACHE_PATH="$VERIFY_ROOT/bun-transpiler"
./gradlew --offline --no-daemon --max-workers=2 \
  -Pkotlin.compiler.execution.strategy=in-process -Dorg.gradle.jvmargs=-Xmx4g \
  --project-cache-dir "$VERIFY_ROOT/project-cache" --init-script "$VERIFY_ROOT/verification.init.gradle" \
  test --tests 'kr.co.vividnext.sodalive.v2.subtitle.SubtitleSchemaValidationTest' bootJar ktlintCheck

로그는 위 root의 red.log, green.log다. 실제 운영 DB 접속·DDL 변경·서버 배포/재시작·기존 테스트 실행은 하지 않았다.

정리

검증·리뷰 후 작업 root를 참조하는 프로세스의 부재를 확인하고 소유 root/build/cache/log/JAR를 제거했다. test ! -e exit0으로 확인했으며 위 임시 경로는 과거 검증 위치다. 공유 캐시/다른 작업은 변경하지 않았다. 새 테스트/원인/명령/결과와 리뷰 기록은 저장소에 남겼고 이번 수정은 커밋하지 않았다.

후속 커밋 검증

2026-11-11 별도 /commit-with-validation 요청으로 요구사항·계획은 cf31c570, 수정·회귀 테스트는 c3e5f19c에 커밋했다. 두 커밋 모두 저장소 메시지 검증 스크립트의 직전·직후 검증을 통과했다. 수정 커밋의 pre-commit hook에서 변경 Kotlin 2개 파일의 ktlintCheck도 통과했다.

./gradlew --no-daemon test --tests 'kr.co.vividnext.sodalive.v2.subtitle.SubtitleSchemaValidationTest'를 재실행해 BUILD SUCCESSFUL in 46s, 테스트 1개/실패 0/오류 0/건너뜀 0을 확인했다. 전체 테스트·실제 MySQL 기동·bootJar는 이번 커밋 작업에서 실행하지 않았다. 전체 테스트 사용자 명령은 ./gradlew test다.