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

21 KiB

자막 Phase 1 Kotlin 구현 보고

결과와 검증 경계

  • 재생성을 제외한 기존 업로드 등록, 네 BOT 내부 API, callback 원자 저장, 기존 콘텐츠 상세의 권한별 ko/ja 자막 조회를 구현했다.
  • 새로 작성한 SubtitleHttpIntegrationTest 13개는 실제 RANDOM_PORT localhost HTTP/H2에서 전부 통과했다. 외부 AWS 클라이언트와 서명 URL 생성만 대체하고, 회원 서비스·로그인·비밀번호 검증·JWT·DB principal/role은 실제 경로를 사용했다.
  • bootJar와 변경 Kotlin 파일에 한정한 ktlintCheck가 성공했다. LSP 도구는 제공되지 않아 실행하지 않았으며 실제 Kotlin/JPA 생성 코드·테스트 컴파일과 lint로 대체했다.
  • PRD/계획과 SQL/deployment 문서는 병렬 담당자 소유다. 이 담당자는 해당 문서를 수정하지 않았으며 전체 Phase gate/독립 리뷰 완료를 대신 선언하지 않는다.

구현 선택

  1. AudioContentService.createAudioContent 두 overload와 AI 관리자 create orchestration은 NOT_SUPPORTED다. 필수 주입된 transaction manager의 TransactionTemplate(REQUIRES_NEW)가 content·tags·cover·source/job·latest pointer를 commit한 뒤 원본 S3 업로드를 시작한다. 업로드 후 content의 input 경로를 다시 대입하지 않으므로 조기 MediaConvert 완료 경로를 되돌리지 않는다. 성공 후 기존 언어 이벤트는 별도의 짧은 실제 쓰기 트랜잭션에서 발행한다.
  2. 원본은 input/{contentId}/{canonical UUID}.{safe actual extension}이다. multipart filename의 경로는 버리고 영문/숫자 1~16자리 실제 확장자만 소문자로 보존한다. 예를 들어 C:\client\dir/file.MP3는 UUID .mp3로 등록된다. 확장자가 없거나 안전하게 보존할 수 없으면 UUID만 사용한다. 업로드 자체를 새로 거부하거나 .mp3로 허위 변경하지 않는다. 확장자는 형식 hint일 뿐 실제 오디오 검증 증거가 아니다.
  3. SubtitleJob.outputKey는 확장자를 제외한 output/{contentId}/{same UUID} stem이다. 새 등록의 upload-complete는 이 정확한 stem과 aac/m4a/mp3 확장자 조합만 수락한다. 다른 콘텐츠, 다른 UUID, 추가 suffix, 임의 디렉터리, 비오디오 확장자는 거부한다. source가 없는 기존 콘텐츠의 동작은 기존 prefix 검증을 유지한다. 실제 consumer의 basename·ID 추출 호환성은 여전히 배포 전 확인 대상이다.
  4. 언어 결과는 Subtitle 행에 저장한다. jobId scalar와 unique(job_id, language), 실제 language·format·s3Bucket·s3Key를 보존한다. callback 허용 언어는 여전히 ko-KR/ja-JP이며 상태 규칙은 2/1/0개 그대로다. 미래 언어 활성화/새 provider framework는 추가하지 않았다.
  5. callback은 원래 HTTP ByteArray SHA-256을 receipt와 함께 저장한다. REQUIRES_NEW/READ_COMMITTED에서 receipt를 먼저 조회하고 job lock 뒤 다시 조회한다. 기존 eventId로 unknown job을 보낸 변경 body도 422다. receipt는 JPA Persistable.isNew=true로 insert하여 merge가 경합한 다른 receipt를 update하지 못하게 했다. unique 경합은 rollback 종료 후 새 트랜잭션에서 receipt를 읽어 200/422를 판정한다. terminal의 동일 결과는 행 ID를 보존하고, 다른 결과는 409다.

