Files

18 KiB

성인 콘텐츠 노출 정책 Deprecated 함수 제거 구현 계획

문서 항목 내용
상태 Phase 1~3 완료, P3-R1 구현 완료·실행 검증 대기
작성일 2026-09-17
요구사항 기준 docs/20260917_성인콘텐츠노출정책_Deprecated함수_제거/prd.md
API 기준 기존 API 응답 스키마 변경 없음
현재 Phase Phase 3 후속 실행 검증 대기
현재 활성 Goal P3-R1 실행 검증
다음 Goal 없음

목표

MemberContentPreferencePolicy.isAdultVisibleByPolicy(...) / resolveCountryCodeByPolicy(...) Deprecated 경로를 MemberContentPreferenceService 정식 경로로 이전하고, Deprecated 함수와 전용 테스트를 제거한다. 사용자가 확정한 JP 강제 매핑의 2L 제거를 제외하고 성인 콘텐츠 노출 판정 결과는 유지한다.

현재 상태

Phase 상태 완료 Task 활성/다음 Goal 차단 또는 남은 조건
1 완료 1/1 없음 없음
2 완료 4/4 없음 없음
3 완료 1/1 없음 없음
3 후속 진행 중 0/1 활성: P3-R1 실행 검증 회귀 테스트 구현·소스 검토 완료, 자동 실행 대기

범위

포함

  • MemberContentPreferenceService에 isAdultVisibleForQuery(member, isAdultContentVisible) 추가.
  • production 12개 파일 27곳의 isAdultVisibleByPolicy(...) 호출 치환 및 MemberContentPreferenceService 생성자 주입.
  • MemberContentPreferencePolicy.kt, MemberContentPreferencePolicyTest.kt 제거와 고유 검증 이전.
  • 영향 범위 focused test 및 ktlint 검증.

제외

  • isAdult 판정 알고리즘 변경 및 JP 강제 매핑의 2L 제거 외 국가 강제 매핑 회원 ID 변경.
  • 각 서비스 public 메서드 시그니처 변경, 공개 API 스키마 변경.
  • CanController의 통화 강제 로직.

기술적 제약

  • 기술 스택: Kotlin, Spring Boot 2.7.14, Gradle Wrapper, ktlint.
  • 신규 abstraction을 만들지 않는다. 기존 MemberContentPreferenceService의 resolveCountryCode + calculateIsAdultForQuery를 재사용한다.
  • CountryContext는 @RequestScope이므로 마이그레이션 대상은 웹 요청 경로에서만 호출되는 서비스로 한정한다(RISK-001).
  • 모든 production 변경 Task는 RED → GREEN → REFACTOR 순서로 진행한다.
  • 테스트 실행은 변경 범위 focused test를 우선하고, 전체 회귀는 실행하지 않으며 생략 근거를 검증 기록에 남긴다.

Phase 1: 정식 경로 단일 진입점 확보

Phase 결과: MemberContentPreferenceService.isAdultVisibleForQuery(...)가 Deprecated 함수와 동일한 판정을 제공한다.

선행조건: PRD 확정.

Phase 완료 조건: P1-T1 완료 및 검증 기록 누적.

구현 항목

Task 1.1 isAdultVisibleForQuery 추가

Goal 실행 P1-T1: MemberContentPreferenceService에 접속 국가 계산과 성인 노출 판정을 묶은 단일 메서드를 추가한다.

  • 시작 조건: DEPREM-001 확정.
  • 완료 증거: KR + 미인증 → false, KR + 인증 → 전달값, 비KR → 전달값, 강제 매핑 회원은 헤더보다 강제 국가 우선인 focused test 통과.
  • 범위 밖: 호출부 치환.

Files:

  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/member/contentpreference/MemberContentPreferenceService.kt
  • Test: src/test/kotlin/kr/co/vividnext/sodalive/member/contentpreference/MemberContentPreferenceServiceTest.kt

