Files

7.4 KiB

관리자 회원 목록 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 재현 테스트

  • 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 트랜잭션 보강

  • 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 트랜잭션 안에서 signOutReasonsauth lazy 접근이 완료되는지 확인했다.
    • 후속 수정(2026-06-27): 리뷰 피드백에 따라 개별 조회 메서드 annotation을 제거하고 AdminMemberService 클래스 레벨 @Transactional(readOnly = true)로 정리했다. 쓰기 메서드 updateMember, resetPassword는 기존 메서드 레벨 @Transactional을 유지했다.

Phase 3: 회귀 검증과 문서 기록

  • 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을 확인했다.