변경 경로

신규 production

src/main/kotlin/kr/co/vividnext/sodalive/v2/subtitle/domain/SubtitleJobStatus.kt
src/main/kotlin/kr/co/vividnext/sodalive/v2/subtitle/dto/SubtitleJobDtos.kt
src/main/kotlin/kr/co/vividnext/sodalive/v2/subtitle/adapter/persistence/SubtitleJob.kt
src/main/kotlin/kr/co/vividnext/sodalive/v2/subtitle/adapter/persistence/SubtitleJobRepository.kt
src/main/kotlin/kr/co/vividnext/sodalive/v2/subtitle/adapter/persistence/Subtitle.kt
src/main/kotlin/kr/co/vividnext/sodalive/v2/subtitle/adapter/persistence/SubtitleRepository.kt
src/main/kotlin/kr/co/vividnext/sodalive/v2/subtitle/adapter/persistence/SubtitleCallbackReceipt.kt
src/main/kotlin/kr/co/vividnext/sodalive/v2/subtitle/adapter/persistence/SubtitleCallbackReceiptRepository.kt
src/main/kotlin/kr/co/vividnext/sodalive/v2/subtitle/application/SubtitleJobService.kt
src/main/kotlin/kr/co/vividnext/sodalive/v2/subtitle/adapter/web/SubtitleJobController.kt
src/main/kotlin/kr/co/vividnext/sodalive/v2/subtitle/adapter/web/SubtitleRequestParser.kt
src/main/kotlin/kr/co/vividnext/sodalive/v2/subtitle/adapter/web/SubtitleExceptionHandler.kt

기존 production 연결

src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt
src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContent.kt
src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt
src/main/kotlin/kr/co/vividnext/sodalive/content/GetAudioContentDetailResponse.kt
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt

테스트와 보고

src/test/kotlin/kr/co/vividnext/sodalive/v2/subtitle/SubtitleHttpIntegrationTest.kt
src/test/kotlin/kr/co/vividnext/sodalive/content/AudioContentServiceTest.kt
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCreateTest.kt
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/LegacyCreatorAdminAudioContentCharacterizationTest.kt
docs/20261110_자막Phase1연동/implementation-report.md
  • 기존 수동 AudioContentService 생성자는 필수 자막 서비스/transaction manager 인자를 반영했다.
  • 기존 생성 통합 테스트의 성공 fixture는 등록 전에 실제 commit되도록 조정했다. audio 업로드/후속 이벤트 실패 테스트는 이미 commit된 등록을 보존한다는 새 승인 계약에 맞췄다. 실패 테스트를 삭제하거나 성공 assertion을 느슨하게 하지 않았다.
  • 이 기존 테스트들은 후속 사용자 지시 이후 실행하지 않았다. 관련 기존 5개 클래스는 새 테스트 실행의 컴파일 입력에 포함했지만 실행 selector는 신규 자막 패키지뿐이다.
  • production 파일 삭제, 기존 JWT/member/텍스트 TranslationJob worker 변경, 외부 프로젝트 변경, 신규 AWS SDK 의존성, 재생성 API/START 호출은 없다.

새 테스트 selector

전체 신규 클래스만 실행하는 selector:

--tests 'kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest'

개별 selector는 아래 클래스명 뒤에 각 메서드를 붙인다. 모두 이번 작업에서 새로 작성한 case다.

kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.shouldRejectActualNonBotLoginJwt
kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.shouldUseRealBotJwtAndStoreCallbackBeforeStartedWithExactByteReplay
kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.shouldStartMonotonicallyAndAcceptPartialAndFailedResults
kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.shouldReturn404WithoutDiscoveryAndCheckCurrentDatabaseRole
kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.shouldRejectMalformedContractsWithoutPersistingAnything
kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.shouldSerializeConcurrentCallbacksAndRollbackLosingJob
kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.shouldRollbackResultsAndStatusWhenReceiptInsertFails
kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.shouldKeepLatestPointerWhenOlderJobCompletes
kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.shouldCommitRegistrationBeforeUploadAndNeverRevertEarlyMediaConvertOutput
kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.shouldKeepAcceptedSourceAndTerminalResultWhenUploadReturnFails
kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.shouldPreserveOnlySafeActualSourceExtensionsWithoutClaimingConversion
kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.shouldExposeLanguageSubtitleOnlyWithReleasedOriginalAccessInExistingDetail
kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.shouldReadOnlyPersistedLanguageOfMatchingLatestCompletedJob

재현 명령과 아티팩트

작업 전용 root는 삭제하지 않고 부모 검증에 인계한다.

ROOT=/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/subtitle-phase1.wiRcAJ/backend.HX0j0B
export TMPDIR="$ROOT"
export NODE_COMPILE_CACHE="$ROOT/node"
export BUN_RUNTIME_TRANSPILER_CACHE_PATH="$ROOT/bun"

./gradlew --no-daemon --max-workers=2 \
  -Pkotlin.compiler.execution.strategy=in-process -Dorg.gradle.jvmargs=-Xmx4g \
  --project-cache-dir "$ROOT/gradle-cache" -I "$ROOT/isolation.gradle" \
  test --tests 'kr.co.vividnext.sodalive.v2.subtitle.*'

./gradlew --no-daemon --max-workers=2 \
  -Pkotlin.compiler.execution.strategy=in-process -Dorg.gradle.jvmargs=-Xmx4g \
  --project-cache-dir "$ROOT/gradle-cache" -I "$ROOT/isolation.gradle" \
  bootJar ktlintCheck
  • 임시 init script는 build 출력과 Gradle project cache를 공유 출력에서 격리한다. 테스트 Kotlin 컴파일 입력은 신규 자막/직접 영향 기존 5개 클래스/support이며, lint filter도 변경 Kotlin 파일만 대상으로 한다. 전역 설정이나 저장소 build 설정은 변경하지 않았다.
  • 성공 테스트: $ROOT/green-new-tests-cached.log, BUILD SUCCESSFUL in 1m 32s. 임시 script의 사용하지 않는 JWT 기본값을 제거한 뒤 다시 실행한 최종 $ROOT/final-new-tests.log도 BUILD SUCCESSFUL in 2m 25s다. XML: $ROOT/build/test-results/test/TEST-kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.xml, tests=13, failures=0, errors=0, skipped=0.
  • 실제 두 job의 동일 eventId 경합에서 H2 SQLState 23505 unique 위반이 관찰됐고, 최종 HTTP 200/422와 loser job의 PENDING/결과 행 없음 assertion이 통과했다. 별도의 receipt 쓰기 실패 주입에서도 identity 생성으로 flush된 결과 행까지 rollback됨을 확인했다.
  • 성공 빌드/lint: $ROOT/build-lint.log, BUILD SUCCESSFUL in 1m 28s. 실행 가능한 boot JAR은 $ROOT/build/libs/sodalive-0.0.1-SNAPSHOT.jar다. jar tf에서 새 컨트롤러/서비스/엔티티/repository/DTO가 BOOT-INF/classes에 포함된 것을 확인했다.
  • 검증 완료 후 root를 참조하는 작업 소유 Gradle/HTTP/Redis 프로세스는 관찰되지 않았다. 기존 공유 worker PID 55393과 그 Redis PID 55452는 이 작업 소유가 아니며 종료하지 않았다. 테스트 실행 중 관찰된 작업 소유 executor PID 20950과 최종 executor PID 22207은 종료됐다.