Interfaces:

  • fun isAdultVisibleForQuery(member: Member, isAdultContentVisible: Boolean): Boolean

  • 구현은 calculateIsAdultForQuery(member, resolveCountryCode(member), isAdultContentVisible)로 한다. DB 조회를 추가하지 않는다.

  • RED: MemberContentPreferenceServiceTest에 isAdultVisibleForQuery 판정 테스트 4건(KR 미인증/KR 인증/비KR/강제 매핑)을 작성한다.

  • RED 확인: MemberContentPreferenceServiceTest 실행 결과 Unresolved reference: isAdultVisibleForQuery 컴파일 실패를 확인했다.

  • GREEN: MemberContentPreferenceService에 isAdultVisibleForQuery를 추가했다.

  • GREEN 확인: MemberContentPreferenceServiceTest 25/25 통과.

  • REFACTOR: resolveCountryCode + calculateIsAdultForQuery 재사용만으로 구현해 중복 계산이 없음을 확인했고 ktlint 통과.

Phase 2: production 호출부 마이그레이션

Phase 결과: production 코드에서 isAdultVisibleByPolicy(...) 호출이 0건이 된다.

선행조건: Phase 1 완료.

Phase 완료 조건: P2-T1~P2-T4와 P2-GATE 완료 및 검증 기록 누적.

구현 항목

Task 2.1 메인 탭 서비스 7개 치환

Goal 실행 P2-T1: 콘텐츠 메인 탭 서비스 7개가 MemberContentPreferenceService로 성인 노출 여부를 계산한다.

  • 시작 조건: P1-T1 완료.
  • 완료 증거: 7개 파일에서 isAdultVisibleByPolicy import/호출 0건, 컴파일 및 영향 범위 테스트 통과.
  • 범위 밖: AudioContentService, ContentSeriesService.

Files:

  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/asmr/AudioContentMainTabAsmrService.kt
  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/replay/AudioContentMainTabLiveReplayService.kt
  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/series/AudioContentMainTabSeriesService.kt
  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/alarm/AudioContentMainTabAlarmService.kt
  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/content/AudioContentMainTabContentService.kt
  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/free/AudioContentMainTabFreeService.kt
  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/home/AudioContentMainTabHomeService.kt

Interfaces:

  • 각 서비스 생성자에 private val memberContentPreferenceService: MemberContentPreferenceService를 추가한다.

  • isAdultVisibleByPolicy(member, isAdultContentVisible) → memberContentPreferenceService.isAdultVisibleForQuery(member, isAdultContentVisible).

  • AudioContentMainTabHomeService의 nullable member 처리(member?.let { ... } ?: false)는 유지한다.

  • RED: 7개 서비스가 각각 대응 *Controller에서만 주입됨을 확인해 RISK-001 위반 경로가 없음을 검증했다.

  • GREEN: 7개 파일의 호출부와 생성자를 치환했다.

  • GREEN 확인: 테스트 컴파일과 src/test/kotlin/kr/co/vividnext/sodalive/content 42/42 통과로 확인했다.

  • REFACTOR: isAdultVisibleByPolicy import를 MemberContentPreferenceService import로 교체했고 ktlint 통과.

Task 2.2 메인/큐레이션/테마 서비스 치환

Goal 실행 P2-T2: AudioContentMainService, AudioContentCurationService, AudioContentThemeService를 정식 경로로 치환한다.

  • 시작 조건: P2-T1 완료.
  • 완료 증거: 3개 파일에서 isAdultVisibleByPolicy 0건, 컴파일 및 영향 범위 테스트 통과.
  • 범위 밖: 다른 Task 대상 파일.

Files:

  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/content/main/AudioContentMainService.kt
  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/content/main/curation/AudioContentCurationService.kt
  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/content/theme/AudioContentThemeService.kt

Interfaces:

  • P2-T1과 동일한 치환 규칙을 적용한다.

  • RED: 3개 서비스의 치환 대상이 모두 member를 받는 웹 조회 메서드임을 확인했다.

  • GREEN: 3개 파일을 치환했다.

  • GREEN 확인: 테스트 컴파일 성공 및 content 패키지 회귀 통과.

  • REFACTOR: import 정리 후 ktlint 통과.

Task 2.3 AudioContentService 치환

