Files

11 KiB

성인 콘텐츠 노출 정책 Deprecated 함수 제거 PRD

문서 정보

항목 내용
문서 상태 Phase 1~3 완료, 리뷰 후 고유 검증 보강 대기
작성일 2026-09-17
최종 수정일 2026-09-17
대상 제품 성인 콘텐츠 노출 여부 / 접속 국가 코드 계산 정책
작성자·결정권자 Junie / 사용자
관련 API Contract 기존 API 응답 스키마 변경 없음
관련 구현 계획 docs/20260917_성인콘텐츠노출정책_Deprecated함수_제거/plan-task.md
관련 review reviews/문서대비구현-review.md

1. Overview

성인 콘텐츠 노출 여부(isAdult)와 접속 국가 코드 계산 정책은 현재 두 경로에 중복 구현되어 있다. 하나는 @Deprecated로 표시된 top-level 함수 MemberContentPreferencePolicy.isAdultVisibleByPolicy(...) / resolveCountryCodeByPolicy(...)이고, 다른 하나는 정식 경로인 MemberContentPreferenceService다. Deprecated 경로가 production 12개 파일에서 여전히 27곳 호출되고 있어 정책 변경 시 두 곳을 모두 고쳐야 하는 위험이 남아 있다. 이 작업은 Deprecated 경로 호출부를 MemberContentPreferenceService로 이전하고 Deprecated 함수와 그 전용 테스트를 제거한다.

2. Problem Statement

  • MemberContentPreferencePolicy.kt의 두 함수는 docs/20260623_메인_콘텐츠_추천_탭_API/plan-task.md에서 @Deprecated 처리만 되고 호출부 이전은 완료되지 않았다.
  • 접속 국가 판정 기준이 두 경로에서 다르게 조달된다. Deprecated 경로는 RequestContextHolder에서 CloudFront-Viewer-Country 헤더를 직접 읽고, 정식 경로는 CountryContext(요청 스코프)에서 읽는다. 값 출처(같은 헤더, CountryInterceptor가 주입)는 같지만 경로가 둘이다.
  • Deprecated 함수의 유일한 테스트인 MemberContentPreferencePolicyTest의 5개 검증은 MemberContentPreferenceServiceTest / MemberContentPreferenceIntegrationTest에 이미 동등하게 존재해, 회원 ID 강제 매핑 정책을 변경할 때마다 같은 상수를 3~4개 파일에서 함께 고쳐야 한다.
  • 결과적으로 정책 변경(예: 국가 강제 매핑 회원 ID 추가/삭제)이 중복 지점 누락으로 회귀를 만들 수 있다.

문제를 해결했다는 판단은 isAdultVisibleByPolicy / resolveCountryCodeByPolicy 참조가 src/main/kotlin과 src/test/kotlin에서 0건이 되고, 기존 legacy 콘텐츠 조회 API의 isAdult 계산 결과가 그대로 유지되는 것으로 한다.

3. Goals

  • 성인 콘텐츠 노출 여부 계산의 단일 진입점을 MemberContentPreferenceService로 만든다.
  • MemberContentPreferencePolicy.kt의 Deprecated 함수 2개와 파일, 전용 테스트를 제거한다.
  • 기존 legacy 콘텐츠 조회 API의 isAdult / 국가 코드 계산 동작을 유지하되, 사용자가 확정한 JP 강제 매핑의 2L 제거는 예외로 반영한다.
  • 공개 API request/response 스키마를 변경하지 않는다.

4. Non-Goals

  • isAdult 판정 정책 자체(KR + 본인인증 여부, 해외 전달값 사용)를 변경하지 않는다.
  • JP 강제 매핑의 2L 제거 외에는 회원 ID 국가 강제 매핑(FORCED_KR_MEMBER_IDS, FORCED_JP_MEMBER_IDS) 값을 변경하지 않는다.
  • isAdultContentVisible을 서비스 파라미터에서 제거하거나 저장값 조회로 대체하는 리팩터링은 하지 않는다. 호출부 시그니처는 유지한다.
  • CanController의 통화(currency) 강제 지정 로직은 국가 강제 매핑과 무관하므로 건드리지 않는다.
  • MemberContentPreferenceCountryResolver.kt의 강제 매핑 알고리즘은 유지하고, JP 대상에서 2L만 제외한다.