RED와 실패 이력

  • 첫 baseline/신규 RED 명령은 테스트 컴파일 중 600초 제한으로 종료됐다(red.log). 테스트 결과가 아니며 RED로 인정하지 않았다.
  • red-focused.log는 이전 지시에 따른 기존 3개 회귀 클래스와 신규 1개 case 실행이다. 총 41개 중 1개가 테스트 JWT key setup에서 실패했다. 이를 기능 RED로 인정하지 않았다. 이 명령은 기존 테스트 실행 금지라는 후속 사용자 지시 전의 기록이며 현재 구현의 회귀 성공 증거로 사용하지 않는다.
  • 테스트 전용 DynamicPropertySource로 유효 길이의 비운영 JWT key를 지정한 뒤 red-http.log에서 실제 로그인 성공 후 403 기대 / 404 실제를 확인했다. BOT 전용 경로가 없던 동작의 실제 RED다.
  • 초기 red-binding.log는 JPA unique constraint의 logical/physical column 이름 오류로 context가 뜨지 못했다. 이를 RED로 인정하지 않고 엔티티에 명시적 column 이름을 붙였다.
  • red-binding-assertions.log에서 새 두 case가 실제 assertion으로 실패했다: source 확장자 보존 실패, 기존 eventId의 unknown job 변경에 대해 422 기대 / 404 실제. 확장자 전달과 receipt 선조회/명시적 isolation 변경 후 같은 두 case를 포함한 13개 전체 신규 case가 GREEN이다.
  • green-first.log는 사용자 지시 갱신을 위한 중단으로 완료 결과가 없다. green-new-tests.log는 compile을 통과한 뒤 test 시작 시점에 600초 도구 제한으로 종료됐다. 공유 프로세스를 종료하지 않고 같은 격리 경로의 캐시를 재사용한 green-new-tests-cached.log에서 완료 결과를 얻었다.
  • format.log의 첫 변경 파일 format 실행은 길이 위반을 보고했다. 해당 새 줄만 정리한 뒤 최종 ktlintCheck가 통과했다.

외부 게이트와 남은 미검증

  1. 실제 MediaConvert consumer가 기존 contentId filename prefix를 읽는지, UUID basename·출력 확장자·디렉터리를 어떻게 구성하는지 운영 증거가 없다. 로컬에서는 정확한 UUID stem과 AAC/M4A/MP3 경로 검증만 확인했다. consumer의 운영 호환성을 확인하기 전 배포 전환은 BLOCKED다.
  2. 부모가 승인된 ZIP에서 확인한 Transcribe 지원 확장자는 amr/flac/m4a/mp3/mp4/ogg/webm/wav다. Spring은 AAC/opus 등 기존 업로드를 임의로 거부하지 않는다. 이들 또는 누락/불안전 확장자에 대한 자막 성공은 보장하지 않으며 Lambda의 명시적 unsupported 결과/수동 확인 대상이다. 테스트의 한 바이트 파일은 source binding/transaction fixture일 뿐 실제 미디어 증거가 아니다.
  3. S3 putObject 수락 후 getUrl 실패는 성공으로 숨기지 않는다. 원래 예외를 호출자에게 전파하고 SOURCE_UPLOAD_UNCERTAIN 및 초 정밀도 TIMESTAMP를 별도 기록한다. 이미 완료된 자막 상태/metadata와 content를 삭제하거나 회귀시키지 않는다. 실제 객체/MediaConvert 상태는 운영자 수동 확인이 필요하다.
  4. 운영 MySQL DDL 적용, case-sensitive/NO PAD collation 및 FK/인덱스 검증, private CloudFront origin과 실제 SRT 서명 접근, 실서버 권한/앱 표시, live ko/ja/mixed 및 실제 duration/2시간 처리 예산은 미검증이다. AWS 호출·배포·DB 적용은 수행하지 않았다.
  5. 전체 테스트와 후속 지시 이후의 기존 회귀 case 실행은 수행하지 않았다. H2 전체 Spring context의 기존 charge/translation scheduler는 MySQL FOR UPDATE SKIP LOCKED 구문 때문에 로그 오류를 남겼다. 이번 신규 case는 모두 통과했지만 그 worker 기능의 성공 증거는 아니다. 사용자가 별도로 전체 테스트를 원할 때만 ./gradlew test를 실행한다.