Goal 실행 P2-T3: AudioContentService의 3곳을 정식 경로로 치환한다.

  • 시작 조건: P2-T2 완료.
  • 완료 증거: AudioContentServiceTest가 갱신된 생성자로 통과한다.
  • 범위 밖: ContentSeriesService.

Files:

  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt
  • Modify: src/test/kotlin/kr/co/vividnext/sodalive/content/AudioContentServiceTest.kt

Interfaces:

  • P2-T1과 동일한 치환 규칙을 적용하고, 테스트에서는 MemberContentPreferenceService를 mock으로 주입한다.

  • RED: AudioContentServiceTest.kt:118에서 No value passed for parameter 'memberContentPreferenceService' 컴파일 실패를 확인했다.

  • GREEN: getDetail, getLatestCreatorAudioContent, getAudioContentList 3곳을 치환하고 테스트에 mock을 주입했다.

  • GREEN 확인: AudioContentServiceTest 16/16 통과.

  • REFACTOR: import 정리 후 ktlint 통과.

Task 2.4 ContentSeriesService 치환

Goal 실행 P2-T4: ContentSeriesService의 5곳을 정식 경로로 치환한다.

  • 시작 조건: P2-T3 완료.
  • 완료 증거: 시리즈 관련 영향 범위 테스트 통과, isAdultVisibleByPolicy 0건.
  • 범위 밖: Deprecated 함수 제거.

Files:

  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/content/series/ContentSeriesService.kt
  • Test: 필요 시 src/test/kotlin/kr/co/vividnext/sodalive/content/series/main/SeriesMainControllerTest.kt 등 영향 테스트 갱신

Interfaces:

  • P2-T1과 동일한 치환 규칙을 적용한다.

  • RED: 테스트 컴파일을 실행해 ContentSeriesService를 직접 생성하는 테스트가 없어 추가 수정이 필요 없음을 확인했다.

  • GREEN: 5곳(getSeriesList, getSeriesListByGenre, getSeriesDetail, getSeriesContentList, getRecommendSeriesList)을 치환했다.

  • GREEN 확인: 테스트 컴파일 성공 및 Spring 컨텍스트 로딩 테스트 통과.

  • REFACTOR: import 정리 후 ktlint 통과.

Phase 2 Gate

Goal 실행 P2-GATE: production에서 Deprecated 호출이 0건임을 확인한다.

  • isAdultVisibleByPolicy|resolveCountryCodeByPolicy 검색 결과가 src/main/kotlin에서 0건이다.
  • Phase 2 대상 파일의 영향 범위 테스트가 통과한다(content 42/42).

Phase 3: Deprecated 함수 제거와 테스트 정리

Phase 결과: Deprecated 함수와 전용 테스트가 제거되고 고유 검증이 정식 경로 테스트에 남는다.

선행조건: Phase 2 완료.

Phase 완료 조건: P3-T1, P3-GATE 완료 및 검증 기록 누적.

구현 항목

Task 3.1 Deprecated 정의 제거와 검증 이전

Goal 실행 P3-T1: MemberContentPreferencePolicy.kt와 MemberContentPreferencePolicyTest.kt를 제거하고 고유 검증을 이전한다.

  • 시작 조건: P2-GATE 완료.
  • 완료 증거: 두 파일이 삭제되고 member/contentpreference 테스트가 모두 통과한다.
  • 범위 밖: MemberContentPreferenceCountryResolver.kt 변경.

Files:

  • Delete: src/main/kotlin/kr/co/vividnext/sodalive/member/contentpreference/MemberContentPreferencePolicy.kt
  • Delete: src/test/kotlin/kr/co/vividnext/sodalive/member/contentpreference/MemberContentPreferencePolicyTest.kt
  • Modify: src/test/kotlin/kr/co/vividnext/sodalive/member/contentpreference/MemberContentPreferenceServiceTest.kt

