test #440
63
docs/20260715_라이브_예약_LazyInitializationException_수정/prd.md
Normal file
63
docs/20260715_라이브_예약_LazyInitializationException_수정/prd.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# PRD: 라이브 예약 LazyInitializationException 수정
|
||||||
|
|
||||||
|
## 1. Overview
|
||||||
|
`spring.jpa.open-in-view=false` 환경에서 라이브 예약 생성 시 `LiveRoom.reservations` lazy 컬렉션 접근으로 발생하는 `LazyInitializationException`을 서비스 트랜잭션 경계로 방지한다.
|
||||||
|
|
||||||
|
## 2. Problem
|
||||||
|
- `LiveReservationService.makeReservation()`에는 쓰기 트랜잭션 경계가 없다.
|
||||||
|
- `liveRoomRepository.findByIdOrNull()` 호출이 끝난 뒤 반환된 `LiveRoom`은 영속성 컨텍스트에서 분리된다.
|
||||||
|
- `reservation.room = room`은 `LiveReservation.room`의 사용자 정의 setter를 호출하고, setter는 `room.reservations.add(this)`로 lazy 컬렉션을 초기화한다.
|
||||||
|
- OSIV가 비활성화된 상태에서는 컬렉션을 초기화할 Session이 없어 `org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: kr.co.vividnext.sodalive.live.room.LiveRoom.reservations, could not initialize proxy - no Session`이 발생한다.
|
||||||
|
- 유료 예약에서는 `CanPaymentService.spendCan()`만 자체 트랜잭션으로 먼저 커밋될 수 있어, 이후 예약 저장이 실패하면 결제와 예약 상태가 분리될 위험도 있다.
|
||||||
|
|
||||||
|
## 3. Goals
|
||||||
|
- OSIV off 환경에서도 라이브 예약 생성이 lazy 초기화 예외 없이 완료된다.
|
||||||
|
- `makeReservation()`의 라이브방 조회, 결제, 예약 저장을 하나의 트랜잭션 경계에서 처리한다.
|
||||||
|
- 트랜잭션 없는 테스트 메서드에서 실제 Spring 서비스 프록시를 호출해 기존 오류를 재현하고 회귀를 방지한다.
|
||||||
|
- 기존 라이브 예약 API URL, 요청 및 응답 스키마를 변경하지 않는다.
|
||||||
|
|
||||||
|
## 4. Non-Goals
|
||||||
|
- `spring.jpa.open-in-view`를 활성화하지 않는다.
|
||||||
|
- `LiveRoom.reservations`를 eager fetch로 변경하지 않는다.
|
||||||
|
- `LiveReservation.room`의 양방향 연관관계 setter를 재설계하지 않는다.
|
||||||
|
- 예약 중복 방지나 동시성 정책을 새로 도입하지 않는다.
|
||||||
|
- 결제 및 예약 정책, 응답 문구와 날짜 포맷을 변경하지 않는다.
|
||||||
|
|
||||||
|
## 5. Target Users
|
||||||
|
- 사용자: 무료 또는 유료 라이브를 오류 없이 예약하려는 회원
|
||||||
|
- 운영자: OSIV off 정책을 유지하면서 결제와 예약 저장의 일관성을 보장하려는 운영 담당자
|
||||||
|
|
||||||
|
## 6. User Stories
|
||||||
|
- 사용자는 예약 가능한 라이브를 선택했을 때 서버의 lazy 초기화 오류 없이 예약을 완료할 수 있어야 한다.
|
||||||
|
- 유료 라이브 예약은 결제와 예약 저장 중 하나가 실패하면 전체 작업이 함께 롤백되어야 한다.
|
||||||
|
- 운영자는 OSIV와 엔티티 fetch 전략을 변경하지 않고 서비스 트랜잭션 경계로 오류를 방지할 수 있어야 한다.
|
||||||
|
|
||||||
|
## 7. Core Features
|
||||||
|
|
||||||
|
### Feature A. 라이브 예약 생성 트랜잭션 보강
|
||||||
|
|
||||||
|
#### Requirements
|
||||||
|
- `LiveReservationService.makeReservation()`에 쓰기 `@Transactional`을 적용한다.
|
||||||
|
- `liveRoomRepository.findByIdOrNull()`로 조회한 `LiveRoom`은 예약 연관관계 설정과 저장이 끝날 때까지 managed 상태를 유지한다.
|
||||||
|
- `CanPaymentService.spendCan()`의 기본 `REQUIRED` 전파 속성은 외부 예약 트랜잭션에 참여한다.
|
||||||
|
- 기존 비밀번호 검증, 중복 예약 검증, 보유 캔 검증, 결제, 예약 응답 생성 순서를 유지한다.
|
||||||
|
|
||||||
|
#### Edge Cases
|
||||||
|
- 가격이 0인 무료 라이브도 `LiveRoom.reservations` lazy 컬렉션 초기화 예외 없이 예약된다.
|
||||||
|
- 가격이 0보다 큰 라이브는 결제와 예약 저장이 같은 트랜잭션에 참여한다.
|
||||||
|
- 존재하지 않는 라이브방이나 회원, 잘못된 비밀번호, 중복 예약, 캔 부족에 대한 기존 예외 동작을 유지한다.
|
||||||
|
|
||||||
|
## 8. Technical Constraints
|
||||||
|
- Kotlin, Java 17, Spring Boot 2.7.14, Spring Data JPA, JUnit 5, Gradle Wrapper를 사용한다.
|
||||||
|
- 서비스 쓰기 메서드 단위로 `@Transactional` 경계를 명확히 한다.
|
||||||
|
- 테스트 클래스 자체 트랜잭션은 비활성화해 서비스 프록시의 트랜잭션 경계만 검증한다.
|
||||||
|
- 실제 JPA 엔티티를 저장하고 영속성 컨텍스트를 비운 뒤 서비스를 호출하는 통합 테스트로 검증한다.
|
||||||
|
- 변경 범위는 `LiveReservationService.makeReservation()`, 해당 회귀 테스트, PRD와 Plan/TASK 문서로 제한한다.
|
||||||
|
|
||||||
|
## 9. Metrics
|
||||||
|
- 수정 전 회귀 테스트가 `LazyInitializationException`으로 실패한다.
|
||||||
|
- 수정 후 같은 회귀 테스트가 통과하고 예약 레코드가 저장된다.
|
||||||
|
- 관련 테스트, `ktlintCheck`, `tasks --all`, `git diff --check`가 통과한다.
|
||||||
|
|
||||||
|
## 10. Open Questions
|
||||||
|
- 없음. 승인된 권장안인 서비스 쓰기 트랜잭션 적용과 OSIV off 통합 회귀 테스트로 범위를 확정한다.
|
||||||
Reference in New Issue
Block a user