docs(admin-live): 추천 크리에이터 lazy 수정 계획을 추가한다

This commit is contained in:
2026-06-29 13:19:00 +09:00
parent 235d0c8389
commit 1d9f7f0fb6
2 changed files with 196 additions and 0 deletions

View File

@@ -0,0 +1,114 @@
# 관리자 라이브 추천 크리에이터 배너 LazyInitializationException 수정 Implementation Plan
> **For agentic workers:** 각 task는 TDD 기준으로 RED 실패 확인 후 GREEN 최소 구현을 진행한다. 구현 완료 즉시 체크박스와 검증 기록을 갱신한다.
**Goal:** `spring.jpa.open-in-view=false` 환경에서 관리자 라이브 추천 크리에이터 배너 목록 조회가 `RecommendLiveCreatorBanner.creator` lazy proxy 접근 때문에 실패하지 않게 한다.
**Architecture:** 기존 관리자 API 응답 DTO와 URL은 유지한다. `AdminLiveService.getRecommendCreator(...)`에 read-only 트랜잭션 경계를 적용하고, 그 경계 안에서 배너 조회와 `GetAdminRecommendCreatorResponse` 생성을 완료해 lazy proxy 접근을 안전하게 처리한다. 등록/수정/정렬/라이브 취소 쓰기 메서드의 기존 메서드 레벨 `@Transactional`은 유지한다.
**Tech Stack:** Kotlin, Spring Boot 2.7.14, Java 17, Spring Data JPA, QueryDSL, JUnit 5, Gradle Wrapper
---
## 0. 구현 전 확정 사항
- API 응답 스키마는 변경하지 않는다.
- OSIV 설정을 켜지 않는다.
- `RecommendLiveCreatorBanner.creator`를 eager로 바꾸지 않는다.
- 사용자용 라이브 추천 크리에이터 조회 흐름은 변경하지 않는다.
- QueryDSL projection/fetch join 전면 개편은 이번 범위에서 제외한다.
- 원인 확인:
- `AdminLiveRoomQueryRepository.getRecommendCreatorList(...)``RecommendLiveCreatorBanner` 엔티티를 반환한다.
- `RecommendLiveCreatorBanner.creator`는 lazy association이다.
- `AdminLiveService.getRecommendCreator(...)``it.creator!!.id/nickname`을 읽는다.
- `AdminLiveService.getRecommendCreator(...)`에 조회 트랜잭션 경계가 없어 OSIV off 환경에서는 lazy proxy 초기화가 실패할 수 있다.
---
## 1. 파일 구조 계획
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveService.kt`
- `getRecommendCreator(pageable: Pageable)``@Transactional(readOnly = true)`를 추가한다.
- 기존 쓰기 메서드의 메서드 레벨 `@Transactional`은 유지한다.
- DTO 변환, 이미지 URL 조합, 시간 포맷, 정렬 흐름은 변경하지 않는다.
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveServiceIntegrationTest.kt`
- OSIV off JPA 환경에서 서비스가 관리자 라이브 추천 크리에이터 배너 목록 response를 생성할 때 lazy 초기화 예외가 발생하지 않는지 검증한다.
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveControllerIntegrationTest.kt`
- 실제 Spring Context, `MockMvc`, JPA fixture로 `/admin/live/recommend-creator` API 응답과 관리자 권한을 검증한다.
- Verify: `src/test/resources/application.yml`
- `spring.jpa.open-in-view: false` 테스트 설정을 그대로 사용한다.
---
### Phase 1: LazyInitializationException 재현 테스트
- [x] **Task 1.1: 서비스 통합 실패 테스트 작성**
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveServiceIntegrationTest.kt`
- RED: `@SpringBootTest(properties = ["cloud.aws.cloud-front.host=https://cdn.test", ...])` 통합 테스트를 추가한다.
- RED: `@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class])`를 추가해 기존 Spring Boot 통합 테스트 Redis 패턴을 따른다.
- RED: 테스트 클래스에는 `@Transactional`을 붙이지 않아 테스트 트랜잭션이 lazy 문제를 가리지 않게 한다.
- RED: `TransactionTemplate` 안에서 `Member(role = CREATOR)``RecommendLiveCreatorBanner`를 저장하고 `EntityManager.flush()`, `EntityManager.clear()`로 영속성 컨텍스트를 비운다.
- RED: `service.getRecommendCreator(PageRequest.of(0, 20))`를 호출해 `totalCount`, `creatorId`, `creatorNickname`, `image`, `startDate`, `endDate`, `isAdult`를 검증한다.
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.admin.live.AdminLiveServiceIntegrationTest`
- 기대 결과: production code 수정 전에는 `LazyInitializationException`으로 테스트가 실패한다.
- 검증 기록: production code 수정 전 `./gradlew test --tests kr.co.vividnext.sodalive.admin.live.AdminLiveServiceIntegrationTest`를 실행했고, `AdminLiveServiceIntegrationTest.kt:39`에서 `org.hibernate.LazyInitializationException`으로 실패해 RED를 확인했다.
---
### Phase 2: 서비스 조회 트랜잭션 보강
- [x] **Task 2.1: 추천 크리에이터 배너 목록 조회 read-only 트랜잭션 적용**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveService.kt`
- GREEN: `getRecommendCreator(pageable: Pageable)``@Transactional(readOnly = true)`를 추가한다.
- GREEN: 기존 DTO 변환 로직은 유지하고, 추가적인 fetch 전략 변경이나 응답 구조 변경을 하지 않는다.
- 통과 확인: `./gradlew test --tests kr.co.vividnext.sodalive.admin.live.AdminLiveServiceIntegrationTest`
- 기대 결과: `BUILD SUCCESSFUL`
- REFACTOR: 불필요한 import/format 변경이 생기지 않았는지 확인한다.
- 검증 기록: `getRecommendCreator(pageable: Pageable)`에만 `@Transactional(readOnly = true)`를 추가한 뒤 `./gradlew test --tests kr.co.vividnext.sodalive.admin.live.AdminLiveServiceIntegrationTest`를 재실행했고, `BUILD SUCCESSFUL`을 확인했다.
---
### Phase 3: 실제 Spring Context 기반 관리자 목록 API 검증
- [x] **Task 3.1: 관리자 목록 API 통합 테스트 작성**
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveControllerIntegrationTest.kt`
- RED/GREEN: 서비스 수정 후 실제 API 경로가 같은 응답을 반환하는 회귀 테스트를 작성한다.
- GREEN: `@SpringBootTest(properties = ["cloud.aws.cloud-front.host=https://cdn.test", ...])``@AutoConfigureMockMvc`를 사용한다.
- GREEN: `@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class])`를 추가한다.
- GREEN: 테스트 데이터 생성은 `TransactionTemplate` 안에서 수행하고 `EntityManager.flush()`, `EntityManager.clear()`로 영속성 컨텍스트를 비운다.
- GREEN: `MockMvc``GET /admin/live/recommend-creator?page=0&size=20`을 호출하고 `with(user("admin").roles("ADMIN"))`로 관리자 권한을 부여한다.
- GREEN: `$.success = true`, `$.data.totalCount = 1`, `$.data.recommendCreatorList[0].creatorNickname`, `$.data.recommendCreatorList[0].image`, `$.data.recommendCreatorList[0].startDate`, `$.data.recommendCreatorList[0].endDate`, `$.data.recommendCreatorList[0].isAdult`를 검증한다.
- 통과 확인: `./gradlew test --tests kr.co.vividnext.sodalive.admin.live.AdminLiveControllerIntegrationTest`
- 기대 결과: `BUILD SUCCESSFUL`
- 검증 기록: `./gradlew test --tests kr.co.vividnext.sodalive.admin.live.AdminLiveControllerIntegrationTest`를 실행했고, 관리자 권한 MockMvc `GET /admin/live/recommend-creator?page=0&size=20` 응답 검증이 `BUILD SUCCESSFUL`로 통과했다.
- [x] **Task 3.2: 관련 검증 실행 및 문서 기록**
- Verify: `./gradlew test --tests kr.co.vividnext.sodalive.admin.live.AdminLiveServiceIntegrationTest`
- Verify: `./gradlew test --tests kr.co.vividnext.sodalive.admin.live.AdminLiveControllerIntegrationTest`
- Verify: `./gradlew test --tests kr.co.vividnext.sodalive.admin.live.AdminLiveServiceTest`
- Verify: `./gradlew --no-daemon ktlintCheck`
- Verify: `./gradlew --no-daemon tasks --all`
- 문서 기록: 각 task 아래에 실행 명령, 결과, 검증 이유를 한국어로 누적한다.
- 기대 결과: 모든 명령이 `BUILD SUCCESSFUL`로 종료된다.
- 검증 기록:
- `./gradlew test --tests kr.co.vividnext.sodalive.admin.live.AdminLiveServiceIntegrationTest`: `BUILD SUCCESSFUL`. 서비스 트랜잭션 경계 안에서 lazy creator 접근과 DTO 변환이 완료되는지 재검증했다.
- `./gradlew test --tests kr.co.vividnext.sodalive.admin.live.AdminLiveControllerIntegrationTest`: 병렬 Gradle 실행 중 XML test result 파일 쓰기 충돌로 1회 실패했으나, 순차 재실행에서 `BUILD SUCCESSFUL`. MockMvc 관리자 API 응답 surface를 검증했다.
- `./gradlew test --tests kr.co.vividnext.sodalive.admin.live.AdminLiveServiceTest`: `BUILD SUCCESSFUL`. 기존 관리자 라이브 서비스 단위 테스트 회귀를 확인했다.
- `./gradlew --no-daemon ktlintCheck`: `BUILD SUCCESSFUL`. Kotlin 포맷/스타일 위반이 없음을 확인했다.
- `./gradlew --no-daemon tasks --all`: `BUILD SUCCESSFUL`. 문서에 기재된 Gradle 명령 유효성을 확인했다.
---
## 검증 기록
- 구현 전 문서 작성 단계에서는 아직 테스트를 실행하지 않았다.
- 2026-06-29: 문서 변경 후 명령 유효성 확인을 위해 `./gradlew --no-daemon tasks --all`을 실행했다.
- 1차 실행: sandbox에서 `/Users/klaus/.gradle/wrapper/dists/.../gradle-8.1.1-bin.zip.lck` 접근이 차단되어 실패했다.
- escalated 재실행: `BUILD SUCCESSFUL`을 확인했다.
- 2026-06-29: 구현 후 `AdminLiveServiceIntegrationTest`, `AdminLiveControllerIntegrationTest`, `AdminLiveServiceTest`, `ktlintCheck`, `tasks --all`을 실행했고 모두 최종적으로 `BUILD SUCCESSFUL`을 확인했다.
- 2026-06-29: 코드 리뷰 및 재검증 요청에 따라 현재 워크트리 기준으로 관련 검증을 재실행했다.
- `./gradlew test --tests kr.co.vividnext.sodalive.admin.live.AdminLiveServiceIntegrationTest`: `BUILD SUCCESSFUL`
- `./gradlew test --tests kr.co.vividnext.sodalive.admin.live.AdminLiveControllerIntegrationTest`: `BUILD SUCCESSFUL`
- `./gradlew test --tests kr.co.vividnext.sodalive.admin.live.AdminLiveServiceTest`: `BUILD SUCCESSFUL`
- `./gradlew --no-daemon ktlintCheck`: `BUILD SUCCESSFUL`
- `./gradlew --no-daemon tasks --all`: sandbox에서 wrapper lock 접근 제한으로 1차 실패했고, escalated 재실행에서 `BUILD SUCCESSFUL`

View File

@@ -0,0 +1,82 @@
# PRD: 관리자 라이브 추천 크리에이터 배너 LazyInitializationException 수정
## 1. Overview
`spring.jpa.open-in-view=false` 환경에서 관리자 라이브 추천 크리에이터 배너 목록 조회 시 `RecommendLiveCreatorBanner.creator` lazy proxy 접근으로 발생하는 `LazyInitializationException`을 방지한다.
---
## 2. Problem
- `AdminLiveController.getRecommendCreatorBanner(...)``AdminLiveService.getRecommendCreator(...)`가 만든 관리자 추천 크리에이터 배너 목록 응답을 반환한다.
- `AdminLiveRoomQueryRepository.getRecommendCreatorList(...)``RecommendLiveCreatorBanner` 엔티티를 조회한다.
- `RecommendLiveCreatorBanner.creator``@ManyToOne(fetch = FetchType.LAZY)`이다.
- `AdminLiveService.getRecommendCreator(...)`는 조회 결과를 응답 DTO로 변환하며 `it.creator!!.id`, `it.creator!!.nickname`을 읽는다.
- 현재 `AdminLiveService.getRecommendCreator(...)`에는 조회 트랜잭션 경계가 없어, repository 호출 이후 영속성 컨텍스트가 닫힌 상태에서 `creator` lazy proxy 초기화를 시도하면 `org.hibernate.LazyInitializationException: could not initialize proxy [kr.co.vividnext.sodalive.member.Member#289] - no Session`이 발생할 수 있다.
---
## 3. Goals
- OSIV off 환경에서도 관리자 라이브 추천 크리에이터 배너 목록 조회가 예외 없이 응답된다.
- 기존 관리자 추천 크리에이터 배너 목록 API의 URL과 응답 스키마를 변경하지 않는다.
- 기존 이미지 URL 조합, 기간 표시 포맷, 정렬 기준을 유지한다.
- 실패 재현 테스트를 먼저 작성하고, 최소 수정으로 통과시킨다.
---
## 4. Non-Goals
- OSIV 설정을 다시 켜지 않는다.
- `RecommendLiveCreatorBanner.creator` fetch 전략을 전역 eager로 바꾸지 않는다.
- 관리자 라이브 추천 크리에이터 배너 등록/수정/정렬 API 동작을 변경하지 않는다.
- 사용자용 라이브 추천 크리에이터 조회 정책을 변경하지 않는다.
- QueryDSL projection 기반으로 관리자 라이브 목록 조회 전체를 재설계하지 않는다.
---
## 5. Target Users
- 관리자: 관리자 화면에서 라이브 추천 크리에이터 배너 목록을 조회하고 정렬/수정 대상을 확인하는 사용자
- 운영자: OSIV off 운영 환경에서도 관리자 배너 목록이 안정적으로 열리기를 기대하는 사용자
---
## 6. User Stories
- 관리자는 추천 크리에이터가 연결된 배너 목록을 조회할 때 서버 오류를 만나지 않아야 한다.
- 관리자는 기존과 동일한 응답 필드로 크리에이터 ID, 닉네임, 이미지, 시작/종료 시간, 성인 여부를 확인할 수 있어야 한다.
- 운영자는 OSIV off 설정을 유지하면서 lazy 초기화 예외를 회피할 수 있어야 한다.
---
## 7. Core Features
### Feature A. 관리자 라이브 추천 크리에이터 배너 조회 트랜잭션 보강
#### Requirements
- `AdminLiveService.getRecommendCreator(...)`는 read-only 트랜잭션 경계 안에서 배너 조회와 `GetAdminRecommendCreatorResponse` 변환을 완료한다.
- `AdminLiveService.getRecommendCreator(...)`는 기존처럼 `repository.getRecommendCreatorTotalCount()``repository.getRecommendCreatorList(pageable)`를 사용한다.
- `GetAdminRecommendCreatorResponse.totalCount`, `recommendCreatorList[].id`, `image`, `creatorId`, `creatorNickname`, `startDate`, `endDate`, `isAdult` 필드는 유지한다.
- `startDate`, `endDate`는 기존처럼 UTC 저장값을 Asia/Seoul 기준 `yyyy-MM-dd HH:mm` 문자열로 변환한다.
- 이미지 경로 조합은 기존처럼 `"$coverImageHost/${banner.image}"` 형식을 유지한다.
#### Edge Cases
- 배너가 없으면 `totalCount = 0`, `recommendCreatorList = []`를 반환한다.
- 배너의 `creator`가 lazy proxy 상태로 조회되어도 서비스 트랜잭션 안에서 DTO 변환이 완료된다.
- 페이지 파라미터에 따른 offset/limit와 기존 정렬(`orders asc`, `id desc`)을 유지한다.
---
## 8. Technical Constraints
- Kotlin + Spring Boot 2.7.14 + Java 17 + Spring Data JPA 기준으로 구현한다.
- 테스트 환경의 `spring.jpa.open-in-view=false` 설정을 유지한다.
- 변경 범위는 관리자 라이브 추천 크리에이터 배너 목록 조회 흐름과 해당 테스트로 제한한다.
- lazy proxy 재현은 실제 JPA 환경에서 확인할 수 있도록 서비스 통합 테스트로 검증한다.
- 관리자 목록 API 응답은 실제 Spring Context, `MockMvc`, JPA fixture를 연결한 통합 테스트로 검증한다.
---
## 9. Metrics
- `AdminLiveServiceIntegrationTest`에서 OSIV off 조건의 관리자 라이브 추천 크리에이터 배너 목록 응답 생성 테스트가 통과한다.
- `AdminLiveControllerIntegrationTest`에서 관리자 라이브 추천 크리에이터 배너 목록 API 테스트가 통과한다.
- 관련 단일 테스트와 `ktlintCheck`가 통과한다.
---
## 10. Open Questions
- 없음. 해결 범위는 기존 채팅 배너 LazyInitializationException 수정과 같은 패턴의 관리자 목록 조회 안정화로 한정한다.