5. 영향 범위

5.1 Deprecated 함수 정의

파일 대상
src/main/kotlin/kr/co/vividnext/sodalive/member/contentpreference/MemberContentPreferencePolicy.kt resolveCountryCodeByPolicy(member), isAdultVisibleByPolicy(member, isAdultContentVisible)

5.2 production 호출부

파일 호출 수
content/series/ContentSeriesService.kt 5
content/AudioContentService.kt 3
content/main/AudioContentMainService.kt 2
content/main/curation/AudioContentCurationService.kt 2
content/theme/AudioContentThemeService.kt 2
content/main/tab/free/AudioContentMainTabFreeService.kt 4
content/main/tab/alarm/AudioContentMainTabAlarmService.kt 2
content/main/tab/content/AudioContentMainTabContentService.kt 2
content/main/tab/home/AudioContentMainTabHomeService.kt 2
content/main/tab/asmr/AudioContentMainTabAsmrService.kt 1
content/main/tab/replay/AudioContentMainTabLiveReplayService.kt 1
content/main/tab/series/AudioContentMainTabSeriesService.kt 1

12개 파일, 총 27곳이다. 어떤 파일도 현재 MemberContentPreferenceService를 주입받지 않는다.

5.3 테스트 호출부

파일 처리
src/test/kotlin/.../member/contentpreference/MemberContentPreferencePolicyTest.kt 삭제. 동등 검증이 MemberContentPreferenceServiceTest, MemberContentPreferenceIntegrationTest에 이미 존재하며, 부족한 항목은 MemberContentPreferenceServiceTest로 이전한다.

6. 기능 요구사항

ID 상태 요구사항 수용 기준 계약/Goal 연결
DEPREM-001 확정 MemberContentPreferenceService에 isAdultVisibleForQuery(member, isAdultContentVisible)를 추가해 resolveCountryCode(member) + calculateIsAdultForQuery(...) 조합을 단일 메서드로 제공한다. KR + auth == null이면 false, KR + auth != null이면 전달값, 비KR이면 전달값. 회원 ID 강제 매핑이 접속 국가 헤더보다 우선한다. P1-T1
DEPREM-002 확정 production 12개 파일의 isAdultVisibleByPolicy(...) 27곳을 memberContentPreferenceService.isAdultVisibleForQuery(...)로 치환하고 MemberContentPreferenceService를 생성자 주입한다. 각 파일에서 isAdultVisibleByPolicy import와 호출이 사라지고, isAdult 계산 결과가 기존과 동일하다. P2-T1~P2-T4
DEPREM-003 확정 MemberContentPreferencePolicy.kt와 MemberContentPreferencePolicyTest.kt를 제거한다. `isAdultVisibleByPolicy resolveCountryCodeByPolicy검색 결과가src/main/kotlin, src/test/kotlin`에서 0건이다.
DEPREM-004 확정 Deprecated 테스트에서 제거되는 검증 항목 중 정식 경로 테스트에 없는 것은 MemberContentPreferenceServiceTest로 이전한다. 헤더 공백/소문자 정규화(" us " → US), 국가 컨텍스트 없음 → KR fallback, 비KR에서 저장 countryCode 무시 검증이 정식 경로 테스트에 존재한다. P3-T1
DEPREM-005 확정 기존 legacy 콘텐츠 조회 동작을 회귀 없이 유지한다. AudioContentServiceTest 등 영향 범위 테스트와 member/contentpreference 테스트가 모두 통과하고 ktlint가 통과한다. P3-GATE

7. API 계약

  • 변경되는 공개 request/response 필드는 없다.
  • 신규 message key, 신규 endpoint는 없다.
  • 각 서비스의 public 메서드 시그니처(isAdultContentVisible 파라미터 포함)는 유지한다. 변경되는 것은 생성자 의존성뿐이다.

8. 보안과 데이터 취급

  • 접속 국가 판정은 계속 CloudFront-Viewer-Country 헤더를 CountryInterceptor가 CountryContext에 주입한 값만 사용한다.
  • 성인 콘텐츠 노출은 KR에서 본인인증(member.auth != null)이 있는 경우에만 허용하는 기존 정책을 유지한다.
  • 회원 ID, 이메일 등 개인정보는 로그에 추가하지 않는다.