Interfaces:

  • 이전 대상 검증: 헤더 공백/소문자 정규화(" us " → US), 국가 컨텍스트 없음 → KR fallback, 비KR에서 저장 countryCode 무시.

  • 이미 정식 경로 테스트에 존재하는 검증은 중복 추가하지 않는다.

  • RED: 로그인 회원의 헤더 공백/소문자 정규화와 KR fallback 검증을 MemberContentPreferenceServiceTest에 추가했다. 나머지 항목(비로그인 정규화, 저장 countryCode 미사용)은 기존 테스트가 이미 커버함을 확인해 중복 추가하지 않았다.

  • GREEN: MemberContentPreferencePolicy.kt와 MemberContentPreferencePolicyTest.kt를 삭제했다.

  • GREEN 확인: member/contentpreference 36/36 통과.

  • REFACTOR: ktlint 통과 및 잔여 참조 0건 확인.

Phase 3 Gate

Goal 실행 P3-GATE: 전체 정리 상태를 확인한다.

  • isAdultVisibleByPolicy|resolveCountryCodeByPolicy 참조가 src/main/kotlin, src/test/kotlin에서 0건이다.
  • member/contentpreference 테스트와 Phase 2 영향 범위 테스트가 통과한다.
  • ./gradlew ktlintMainSourceSetCheck ktlintTestSourceSetCheck가 통과한다.
  • PRD 성공 기준 체크박스를 갱신했다.

Phase 3 후속: 리뷰 검증 보강

Task 3.R1 저장 국가 무시 검증 이전

Goal 실행 P3-R1: 저장 국가가 KR인 미인증 회원이 US에서 조회할 때 저장 국가를 무시하는 기존 정책을 정식 서비스 테스트로 검증한다.

  • 시작 조건: DEPREM-004 및 reviews/문서대비구현-review.md의 REV-002 확정. REV-001은 사용자 확인으로 결함 판정 철회; 2L 제거 유지.
  • 완료 증거: 아래 테스트 추가 및 조건·assertion 소스 검토, 후속 실행이 허용된 경우 focused test 결과 기록. 실행하지 않았으면 실행 검증 대기로 남기고 완료 처리하지 않는다.
  • 범위 밖: production 정책·resolver·매핑 변경, API 변경, 테스트 helper 리팩터링, 전체 회귀 실행.
  • TDD 예외: 현재 production은 올바르게 저장 국가를 무시하므로 신규 테스트는 처음부터 통과하는 것이 정상이다. 실패를 만들려고 정상 구현을 변경하지 않는다. 삭제된 테스트와 새 테스트의 입력·assertion 대조로 누락 검증 이전을 확인한다.
  • 실행 제약: 이번 문서 갱신에서는 테스트를 실행하지 않는다. 기존 자동 테스트 미실행 지시가 유지되는 동안 후속 작업도 소스 검토까지만 진행하고 실행 상태를 별도로 남긴다.

Files:

  • Modify: src/test/kotlin/kr/co/vividnext/sodalive/member/contentpreference/MemberContentPreferenceServiceTest.kt
  • Update: docs/20260917_성인콘텐츠노출정책_Deprecated함수_제거/plan-task.md
  • Update: docs/20260917_성인콘텐츠노출정책_Deprecated함수_제거/reviews/문서대비구현-review.md

추가할 테스트: 기존 createMember, countryContext, service, JUnit import를 재사용한다.

@Test
@DisplayName("비KR 요청에서는 회원의 저장 국가와 무관하게 전달한 성인 노출값을 사용한다")
fun shouldIgnoreStoredCountryCodeWhenRequestCountryIsNotKr() {
    val member = createMember(id = 2401L, withAuth = false).apply {
        countryCode = "KR"
    }
    countryContext.setCountryCode("US")

    assertEquals("US", service.resolveCountryCode(member))
    assertTrue(service.isAdultVisibleForQuery(member, isAdultContentVisible = true))
    assertFalse(service.isAdultVisibleForQuery(member, isAdultContentVisible = false))
}
  • 누락 확인: 삭제된 정책 테스트와 비교해 저장 KR·요청 US·미인증 조건이 기존 정식 테스트에 없음을 확인했다(TDD 예외 적용).
  • 최소 구현: 위 테스트 1건만 추가했다. 강제 매핑이 없는 ID 2401을 사용하고 production은 변경하지 않았다.
  • 소스 검토: 국가 결과 US 및 노출 전달값 true/false 보존 assertion을 확인했다. 저장 국가 KR을 우선하면 국가 assertion과 true 전달값 assertion이 실패한다.
  • 실행 검증: 자동 테스트 실행이 허용되는 후속 단계에서 아래 focused test와 ktlint를 실행하고 결과를 기록한다. 미실행이면 완료 표시하지 않는다.
  • 기록 갱신: P3-T1의 기존 완료 기록을 유지하고, REV-002 보강 결과와 실행 대기 상태를 본 Task와 리뷰에 누적했다.

