Files

9.9 KiB

PRD: 크리에이터 관리자 시리즈 상세 LazyInitializationException 수정

문서 정보

항목 내용
문서 상태 구현 기준 확정
작성일 2026-08-05
최종 수정일 2026-08-05
대상 기능 크리에이터 관리자 시리즈 상세 조회
관련 구현 계획 docs/20260805_크리에이터관리자_시리즈상세_LazyInitializationException_수정/plan-task.md

1. Overview

spring.jpa.open-in-view=false 환경에서 GET /creator-admin/audio-content/series/{seriesId} 호출 시 Series.keywordList 접근으로 발생하는 LazyInitializationException을 서비스 클래스의 기본 read-only 트랜잭션 경계로 방지한다.

2. Problem Statement

  • CreatorAdminContentSeriesController.getDetail()CreatorAdminContentSeriesService.getDetail()에 상세 조회를 위임한다.
  • CreatorAdminContentSeriesService.getDetail()에는 트랜잭션이 없으며, CreatorAdminContentSeriesRepository.findByIdAndCreatorId()가 반환한 Series로 상세 DTO를 생성한다.
  • 같은 서비스의 트랜잭션 없는 public 메서드는 모두 조회 기능이고, 데이터를 변경하는 public 메서드에는 이미 메서드 레벨 @Transactional이 적용되어 있다.
  • Series.keywordList는 별도 fetch 설정이 없는 @OneToMany이므로 lazy 컬렉션이다.
  • Series.toDetailResponse()keywordList.map { it.keyword!!.tag }를 실행한다.
  • 운영·테스트 설정의 spring.jpa.open-in-view=false 때문에 리포지토리 호출 후 영속성 컨텍스트가 종료되고, DTO 변환 중 org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: kr.co.vividnext.sodalive.creator.admin.content.series.Series.keywordList, could not initialize proxy - no Session 예외가 발생한다.
  • 기존 LegacyCreatorAdminSeriesCharacterizationTest는 클래스 레벨 @Transactional과 직접 생성한 서비스 객체를 사용하므로, 실제 Spring 서비스 프록시의 트랜잭션 유무에 따른 회귀를 검증하지 못한다.

문제를 해결했다는 판단은 외부 테스트 트랜잭션이 없는 OSIV off 통합 테스트에서 실제 Spring 서비스 프록시로 상세 조회 후 키워드가 포함된 기존 응답을 정상 생성하는 것으로 한다.

3. Goals

  • OSIV off 환경에서도 소유한 시리즈 상세 조회가 LazyInitializationException 없이 완료된다.
  • 서비스 클래스의 조회 기본값을 read-only 트랜잭션으로 두고, 시리즈 조회부터 toDetailResponse()의 lazy 컬렉션 접근까지 같은 영속성 컨텍스트에서 처리한다.
  • 실제 Spring 서비스 프록시를 호출하는 통합 테스트로 수정 전 실패와 수정 후 성공을 검증한다.
  • 기존 endpoint, 인증·소유권 검사, 성공 응답 필드와 값 형식을 유지한다.

4. Non-Goals

  • spring.jpa.open-in-view를 활성화하지 않는다.
  • Series.keywordList를 전역 eager fetch로 변경하지 않는다.
  • 상세 조회 쿼리를 fetch join 또는 projection으로 재작성하지 않는다.
  • Series.toDetailResponse() 또는 GetCreatorAdminContentSeriesDetailResponse 구조를 변경하지 않는다.
  • 시리즈 목록·수정·콘텐츠 연결 등 다른 흐름을 함께 리팩터링하지 않는다.

5. Target Users and Permissions

  • 대상 사용자: 본인이 소유한 시리즈 상세를 조회하는 CREATOR 역할의 크리에이터 관리자
  • 인증·권한: 기존 @PreAuthorize("hasRole('CREATOR')")와 인증 회원 검사를 유지한다.
  • 소유권: 기존 findByIdAndCreatorId(id, creatorId) 조건과 creator.admin.series.invalid_access 오류를 유지한다.

6. 기능 요구사항

ID 상태 요구사항 수용 기준 Goal 연결
CASD-001 확정 서비스 클래스에 read-only 트랜잭션을 기본 적용하고 기존 쓰기 메서드의 메서드 레벨 트랜잭션을 유지한다. 키워드가 있는 소유 시리즈 조회가 OSIV off 환경에서 예외 없이 완료되고 기존 쓰기 메서드 annotation이 보존된다. P1-T1
CASD-002 확정 기존 상세 조회 API 계약을 유지한다. endpoint, 권한, 오류 key, 응답 DTO의 필드·형식이 바뀌지 않는다. P1-T1, P1-GATE
CASD-003 확정 테스트 외부 트랜잭션 없이 실제 서비스 프록시를 검증한다. 수정 전 Series.keywordList 예외를 재현하고, 수정 후 같은 테스트가 통과한다. P1-T1

7. API 계약

Method Path 변경 사항
GET /creator-admin/audio-content/series/{seriesId} 공개 계약 변경 없음
  • 성공 응답은 기존 ApiResponse.ok(GetCreatorAdminContentSeriesDetailResponse)를 유지한다.
  • seriesId, title, introduction, coverImageUrl, publishedDaysOfWeek, genre, keywords, isAdult, state, writer, studio 필드와 기존 문자열 변환 규칙을 유지한다.
  • 인증 실패와 타 소유자·미존재 시리즈 오류 처리를 변경하지 않는다.

