Files

9.8 KiB

관리자 라이브 추천 크리에이터 배너 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 재현 테스트

  • 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: 서비스 조회 트랜잭션 보강

  • 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 검증

  • 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: MockMvcGET /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로 통과했다.
  • 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