20261111 P1 후속 수정: body/DB 실패의 HTTP 200 오인 방지

수정 범위와 계약

  • SubtitleExceptionHandler.kt의 기존 SubtitleJobController 전용 scope와 우선순위를 유지했다. HttpMessageNotReadableException은 HTTP 422와 기존 일반화된 거절 body로, 그 외 예상하지 못한 Exception은 HTTP 500과 {"error":"Subtitle request failed"}로 응답한다.
  • 기존 ResponseStatusException의 404/409/422 처리는 변경하지 않았다. 공통 SodaExceptionHandler, JWT, 컨트롤러, 서비스 및 DB 저장 로직은 변경하지 않았으며 예외 원문·요청·토큰 로그도 추가하지 않았다.
  • 수정 파일은 자막 advice, SubtitleHttpIntegrationTest.kt 및 이 보고서의 추가 기록뿐이다. 기존 직접 서비스 롤백 테스트 shouldRollbackResultsAndStatusWhenReceiptInsertFails와 기존 assertion은 그대로 보존했다. 부모 소유 PRD/계획/final-report는 수정하지 않았다.

새 HTTP 두 사례

  1. shouldRejectEmptyCallbackAndStartedBodiesWithoutPersistingAnything: 실제 BOT 로그인/JWT로 빈 callback·started body를 모두 전송해 422를 확인한다. job/receipt/결과 행 수, PENDING 상태, 갱신 시각 및 오류 필드가 변경되지 않고 해당 job 결과도 없음을 검증한다.
  2. shouldReturn500AndRollbackWhenReceiptInsertFailsOverHttp: 실제 HTTP callback에서 기존 receipt repository SpyBean의 saveAndFlush에 DB 예외를 주입한다. ko-KR partial callback을 사용하여 상태뿐 아니라 오류 필드도 변경 대상에 포함하고, 일반화된 500 body 및 receipt·결과 부재, PENDING/갱신 시각/오류 필드 롤백을 DB 재조회로 확인한다.

RED/GREEN 명령과 증거

위 격리 ROOT와 TMPDIR/Node/Bun cache 환경을 그대로 사용했다. 외부 의존성 다운로드를 막는 --offline과 부모의 verification.gradle을 추가하여 테스트만 강제로 다시 실행했다. clean 및 무필터 test/check/build는 실행하지 않았다.

advice 수정 전 RED와 수정 후 두 사례 GREEN에 사용한 동일 selector/명령:

./gradlew --offline --no-daemon --max-workers=2 \
  -Pkotlin.compiler.execution.strategy=in-process -Dorg.gradle.jvmargs=-Xmx4g \
  --project-cache-dir "$ROOT/gradle-cache" \
  -I "$ROOT/isolation.gradle" -I "$ROOT/../verification.gradle" \
  test \
  --tests 'kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.shouldRejectEmptyCallbackAndStartedBodiesWithoutPersistingAnything' \
  --tests 'kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.shouldReturn500AndRollbackWhenReceiptInsertFailsOverHttp'
  • RED: $ROOT/p1-red-two-http.log, BUILD FAILED in 4m 16s, 정확히 2개 실행/2개 assertion 실패다. $ROOT/p1-red-assertions.log에 XML의 실패 증거를 보존했다. 빈 body는 expected [422, 422] / actual [200, 200], DB 실패는 expected 500 / actual 200이었다.
  • 첫 GREEN 시도 $ROOT/p1-green-two-http.log는 compileKotlin 도중 600초 도구 제한으로 종료됐다. 테스트가 실행되지 않아 GREEN으로 계산하지 않았다. 작업 소유 프로세스가 남지 않은 것을 확인한 후 같은 캐시와 1200초 제한으로 재실행했다.
  • 두 사례 GREEN: $ROOT/p1-green-two-http-retry.log, BUILD SUCCESSFUL in 13m 29s. $ROOT/p1-green-two-assertions.log는 tests=2, failures=0, errors=0, skipped=0이다.