후속 실행용 명령(이번에는 미실행): 저장소와 호환되는 JDK 환경에서 실행한다.

./gradlew test --tests 'kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceServiceTest'
./gradlew ktlintTestSourceSetCheck

기대 결과: 신규 테스트를 포함한 서비스 테스트 및 테스트 소스 ktlint 성공. 테스트 한 파일의 검증 보강이므로 전체 회귀는 생략한다.

검증 기록

  • 2026-09-17 Phase 3 후속 P3-R1: 삭제된 정책 테스트와 현재 서비스 테스트를 대조해 저장 KR·요청 US·미인증 조건의 누락을 확인하고 MemberContentPreferenceServiceTest.shouldIgnoreStoredCountryCodeWhenRequestCountryIsNotKr 1건을 추가했다. 국가 결과 US와 전달값 true/false 보존 assertion을 소스로 검토했다. production은 변경하지 않았으며 기존 자동 테스트 미실행 지시에 따라 focused test와 ktlint는 실행하지 않아 Task 완료 처리는 보류했다.

  • 2026-09-17 리뷰 후속 문서 갱신: 사용자가 JP 매핑의 2L 제거를 의도한 변경으로 확정했다. REV-002 검증 누락은 P3-R1로 추가하고 REV-003 호출 수는 27곳으로 정정했다. 문서와 테스트 소스의 식별자·조건을 대조했으며 사용자 지시에 따라 자동 테스트 및 Gradle 명령은 실행하지 않았다.

  • 2026-09-17 Phase 1: MemberContentPreferenceServiceTest 실행 → RED에서 Unresolved reference: isAdultVisibleForQuery 확인, GREEN 후 25/25 통과. 검증 이유: 신규 판정 메서드가 Deprecated 함수와 동일한 결과를 내는지 확인.

  • 2026-09-17 Phase 2: AudioContentServiceTest.kt:118 컴파일 실패로 생성자 변경 영향을 확인한 뒤 mock 주입. src/test/kotlin/kr/co/vividnext/sodalive/content 42/42 통과, AudioContentServiceTest 16/16 통과.

  • 2026-09-17 Phase 3: src/test/kotlin/kr/co/vividnext/sodalive/member/contentpreference 36/36 통과(Deprecated 테스트 5건 삭제, 신규 3건 추가 반영).

  • 2026-09-17 빈 주입 회귀: SpringBootIntegrationSampleTest 1/1 통과로 12개 서비스에 추가된 MemberContentPreferenceService 주입이 애플리케이션 컨텍스트에서 정상 해석됨을 확인(순환 의존 없음).

  • 2026-09-17 스타일: JAVA_HOME=<Android Studio JBR 21> ./gradlew ktlintMainSourceSetCheck ktlintTestSourceSetCheck BUILD SUCCESSFUL. JetBrains JBR 25는 Gradle 8.1.1과 비호환이어서 Java 21 JBR을 사용했다.

  • 2026-09-17 전체 회귀 생략: ./gradlew test 전체 회귀는 실행하지 않았다. 근거는 변경이 isAdult 계산 위임 경로 치환으로 한정되고 공개 스키마/DB/보안 설정 변경이 없으며, 대신 변경 범위 focused test(content 42, member/contentpreference 36, AudioContentServiceTest 16)와 컨텍스트 로딩 테스트, ktlint 전체 소스셋 검사를 실행했다.