Test #451
@@ -0,0 +1,299 @@
|
||||
# 성인 콘텐츠 노출 정책 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 조회를 추가하지 않는다.
|
||||
|
||||
- [x] **RED:** `MemberContentPreferenceServiceTest`에 `isAdultVisibleForQuery` 판정 테스트 4건(KR 미인증/KR 인증/비KR/강제 매핑)을 작성한다.
|
||||
- [x] **RED 확인:** `MemberContentPreferenceServiceTest` 실행 결과 `Unresolved reference: isAdultVisibleForQuery` 컴파일 실패를 확인했다.
|
||||
- [x] **GREEN:** `MemberContentPreferenceService`에 `isAdultVisibleForQuery`를 추가했다.
|
||||
- [x] **GREEN 확인:** `MemberContentPreferenceServiceTest` 25/25 통과.
|
||||
- [x] **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`)는 유지한다.
|
||||
|
||||
- [x] **RED:** 7개 서비스가 각각 대응 `*Controller`에서만 주입됨을 확인해 `RISK-001` 위반 경로가 없음을 검증했다.
|
||||
- [x] **GREEN:** 7개 파일의 호출부와 생성자를 치환했다.
|
||||
- [x] **GREEN 확인:** 테스트 컴파일과 `src/test/kotlin/kr/co/vividnext/sodalive/content` 42/42 통과로 확인했다.
|
||||
- [x] **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`과 동일한 치환 규칙을 적용한다.
|
||||
|
||||
- [x] **RED:** 3개 서비스의 치환 대상이 모두 `member`를 받는 웹 조회 메서드임을 확인했다.
|
||||
- [x] **GREEN:** 3개 파일을 치환했다.
|
||||
- [x] **GREEN 확인:** 테스트 컴파일 성공 및 `content` 패키지 회귀 통과.
|
||||
- [x] **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으로 주입한다.
|
||||
|
||||
- [x] **RED:** `AudioContentServiceTest.kt:118`에서 `No value passed for parameter 'memberContentPreferenceService'` 컴파일 실패를 확인했다.
|
||||
- [x] **GREEN:** `getDetail`, `getLatestCreatorAudioContent`, `getAudioContentList` 3곳을 치환하고 테스트에 mock을 주입했다.
|
||||
- [x] **GREEN 확인:** `AudioContentServiceTest` 16/16 통과.
|
||||
- [x] **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`과 동일한 치환 규칙을 적용한다.
|
||||
|
||||
- [x] **RED:** 테스트 컴파일을 실행해 `ContentSeriesService`를 직접 생성하는 테스트가 없어 추가 수정이 필요 없음을 확인했다.
|
||||
- [x] **GREEN:** 5곳(`getSeriesList`, `getSeriesListByGenre`, `getSeriesDetail`, `getSeriesContentList`, `getRecommendSeriesList`)을 치환했다.
|
||||
- [x] **GREEN 확인:** 테스트 컴파일 성공 및 Spring 컨텍스트 로딩 테스트 통과.
|
||||
- [x] **REFACTOR:** import 정리 후 ktlint 통과.
|
||||
|
||||
### Phase 2 Gate
|
||||
|
||||
**Goal 실행 `P2-GATE`:** production에서 Deprecated 호출이 0건임을 확인한다.
|
||||
|
||||
- [x] `isAdultVisibleByPolicy|resolveCountryCodeByPolicy` 검색 결과가 `src/main/kotlin`에서 0건이다.
|
||||
- [x] 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` 무시.
|
||||
- 이미 정식 경로 테스트에 존재하는 검증은 중복 추가하지 않는다.
|
||||
|
||||
- [x] **RED:** 로그인 회원의 헤더 공백/소문자 정규화와 KR fallback 검증을 `MemberContentPreferenceServiceTest`에 추가했다. 나머지 항목(비로그인 정규화, 저장 `countryCode` 미사용)은 기존 테스트가 이미 커버함을 확인해 중복 추가하지 않았다.
|
||||
- [x] **GREEN:** `MemberContentPreferencePolicy.kt`와 `MemberContentPreferencePolicyTest.kt`를 삭제했다.
|
||||
- [x] **GREEN 확인:** `member/contentpreference` 36/36 통과.
|
||||
- [x] **REFACTOR:** ktlint 통과 및 잔여 참조 0건 확인.
|
||||
|
||||
### Phase 3 Gate
|
||||
|
||||
**Goal 실행 `P3-GATE`:** 전체 정리 상태를 확인한다.
|
||||
|
||||
- [x] `isAdultVisibleByPolicy|resolveCountryCodeByPolicy` 참조가 `src/main/kotlin`, `src/test/kotlin`에서 0건이다.
|
||||
- [x] `member/contentpreference` 테스트와 Phase 2 영향 범위 테스트가 통과한다.
|
||||
- [x] `./gradlew ktlintMainSourceSetCheck ktlintTestSourceSetCheck`가 통과한다.
|
||||
- [x] 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를 재사용한다.
|
||||
|
||||
```kotlin
|
||||
@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))
|
||||
}
|
||||
```
|
||||
|
||||
- [x] **누락 확인:** 삭제된 정책 테스트와 비교해 저장 KR·요청 US·미인증 조건이 기존 정식 테스트에 없음을 확인했다(TDD 예외 적용).
|
||||
- [x] **최소 구현:** 위 테스트 1건만 추가했다. 강제 매핑이 없는 ID 2401을 사용하고 production은 변경하지 않았다.
|
||||
- [x] **소스 검토:** 국가 결과 US 및 노출 전달값 true/false 보존 assertion을 확인했다. 저장 국가 KR을 우선하면 국가 assertion과 `true` 전달값 assertion이 실패한다.
|
||||
- [ ] **실행 검증:** 자동 테스트 실행이 허용되는 후속 단계에서 아래 focused test와 ktlint를 실행하고 결과를 기록한다. 미실행이면 완료 표시하지 않는다.
|
||||
- [x] **기록 갱신:** P3-T1의 기존 완료 기록을 유지하고, REV-002 보강 결과와 실행 대기 상태를 본 Task와 리뷰에 누적했다.
|
||||
|
||||
**후속 실행용 명령(이번에는 미실행):** 저장소와 호환되는 JDK 환경에서 실행한다.
|
||||
|
||||
```bash
|
||||
./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 전체 소스셋 검사를 실행했다.
|
||||
@@ -0,0 +1,132 @@
|
||||
# 성인 콘텐츠 노출 정책 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건이다. | `P3-T1` |
|
||||
| `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. 성공 기준
|
||||
|
||||
- [x] `MemberContentPreferenceService.isAdultVisibleForQuery(...)`가 기존 `isAdultVisibleByPolicy(...)`와 동일한 판정을 한다. (`DEPREM-001`)
|
||||
- [x] production 12개 파일 27곳이 모두 정식 경로를 호출한다. (`DEPREM-002`)
|
||||
- [x] `MemberContentPreferencePolicy.kt`, `MemberContentPreferencePolicyTest.kt`가 제거되고 참조가 0건이다. (`DEPREM-003`)
|
||||
- [x] Deprecated 테스트의 고유 검증이 `MemberContentPreferenceServiceTest`에 남아 있다. (`DEPREM-004`)
|
||||
- [x] 영향 범위 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`)가 발견되면 계획을 먼저 갱신하고 사용자에게 확인한다.
|
||||
@@ -0,0 +1,65 @@
|
||||
# 문서 대비 구현 리뷰
|
||||
|
||||
## 리뷰 정보와 범위
|
||||
|
||||
- 일자: 2026-09-17
|
||||
- 기준: HEAD `8c87286acd69174f6024d30a267458e838e35cf5` 및 리뷰 시작 시 존재한 미커밋 변경
|
||||
- 요구사항: `../prd.md`, `../plan-task.md`의 Phase 1~3
|
||||
- 방법: 문서, production 코드, 테스트 소스, 현재 diff와 호출 경로 정적 대조
|
||||
- 사용자 지시에 따라 자동 테스트·빌드·ktlint·애플리케이션 실행은 하지 않았다. 기존 실행 성공 기록을 재검증한 것으로 취급하지 않는다.
|
||||
- 구현 및 기존 PRD/계획은 수정하지 않았다. 아래 판정은 현재 작업 트리에 대한 것이며 변경 작성자나 의도를 추정하지 않는다.
|
||||
|
||||
## 발견 사항
|
||||
|
||||
### REV-001 — 강제 JP 매핑 값이 변경됨
|
||||
|
||||
- 심각도: High / 상태: 확정
|
||||
- 요구사항: PRD §3 기존 동작 유지, §4 강제 매핑 값 변경 금지; 계획 P3-T1의 resolver 변경 제외
|
||||
- 근거: `src/main/kotlin/kr/co/vividnext/sodalive/member/contentpreference/MemberContentPreferenceCountryResolver.kt:6`에서 `FORCED_JP_MEMBER_IDS`의 `2L`이 제거됐다.
|
||||
- 코드 추적: 회원 ID 2, 인증 없음, 요청 국가 KR 또는 헤더 없음, `isAdultContentVisible=true`이면 기존 JP 판정/성인 노출 true에서 KR 판정/false로 바뀐다.
|
||||
- 관련 테스트도 `MemberContentPreferenceServiceTest.kt:64`, `MemberContentPreferenceIntegrationTest.kt:187`에서 ID 2를 29721로 교체해 기존 매핑 회귀를 검출하지 못한다.
|
||||
- 권장 조치: 문서대로라면 기존 매핑과 해당 테스트 대상을 복원한다. 별도로 의도한 정책 변경이라면 해당 결정과 범위를 문서에 명시해야 한다.
|
||||
|
||||
### REV-002 — 저장 국가 무시 검증이 이전되지 않음
|
||||
|
||||
- 심각도: Medium / 상태: 확정
|
||||
- 요구사항: DEPREM-004, P3-T1
|
||||
- 근거: 삭제된 PolicyTest는 저장 `member.countryCode="KR"`, 요청 국가 US 조건을 검증했다. 현재 `MemberContentPreferenceServiceTest.kt:469`의 해외 노출 테스트와 `:536`의 회원 생성 함수는 저장 국가를 설정하지 않는다. IntegrationTest에도 이 충돌 조건이 없다.
|
||||
- 영향: 현재 resolver는 저장 국가를 참조하지 않아 구현 자체는 맞지만, 문서에서 이전하도록 요구한 고유 회귀 검증이 빠졌다. `plan-task.md:224`의 기존 테스트가 이미 커버한다는 기록은 부정확하다.
|
||||
- 권장 조치: 일반 미인증 회원의 저장 국가를 KR로 설정하고 요청 국가는 US로 두어, 정식 진입점의 국가 US/성인 노출 true를 확인하는 검증을 추가한다.
|
||||
|
||||
### REV-003 — 호출부 합계 오기
|
||||
|
||||
- 심각도: Low / 상태: 확정
|
||||
- 근거: PRD §5.2 표의 합계와 실제 치환은 모두 12개 파일 27곳이다. PRD와 계획에는 반복해서 24곳으로 기록돼 있다.
|
||||
- 영향: 호출 치환 누락은 없으며 문서 수치가 틀렸다.
|
||||
- 권장 조치: 두 문서의 합계를 27곳으로 정정한다.
|
||||
|
||||
## 충족 항목과 한계
|
||||
|
||||
| 항목 | 정적 검토 결과 |
|
||||
|---|---|
|
||||
| DEPREM-001 단일 진입점 | 기존 resolveCountryCode + calculateIsAdultForQuery 재사용, 추가 DB 조회 없음 |
|
||||
| DEPREM-002 호출부 이전 | 12개 파일 27곳 이전, 인자 및 홈의 member=null → false 유지 |
|
||||
| DEPREM-003 정의/참조 제거 | 두 파일 삭제, src/main/kotlin 및 src/test/kotlin의 두 Deprecated 함수 이름 검색 0건 |
|
||||
| DEPREM-004 고유 검증 이전 | 정규화/null 국가값 fallback은 존재, 저장 국가 충돌 검증 누락 |
|
||||
| DEPREM-005 실행 검증 | 사용자 요청에 따라 재실행 제외 |
|
||||
| API 계약 | 기존 공개 메서드 인자와 request/response 스키마 변경 없음 |
|
||||
| 요청 경로/의존성 | 변경된 조회 메서드의 비웹 호출 및 새 순환 의존 발견 없음 |
|
||||
|
||||
국가값 null 검증은 요청 스코프 자체가 없는 상황과 다르다. 새 경로는 요청 스코프와 회원 ID를 요구하므로 모든 입력에서 구 함수와 동등하다고 일반화할 수 없다. 다만 검토한 변경 호출 경로에서 해당 비웹/ID 없는 입력은 확인하지 못해 별도 운영 결함으로 확정하지 않았다.
|
||||
|
||||
## 리뷰 종료 판정과 후속 작업
|
||||
|
||||
요구사항, 테스트 소스, 코드 품질, 보안, 주변 호출 맥락의 검토 결과를 종합했다. 확정 불일치 3건으로 문서와 완전히 일치하는 구현이라고 판정할 수 없다.
|
||||
|
||||
이번 요청은 검토이므로 수정 goal은 실행하지 않았다. 후속 구현 시 기존 완료 기록을 유지하면서 REV-001 매핑 일치, REV-002 고유 검증 이전, REV-003 문서 수치 정정을 별도 회귀 Task로 계획에 추가한다. 자동 검증은 이번 검토에서 수행하지 않았다.
|
||||
|
||||
## 후속 판정 정정 — 2026-09-17
|
||||
|
||||
위 내용은 최초 검토 당시 기록이며, 사용자 확인 이후 현재 판정은 다음과 같다.
|
||||
|
||||
- **REV-001: 결함 판정 철회.** 사용자가 `2L` 제거를 의도한 변경으로 확정했다. PRD·계획에 해당 예외를 반영했으며 매핑과 테스트 대상을 복원하지 않는다.
|
||||
- **REV-002: 구현 완료 / 실행 검증 대기.** `MemberContentPreferenceServiceTest.shouldIgnoreStoredCountryCodeWhenRequestCountryIsNotKr`를 추가해 저장 KR·요청 US·미인증 회원 조건에서 국가 결과 US와 전달값 true/false 보존을 검증한다. production은 변경하지 않았고, 기존 자동 테스트 미실행 지시에 따라 focused test와 ktlint는 실행하지 않았다.
|
||||
- **REV-003: 문서 정정 완료.** PRD·계획의 24곳 표기를 실제 합계 27곳으로 정정했다.
|
||||
- **현재 결론:** `P3-R1` 테스트 구현과 소스 검토는 완료했으며, 남은 항목은 focused test와 ktlint 실행 검증이다.
|
||||
Reference in New Issue
Block a user