9. 리스크

ID 리스크 대응
RISK-001 CountryContext는 @RequestScope이므로 비웹 스레드에서 호출되면 예외가 발생할 수 있다. Deprecated 함수는 RequestContextHolder가 없을 때 KR로 fallback했다. 대상 12개 서비스가 controller 요청 경로에서만 호출되는지 Phase 2에서 파일별로 확인한다. 스케줄러/비동기 호출 경로가 발견되면 해당 Task를 진행하지 않고 계획을 먼저 갱신한다.
RISK-002 생성자 파라미터 추가로 기존 서비스 테스트가 컴파일 실패할 수 있다. 해소됨. AudioContentServiceTest 1곳만 영향받아 mock 주입으로 해결했다.
RISK-003 순환 의존 발생 가능성. MemberContentPreferenceService는 MemberContentPreferenceRepository, MemberRepository, CountryContext, CacheManager만 의존하므로 콘텐츠 서비스와 순환이 발생하지 않음을 확인했다.

10. 성공 기준

  • MemberContentPreferenceService.isAdultVisibleForQuery(...)가 기존 isAdultVisibleByPolicy(...)와 동일한 판정을 한다. (DEPREM-001)
  • production 12개 파일 27곳이 모두 정식 경로를 호출한다. (DEPREM-002)
  • MemberContentPreferencePolicy.kt, MemberContentPreferencePolicyTest.kt가 제거되고 참조가 0건이다. (DEPREM-003)
  • Deprecated 테스트의 고유 검증이 MemberContentPreferenceServiceTest에 남아 있다. (DEPREM-004)
  • 영향 범위 focused test와 ktlint가 통과한다. (DEPREM-005)

11. Decision Log

기존 성공 기준 체크박스는 당시 완료 기록으로 유지한다. 후속 리뷰에서 DEPREM-004의 저장 국가 충돌 검증 누락을 확인했으며, plan-task.md의 P3-R1 완료 전까지 해당 검증 보강은 미완료다.

일시 결정 근거
2026-09-17 Deprecated 함수 제거를 위해 production 호출부를 모두 마이그레이션하는 방식(선택지 3)을 택한다. 사용자 선택. 테스트만 정식 경로로 바꾸면 production에서 쓰이는 Deprecated 경로가 커버리지 0이 되므로 부적절
2026-09-17 호출부 1:1 치환을 위해 MemberContentPreferenceService에 isAdultVisibleForQuery(member, isAdultContentVisible) wrapper를 추가한다. 호출부마다 resolveCountryCode(...) + calculateIsAdultForQuery(...) 2단계를 반복하면 중복이 27곳으로 늘어남
2026-09-17 MemberContentPreferencePolicyTest는 삭제하고 고유 검증만 MemberContentPreferenceServiceTest로 이전한다. 5개 검증 중 대부분이 정식 경로 테스트와 중복이며, 함수 제거 후에는 테스트 대상이 사라짐
2026-09-17 Phase를 서비스 묶음 단위로 나눠 진행한다. 27곳 동시 변경 시 실패 원인 추적이 어려움. 저장소 규칙("작은 단위로 안전하게 수정") 준수
2026-09-17 RISK-001은 해소로 판정한다. 치환한 27곳이 모두 controller 진입 조회 경로임을 파일별로 확인했고, 스케줄러(Recommendation/Ranking/ChargeEvent)는 이 경로를 호출하지 않음
  • 2026-09-17 사용자 확정: JP 강제 매핑에서 2L을 제거한 것은 의도된 정책 변경이다. REV-001은 결함 판정을 철회하며 2L을 복원하지 않는다. 나머지 매핑은 유지한다.
  • 2026-09-17 후속 계획: REV-002의 저장 국가 무시 검증을 P3-R1로 추가한다. 이번 작업은 문서 갱신이며 테스트 구현·실행은 하지 않는다.

12. 열린 질문

  • 없음. Phase 2에서 비웹 호출 경로(RISK-001)가 발견되면 계획을 먼저 갱신하고 사용자에게 확인한다.