이후 자막 신규 클래스 15개 전체와 기존 격리 script의 변경 Kotlin 파일 lint 및 boot JAR를 함께 검증했다:

./gradlew --offline --no-daemon --max-workers=2 \
  -Pkotlin.compiler.execution.strategy=in-process -Dorg.gradle.jvmargs=-Xmx4g \
  --project-cache-dir "$ROOT/gradle-cache" \
  -I "$ROOT/isolation.gradle" -I "$ROOT/../verification.gradle" \
  test --tests 'kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest' \
  bootJar ktlintCheck
  • 최종 $ROOT/p1-green-15-build-lint.log: BUILD SUCCESSFUL in 7m 44s. production/test 컴파일은 UP-TO-DATE였으며 test, bootJar, main/test 변경 파일 lint는 실제 실행됐다.
  • 최종 XML $ROOT/build/test-results/test/TEST-kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest.xml: tests=15, failures=0, errors=0, skipped=0. 실제 RANDOM_PORT HTTP에서 새 422/500과 기존 404/409/422 및 직접 서비스 롤백 case가 모두 통과했다. 기존 회귀 클래스는 컴파일 입력일 뿐 실행하지 않았다.
  • $ROOT/build/libs/sodalive-0.0.1-SNAPSHOT.jar에 수정된 SubtitleExceptionHandler.class가 포함됨을 jar tf로 확인했다. LSP 도구는 제공되지 않아 사용하지 못했으며 Kotlin 컴파일과 변경 파일 lint를 확인했다.
  • RED/GREEN/최종 테스트 executor PID 28638/37727/39291은 모두 종료됐다($ROOT/p1-test-process-status.log는 header만 존재). 완료 후 작업 root를 사용하는 Gradle/HTTP/Redis 프로세스는 관찰되지 않았다. 기존 공유 PID 55393/55452는 그대로이며 어떤 task도 취소하거나 프로세스를 수동 종료하지 않았다. root와 로그는 부모 재검증용으로 보존했다.

남은 검증 경계

  • 기존 Kotlin 경고, Gradle deprecation, H2의 기존 charge/translation scheduler SQL 오류와 Redis 종료 시 재접속 경고는 관찰됐으나 수정 범위 밖이다. 신규 자막 15개 assertion은 전부 통과했다.
  • 기존 회귀/전체 테스트, 운영 MySQL, AWS·MediaConvert·Lambda 실연동 및 배포 검증은 수행하지 않았다. 앞 절의 외부 게이트는 그대로 남으며, 전체 테스트 사용자 실행 명령은 ./gradlew test다.

부모 최종 검증·정리 추가 기록

  • 부모가 처음 신규13개 및 P1의 두 HTTP 사례를 각각 직접 재실행해 모두 통과했고, 독립 리뷰의 유일 finding은 ADDRESSED 및 로컬 backend APPROVE 판정을 받았다.
  • 부모의 P1 재실행은 BUILD SUCCESSFUL in 4m 55s, 실제 XML2/0/0/0, timestamp 2026-11-10T15:34:43이다.
  • 2026-11-11 작업 소유 PID/listener 부재를 확인한 후 위 임시 ROOT 전체를 제거했다. 따라서 ROOT 기반 로그·init script·JAR 경로는 과거 검증 위치이며 현재 파일로 남아 있지 않다. 명령/결과/실패 이력은 본 보고서와 final-report에 보존했다.
  • 일반 개발 환경의 재현 명령은 ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.subtitle.SubtitleHttpIntegrationTest'다. 기존 테스트는 실행하지 않는 selector다.