docs(admin-member): 회원 목록 lazy 초기화 수정 계획을 추가한다

This commit is contained in:
2026-06-27 07:53:42 +09:00
parent 55abbd2a6d
commit 342c39890e
2 changed files with 171 additions and 0 deletions

View File

@@ -0,0 +1,94 @@
# 관리자 회원 목록 LazyInitializationException 수정 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:executing-plans` 또는 동등한 TDD 절차로 task 단위 구현을 진행한다. 각 단계는 체크박스(`- [ ]`)로 진행 상태를 갱신한다.
**Goal:** `spring.jpa.open-in-view=false` 환경에서 관리자 회원 리스트와 크리에이터 리스트 조회가 `Member.signOutReasons` lazy collection 접근 때문에 실패하지 않게 한다.
**Architecture:** 기존 `AdminMemberService`의 응답 매핑 구조는 유지한다. 서비스 클래스에 read-only 트랜잭션을 기본 적용해 목록 조회와 응답 매핑 전체를 열린 영속성 컨텍스트 안에서 처리한다. 쓰기 메서드는 기존 메서드 레벨 `@Transactional`로 read-only 기본값을 override한다.
**Tech Stack:** Kotlin, Spring Boot 2.7.14, Java 17, Spring Data JPA, QueryDSL, JUnit 5, Gradle Wrapper
---
## 0. 구현 전 확정 사항
- API 응답 스키마는 변경하지 않는다.
- `Member.signOutReasons`를 eager로 바꾸지 않는다.
- OSIV 설정을 켜지 않는다.
- 리포지토리 fetch join이나 projection 전면 개편은 이번 범위에서 제외한다.
- lazy 접근 문제가 확인된 대상 메서드:
- `AdminMemberService.getMemberList(...)`
- `AdminMemberService.searchMember(...)`
- `AdminMemberService.getCreatorList(...)`
- `AdminMemberService.searchCreator(...)`
---
## 1. 파일 구조 계획
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberService.kt`
- 클래스 레벨에 `@Transactional(readOnly = true)`를 추가하고, 기존 쓰기 메서드의 `@Transactional`은 유지한다.
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberServiceTest.kt`
- OSIV off 환경에서 탈퇴 이력이 있는 회원/크리에이터 목록 조회가 예외 없이 응답되는지 검증한다.
- Verify: `src/test/resources/application.yml`
- `spring.jpa.open-in-view: false` 테스트 설정을 그대로 사용한다.
---
### Phase 1: LazyInitializationException 재현 테스트
- [x] **Task 1.1: 관리자 회원/크리에이터 목록 실패 테스트 작성**
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberServiceTest.kt`
- RED: `@SpringBootTest(properties = ["cloud.aws.cloud-front.host=https://cdn.test"])` 통합 테스트를 추가한다.
- RED: 테스트 클래스에는 `@Transactional`을 붙이지 않아 서비스 호출이 테스트 트랜잭션에 의해 가려지지 않게 한다.
- RED: `MemberRole.USER` 회원과 `MemberRole.CREATOR` 회원을 저장하고, 각각 `SignOut`을 저장한다.
- RED: `service.getMemberList(PageRequest.of(0, 20))`, `service.getCreatorList(PageRequest.of(0, 20))`를 호출해 `signOutDate`가 비어 있지 않고 예외가 발생하지 않기를 기대한다.
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.admin.member.AdminMemberServiceTest`
- 기대 결과: production code 수정 전에는 `LazyInitializationException`으로 테스트가 실패한다.
- 구현 기록(2026-06-27): `AdminMemberServiceTest`를 추가해 `@Transactional` 없는 테스트 클래스에서 서비스 목록 조회를 호출하도록 했다.
- 1차 RED: `./gradlew test --tests kr.co.vividnext.sodalive.admin.member.AdminMemberServiceTest` 실행 시 Redis 연결 실패로 Spring context 생성이 실패해 의도한 실패가 아니었다.
- 보정: 기존 통합 테스트 패턴에 맞춰 `EmbeddedRedisInitializer`를 추가했다.
- 2차 RED: 같은 명령 재실행 결과 `getMemberList`, `getCreatorList` 모두 `LazyInitializationException`으로 실패해 OSIV off lazy collection 접근 문제를 재현했다.
---
### Phase 2: 서비스 read-only 트랜잭션 보강
- [x] **Task 2.1: 서비스 클래스에 read-only 트랜잭션 기본값 추가**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberService.kt`
- GREEN: `AdminMemberService` 클래스에 `@Transactional(readOnly = true)`를 추가한다.
- GREEN: `updateMember`, `resetPassword`의 기존 메서드 레벨 `@Transactional`은 유지해 쓰기 트랜잭션으로 동작하게 한다.
- GREEN: 응답 매핑 로직과 리포지토리 쿼리는 변경하지 않는다.
- 통과 확인: `./gradlew test --tests kr.co.vividnext.sodalive.admin.member.AdminMemberServiceTest`
- 기대 결과: `BUILD SUCCESSFUL`
- REFACTOR: 불필요한 import/format 변경이 생기지 않았는지 확인한다.
- 구현 기록(2026-06-27): 최초 구현에서는 `getMemberList`, `searchMember`, `getCreatorList`, `searchCreator``@Transactional(readOnly = true)`를 추가했다.
- GREEN: `./gradlew test --tests kr.co.vividnext.sodalive.admin.member.AdminMemberServiceTest` 실행 결과 `BUILD SUCCESSFUL`을 확인했다.
- 검증 이유: OSIV off 환경에서 서비스 메서드의 read-only 트랜잭션 안에서 `signOutReasons``auth` lazy 접근이 완료되는지 확인했다.
- 후속 수정(2026-06-27): 리뷰 피드백에 따라 개별 조회 메서드 annotation을 제거하고 `AdminMemberService` 클래스 레벨 `@Transactional(readOnly = true)`로 정리했다. 쓰기 메서드 `updateMember`, `resetPassword`는 기존 메서드 레벨 `@Transactional`을 유지했다.
---
### Phase 3: 회귀 검증과 문서 기록
- [x] **Task 3.1: 관련 검증 실행 및 문서 기록**
- Verify: `./gradlew test --tests kr.co.vividnext.sodalive.admin.member.AdminMemberServiceTest`
- Verify: `./gradlew :app:ktlintCheck`는 단일 루트 프로젝트에 `:app` 모듈이 없으면 실행하지 않고 `./gradlew ktlintCheck`로 대체한다.
- Verify: `./gradlew ktlintCheck`
- Verify: `./gradlew tasks --all`
- 문서 기록: 각 task 아래에 실행 명령, 결과, 검증 이유를 한국어로 누적한다.
- 구현 기록(2026-06-27): 관련 단일 테스트, ktlint, Gradle task 목록 검증을 실행했다.
- 단일 테스트: `./gradlew test --tests kr.co.vividnext.sodalive.admin.member.AdminMemberServiceTest` 실행 결과 `BUILD SUCCESSFUL`을 확인했다.
- ktlint 1차: `./gradlew ktlintCheck``./gradlew tasks --all`과 동시에 실행했을 때 `~/.gradle` wrapper lock 파일 접근 sandbox 오류로 실패했다.
- ktlint 재실행: `./gradlew --no-daemon ktlintCheck` 실행 결과 `BUILD SUCCESSFUL`을 확인했다.
- 명령 유효성: `./gradlew --no-daemon tasks --all` 실행 결과 `BUILD SUCCESSFUL`을 확인했다.
---
## 검증 기록
- 2026-06-27: `./gradlew test --tests kr.co.vividnext.sodalive.admin.member.AdminMemberServiceTest`로 OSIV off lazy collection 재현 테스트가 수정 후 통과함을 확인했다.
- 2026-06-27: `./gradlew --no-daemon ktlintCheck`로 Kotlin formatting 검증이 통과함을 확인했다.
- 2026-06-27: `./gradlew --no-daemon tasks --all`로 문서에 안내된 Gradle 명령 목록이 유효함을 확인했다.
- 2026-06-27: 최종 확인으로 `./gradlew --no-daemon test --tests kr.co.vividnext.sodalive.admin.member.AdminMemberServiceTest``./gradlew --no-daemon ktlintCheck`를 재실행했고 둘 다 `BUILD SUCCESSFUL`을 확인했다.
- 2026-06-27: 클래스 레벨 `@Transactional(readOnly = true)` 후속 변경 후 `./gradlew --no-daemon test --tests kr.co.vividnext.sodalive.admin.member.AdminMemberServiceTest``./gradlew --no-daemon ktlintCheck`를 재실행했고 둘 다 `BUILD SUCCESSFUL`을 확인했다.

View File

@@ -0,0 +1,77 @@
# PRD: 관리자 회원 목록 LazyInitializationException 수정
## 1. Overview
`spring.jpa.open-in-view=false` 환경에서 관리자 회원 리스트와 크리에이터 리스트 조회 시 `Member.signOutReasons` lazy collection 접근으로 발생하는 `LazyInitializationException`을 방지한다.
---
## 2. Problem
- 관리자 회원 목록 응답 생성 중 `Member.signOutReasons`를 읽어 탈퇴일을 계산한다.
- 현재 `AdminMemberService`의 목록 조회 메서드는 트랜잭션 경계가 없어 QueryDSL 조회 후 영속성 컨텍스트가 닫힌 상태에서 lazy collection을 접근할 수 있다.
- `spring.jpa.open-in-view=false` 환경에서는 이 접근이 `org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: kr.co.vividnext.sodalive.member.Member.signOutReasons`로 이어진다.
- 같은 응답 매핑 흐름을 사용하는 관리자 회원 리스트, 회원 검색, 크리에이터 리스트, 크리에이터 검색 모두 같은 위험이 있다.
---
## 3. Goals
- `osiv=false` 환경에서도 관리자 회원 리스트와 크리에이터 리스트가 예외 없이 응답된다.
- 기존 API 응답 스키마와 정렬/필터 조건을 변경하지 않는다.
- 탈퇴 이력이 있는 회원의 `signOutDate` 계산 동작을 유지한다.
- 서비스 계층에 명확한 read-only 트랜잭션 기본 경계를 둔다.
- 실패 재현 테스트를 먼저 작성하고, 최소 수정으로 통과시킨다.
---
## 4. Non-Goals
- 관리자 회원 목록 API의 응답 필드를 변경하지 않는다.
- `Member.signOutReasons` fetch 전략을 전역 eager로 바꾸지 않는다.
- 목록 조회를 projection 전용 쿼리로 전면 개편하지 않는다.
- pagination/count 쿼리 구조를 리팩터링하지 않는다.
- OSIV 설정을 다시 켜지 않는다.
---
## 5. Target Users
- 관리자: 관리자 화면에서 회원 목록과 크리에이터 목록을 조회하는 사용자
- 운영자: 탈퇴 또는 차단 이력이 있는 회원을 포함한 목록을 안정적으로 확인해야 하는 사용자
---
## 6. User Stories
- 관리자는 탈퇴 이력이 있는 일반 회원이 포함된 회원 리스트를 조회해도 서버 오류를 만나지 않아야 한다.
- 관리자는 탈퇴 이력이 있는 크리에이터가 포함된 크리에이터 리스트를 조회해도 서버 오류를 만나지 않아야 한다.
- 관리자는 검색 결과에서도 동일하게 탈퇴일과 활성 상태를 확인할 수 있어야 한다.
---
## 7. Core Features
### Feature A. 관리자 회원 목록 조회 트랜잭션 보강
#### Requirements
- `AdminMemberService`는 클래스 레벨 `@Transactional(readOnly = true)`로 조회 기본 트랜잭션을 제공한다.
- `AdminMemberService.getMemberList(...)`는 read-only 트랜잭션 안에서 조회와 응답 매핑을 완료한다.
- `AdminMemberService.searchMember(...)`는 read-only 트랜잭션 안에서 조회와 응답 매핑을 완료한다.
- `AdminMemberService.getCreatorList(...)`는 read-only 트랜잭션 안에서 조회와 응답 매핑을 완료한다.
- `AdminMemberService.searchCreator(...)`는 read-only 트랜잭션 안에서 조회와 응답 매핑을 완료한다.
- `AdminMemberService.updateMember(...)`, `AdminMemberService.resetPassword(...)`는 메서드 레벨 `@Transactional`로 쓰기 트랜잭션을 유지한다.
- 기존 `processMemberListToGetAdminMemberListResponseItemList(...)`의 응답 필드 계산 방식은 유지한다.
#### Edge Cases
- `signOutReasons`가 비어 있으면 기존처럼 `signOutDate`는 빈 문자열이다.
- `signOutReasons`가 있으면 기존처럼 마지막 탈퇴 이력의 `createdAt`을 KST `yyyy-MM-dd HH:mm` 형식으로 내려준다.
- `auth` lazy one-to-one 접근도 같은 read-only 트랜잭션 안에서 처리되어야 한다.
---
## 8. Technical Constraints
- Kotlin + Spring Boot 2.7.14 + Spring Data JPA 기준으로 구현한다.
- 테스트 환경의 `spring.jpa.open-in-view=false` 설정을 유지한다.
- 서비스 클래스에는 `@Transactional(readOnly = true)`를 사용하고, 쓰기 메서드는 기존 메서드 레벨 `@Transactional`을 유지한다.
- 변경 범위는 `AdminMemberService`와 해당 테스트로 제한한다.
---
## 9. Metrics
- `AdminMemberServiceTest`에서 OSIV off 조건의 회원/크리에이터 목록 조회 테스트가 통과한다.
- 관련 단일 테스트와 `ktlintCheck`가 통과한다.