docs(chat): 관리자 채팅 배너 lazy 수정 계획을 추가한다

This commit is contained in:
2026-06-29 11:24:11 +09:00
parent 5c7e8dae0a
commit 9a241f7137
2 changed files with 228 additions and 0 deletions

View File

@@ -0,0 +1,79 @@
# PRD: 관리자 채팅 배너 LazyInitializationException 수정
## 1. Overview
`spring.jpa.open-in-view=false` 환경에서 관리자 채팅 배너 목록 조회 시 `ChatCharacterBanner.chatCharacter` lazy proxy 접근으로 발생하는 `LazyInitializationException`을 방지한다.
---
## 2. Problem
- `AdminChatBannerController.getBannerList(...)``bannerService.getActiveBanners(...)``Page<ChatCharacterBanner>`를 받은 뒤 컨트롤러에서 `ChatCharacterBannerResponse.from(...)`으로 응답을 만든다.
- `ChatCharacterBanner.chatCharacter``@ManyToOne(fetch = FetchType.LAZY)`이다.
- `ChatCharacterBannerResponse.from(...)``banner.chatCharacter.id`, `banner.chatCharacter.name`을 읽는다.
- `spring.jpa.open-in-view=false` 환경에서는 서비스/리포지토리 조회 후 영속성 컨텍스트가 닫힌 상태에서 컨트롤러가 lazy proxy를 초기화하려 하므로 `org.hibernate.LazyInitializationException: could not initialize proxy [kr.co.vividnext.sodalive.chat.character.ChatCharacter#7] - no Session`이 발생할 수 있다.
---
## 3. Goals
- OSIV off 환경에서도 관리자 채팅 배너 목록 조회가 예외 없이 응답된다.
- 사용자가 요청한 방향대로 `bannerService.getActiveBanners(...)` 안에서 관리자 목록 response를 생성한다.
- 기존 관리자 채팅 배너 목록 API의 응답 스키마를 변경하지 않는다.
- 배너 언어 라벨을 캐릭터명 뒤에 붙이는 기존 동작을 유지한다.
- 실패 재현 테스트를 먼저 작성하고, 최소 수정으로 통과시킨다.
---
## 4. Non-Goals
- OSIV 설정을 다시 켜지 않는다.
- `ChatCharacterBanner.chatCharacter` fetch 전략을 전역 eager로 바꾸지 않는다.
- 관리자 채팅 배너 목록 API의 URL, 요청 파라미터, 응답 필드를 변경하지 않는다.
- 배너 등록/수정/삭제/정렬 API 동작을 변경하지 않는다.
- 공개 사용자용 배너 조회(`getDisplayBanners`) 응답 구조를 변경하지 않는다.
- QueryDSL/projection 기반으로 배너 조회 전체를 재설계하지 않는다.
---
## 5. Target Users
- 관리자: 관리자 화면에서 채팅 캐릭터 배너 목록을 조회하고 정렬/수정 대상을 확인하는 사용자
- 운영자: OSIV off 운영 환경에서도 관리자 배너 목록이 안정적으로 열리기를 기대하는 사용자
---
## 6. User Stories
- 관리자는 채팅 캐릭터가 연결된 활성 배너 목록을 조회할 때 서버 오류를 만나지 않아야 한다.
- 관리자는 한국어/영어/일본어 배너가 섞여 있어도 캐릭터명 뒤에 언어 라벨이 붙은 목록을 확인할 수 있어야 한다.
- 운영자는 OSIV off 설정을 유지하면서 lazy 초기화 예외를 회피할 수 있어야 한다.
---
## 7. Core Features
### Feature A. 관리자 채팅 배너 목록 응답 생성 위치 이동
#### Requirements
- `ChatCharacterBannerService.getActiveBanners(...)`는 관리자 목록 응답인 `ChatCharacterBannerListPageResponse`를 생성해 반환한다.
- `ChatCharacterBannerService`는 클래스 레벨 `@Transactional(readOnly = true)`로 조회 기본 트랜잭션을 제공하고, `getActiveBanners(...)`는 그 경계 안에서 배너 조회와 `ChatCharacterBannerResponse.from(...)` 변환을 완료한다.
- `ChatCharacterBannerResponse.from(...)` 호출 시 `appendLanguageToCharacterName = true`를 유지한다.
- `AdminChatBannerController.getBannerList(...)`는 pageable 생성 후 서비스가 만든 response를 그대로 `ApiResponse.ok(...)`로 감싼다.
- 기존 `ChatCharacterBannerListPageResponse.totalCount`, `content[].id`, `content[].imagePath`, `content[].characterId`, `content[].characterName` 필드는 유지한다.
#### Edge Cases
- 활성 배너가 없으면 `totalCount = 0`, `content = []`를 반환한다.
- 배너 언어가 `KO`, `EN`, `JA`인 경우 기존처럼 각각 `한국어`, `영어`, `일본어` 라벨을 캐릭터명 뒤에 붙인다.
- 이미지 경로 조합은 기존처럼 `"$imageHost/${banner.imagePath}"` 형식을 유지한다.
---
## 8. Technical Constraints
- Kotlin + Spring Boot 2.7.14 + Java 17 + Spring Data JPA 기준으로 구현한다.
- 테스트 환경의 `spring.jpa.open-in-view=false` 설정을 유지한다.
- `ChatCharacterBannerService` 클래스 레벨에 `@Transactional(readOnly = true)`를 사용하고, 기존 쓰기 메서드의 메서드 레벨 `@Transactional`은 유지한다.
- 변경 범위는 관리자 채팅 배너 목록 조회 흐름과 해당 테스트로 제한한다.
- lazy proxy 재현은 실제 JPA 환경에서 확인할 수 있도록 서비스 통합 테스트로 검증한다.
- 관리자 목록 API 응답은 mock 기반 컨트롤러 테스트가 아니라 실제 Spring Context, `MockMvc`, JPA fixture를 연결한 통합 테스트로 검증한다.
---
## 9. Metrics
- `ChatCharacterBannerServiceIntegrationTest`에서 OSIV off 조건의 관리자 배너 목록 응답 생성 테스트가 통과한다.
- `AdminChatBannerControllerIntegrationTest`의 관리자 배너 목록 API 테스트가 통과한다.
- 관련 단일 테스트와 `ktlintCheck`가 통과한다.