8. 해결 방안

CreatorAdminContentSeriesService 클래스에 @Transactional(readOnly = true)를 기본 적용한다.

@Service
@Transactional(readOnly = true)
class CreatorAdminContentSeriesService(

getDetail()findByIdAndCreatorId() 조회와 series.toDetailResponse() 변환이 이 경계 안에서 모두 끝나므로 keywordList를 정상 초기화할 수 있다. 다른 조회 메서드도 같은 기본 경계를 사용하며, 기존 쓰기 메서드의 메서드 레벨 @Transactional은 class-level readOnly = true를 쓰기 트랜잭션으로 재정의한다. 이미 서비스 파일에서 Transactional을 사용하고 있어 새 의존성이나 import는 필요하지 않다.

현재 메서드 분류

기본 read-only 트랜잭션을 사용하는 조회 메서드 메서드 레벨 쓰기 트랜잭션을 유지하는 메서드
getSeriesList() createSeries()
getDetail() modifySeries()
getSeriesContent() addingContentToTheSeries()
searchContentNotInSeries() removeContentInTheSeries()
updateSeriesOrders()

제외한 대안

  • OSIV 활성화: 요청 전체로 영속성 컨텍스트를 확장해 현재 저장소 정책을 되돌리므로 제외한다.
  • keywordList eager 변경: 모든 Series 조회 비용에 영향을 주는 전역 변경이므로 제외한다.
  • fetch join/projection 추가: 이 endpoint만의 결함을 고치는 데 리포지토리 계약과 쿼리 변경이 불필요하므로 제외한다.
  • 컨트롤러 트랜잭션: 영속성 및 DTO 변환 경계는 서비스가 소유하는 기존 구조에 맞지 않으므로 제외한다.
  • getDetail()에만 read-only 트랜잭션 적용: 현재 트랜잭션 없는 메서드가 모두 조회 기능이므로 class-level 기본값보다 반복과 누락 가능성이 크다.

9. 기술적 제약

  • Kotlin, Java 17, Spring Boot 2.7.14, Spring Data JPA, Hibernate, JUnit 5, Gradle Wrapper를 사용한다.
  • src/main/resources/application.ymlsrc/test/resources/application.ymlspring.jpa.open-in-view=false를 유지한다.
  • 테스트는 클래스 외부 트랜잭션을 비활성화하고 fixture 생성만 TransactionTemplate로 분리한다.
  • 실제 Spring CreatorAdminContentSeriesService 빈을 주입해 proxy annotation 동작을 검증한다.
  • production code 변경은 CreatorAdminContentSeriesService class-level annotation 한 줄로 제한한다.
  • createSeries(), modifySeries(), addingContentToTheSeries(), removeContentInTheSeries(), updateSeriesOrders()의 기존 메서드 레벨 @Transactional을 유지한다.

10. 성공 기준

  • 수정 전 focused test가 Series.keywordListLazyInitializationException으로 실패한다.
  • 서비스 클래스에 @Transactional(readOnly = true) 적용 후 같은 테스트가 통과한다.
  • 응답의 keywords와 주요 기존 상세 필드 값이 fixture와 일치한다.
  • 기존 소유권·상세 동작 characterization test와 ktlintCheck가 통과한다.
  • tasks --allgit diff --check가 통과한다.
  • API 스키마, 엔티티 fetch 전략, repository query에는 변경이 없다.
  • 모든 기존 쓰기 메서드의 메서드 레벨 @Transactional이 유지된다.

11. 요구사항 추적표

요구사항 계획 Phase Goal 자동 검증
CASD-001, CASD-003 1 P1-T1 CreatorAdminContentSeriesServiceIntegrationTest
CASD-002 1 P1-T1, P1-GATE focused test, LegacyCreatorAdminSeriesCharacterizationTest

12. Open Questions

없음. 운영 stack trace, entity mapping, 서비스 호출 경계와 OSIV 설정으로 원인과 최소 해결 범위가 확인됐다.

13. Decision Log

날짜 ID 상태 결정 근거 영향 요구사항·Goal
2026-08-05 DEC-CASD-001 확정 CreatorAdminContentSeriesService.getDetail()에 메서드 단위 read-only 트랜잭션을 적용한다. DTO 변환이 이미 서비스 내부에 있어 한 줄로 lazy 접근 전체를 영속성 컨텍스트 안에 포함할 수 있다. CASD-001, CASD-002, P1-T1
2026-08-05 DEC-CASD-002 확정 실제 Spring 서비스 프록시와 외부 트랜잭션이 없는 통합 테스트로 회귀를 고정한다. 기존 characterization test는 테스트 트랜잭션과 직접 생성한 서비스 때문에 annotation 회귀를 검증할 수 없다. CASD-003, P1-T1
2026-08-05 DEC-CASD-003 정정 DEC-CASD-001의 메서드 단위 적용을 class-level @Transactional(readOnly = true) 적용으로 정정하고 기존 쓰기 메서드의 메서드 레벨 @Transactional을 유지한다. 트랜잭션 없는 기존 public 메서드는 모두 조회 기능이며, 쓰기 메서드는 이미 메서드 레벨 annotation으로 read-only 기본값을 재정의한다. CASD-001, CASD-002, P1-T1