성인 콘텐츠 노출 정책 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. 성공 기준
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)가 발견되면 계획을 먼저 갱신하고 사용자에게 확인한다.