지연로딩(LAZY)에서 @Transactional이 없으면 proxy 초기화 오류가 발생합니다.
즉시로딩(EAGER)을 선택했을 때는, 오류가 발생하지 않습니다. 왜 이런 상황이 발생할까요?
예시 테스트 코드 설명
Member - Reservation이 연관관계를 가지고 있습니다.
하나의 멤버는 여러개의 예약을 가질 수 있는 상황이기에 Reservation에 ManyToOne 관계를 설정해놨습니다.
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
public Member() {}
public Member(String name) {
this.name = name;
}
}
@Entity
public class Reservation {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String reservationName;
// 하나의 멤버는 여러개의 예약을 가질 수 있다.
@ManyToOne(fetch = FetchType.LAZY)
private Member member;
public Reservation() {}
public Reservation(Member member, String reservationName) {
this.member = member;
this.reservationName = reservationName;
}
}
문제가 되는 테스트 메서드 입니다.
@Test
void 모든_예약기록을_조회한다() {
assertThat(reservationService.findAll()).hasSize(6);
}
public List<ReservationResponse> findAll() {
final List<Reservation> reservations = reservationRepository.findAll();
return reservations.stream()
.map(ReservationResponse::of)
.toList();
}
public static ReservationResponse of(final Reservation reservation) {
return new ReservationResponse(
reservation.getId(),
reservation.getDate(),
ReservationTimeResponse.of(reservation.getTime()),
ThemeResponse.of(reservation.getTheme()),
MemberResponse.of(reservation.getMember())
);
}
public static MemberResponse of(final Member member) {
return new MemberResponse(member.getId());
}
해당 테스트 코드를 실행하면, LazyInitializerExcetion이 발생합니다. 왜 그럴까요?
Lazy 환경에서의 코드 흐름을 따라가봅시다. Lazy의 작동원리를 파악하는게 목적이 아니라, 간략하게만 다루겠습니다.
1) findAll() 메서드를 실행합니다. (findAll()은 커스텀 구현을 하지 않았다. JPA에서 제공하는 메서드를 사용중이다.)
2) JPA의 내부 구현을 통해 List<Reservation>을 가져옵니다.
3) 이 상황에서, Member 객체는 아직 사용을 안 하기에 Proxy 객체로 대체가 됩니다.
Reservation의 예상 값)
- id = 1L
- member = ProxyMember
4) reservations.stream()을 진행하는 과정에서 of() 메서드를 호출합니다.
5) ReservationResponse.of() 메서드에서 getMember() 메서드를 호출합니다. (아직까지는 프록시 객체를 사용합니다.)
6) MemberResponse.of() 에서 member.getId()를 호출합니다.
6번 시점에서, proxy 객체는 member 엔티티의 정보를 얻기 위해 추가 쿼리를 DB에 날립니다. 하지만, 영속성 컨텍스트가 존재하지 않기에 LazyInitializerExcetion이 발생합니다.
왜 영속성 컨텍스트가 존재하지 않다는 오류가 발생할까요?
DB에 접근하려면 영속성 컨텍스트가 살아있어야 합니다. 그리고, 영속성 컨텍스트가 생기고 폐기되는 시점은 @Transactional 단위입니다. 하나의 Transaction이 시작될 때 영속성 컨텍스트가 생기고 트랜잭션이 끝나면, 영속성 컨텍스트는 폐기됩니다.
결국, 오류가 발생한 이유는 트랜잭션이 없으므로, 영속성 컨텍스트가 존재하지 않아 발생하는 오류였습니다.
이 부분은 이해가 되지만, 다른 부분이 이해가 되지 않았습니다. 트랜잭션이 존재하지 않고, 영속성 컨텍스트가 없어 DB에 쿼리를 못 날리는게 문제라면, 애초에 List<Reservation>을 가져오는 쿼리문(select * from reservation)에서 오류가 발생해야 하는 거 아닐까? 라는 의문점이 들었습니다.
위의 이론에 따르면, Transactional이 없다면 영속성 컨텍스트가 존재하지 않을것이고, 1차 쿼리를 날리는 시점에서 오류가 발생해야 하는것 아닐까요? 왜 1차 쿼리를 날리는 시점에서는 정상적으로 쿼리문이 날라갈까요?
@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
JPA의 구현체입니다.
클래스 상단에 @Transactional(readOnly = true) 가 붙어있는 모습을 확인할 수 있습니다.
즉, 따로 트랜잭션을 붙여주지 않아고, Spring의 Repository에서 호출하는 read-only 메서듣르은 영속성 컨텍스트가 잠깐 생겼다가 사라집니다. 위와 같은 이유로 인해 1차 쿼리 (select * from reservation) 시점에서는 영속성 컨텍스트가 존재하고, 쿼리문을 성공적으로 날릴 수 있습니다. 그에 비해, Proxy 객체가 Entity의 값이 필요해 날리는 쿼리는 트랜잭션이 존재하지 않고, 영속성 컨텍스트가 존재하지 않아 오류가 발생하는 것이지요.
fetch = EAGER인 상황에서는 무슨 일이 일어날까요?
위 상황에서는, proxy 객체가 추가 쿼리를 날리는 상황이 존재하지 않아 오류가 발생하지 않습니다.
실험 테스트 코드
@DisplayName("지연로딩을 사용하는 상황에서 Spring이 관리하는 메서드는, 쿼리가 날라간다.")
@Test
void 지연로딩_테스트1() {
Member member = new Member("jenson");
Reservation reservation = new Reservation(member, "reservation1");
memberRepository.save(member);
reservationRepository.save(order);
em.close(); // 영속성 컨텍스트 닫아버리기
assertThatCode(() -> reservationRepository.findById(reservation.getId()))
.doesNotThrowAnyException();
}
// 트랜잭을 따로 명시하지 않았지만, Spring의 Repository에서 제공하는 메서드이기 때문에
// 통과합니다.
@DisplayName("지연로딩을 사용하는 상황에서, 프록시 객체가 추가 쿼리문을 날린다면, 오류가 발생한다.")
@Test
void 지연로딩_테스트2() {
Member member = new Member("jenson");
Order order = new Order(member, "order1");
memberRepository.save(member);
orderRepository.save(order);
em.close();
Optional<Order> findReservation = orderRepository.findById(order.getId());
em.close();
Order order1 = findReservation.get();
em.close();
Member member1 = order1.getMember(); // 단순히 객체를 가져올 땐 터지지 않음
assertThatThrownBy(() -> member1.getName())
.isInstanceOf(LazyInitializationException.class);
}
// Proxy 객체는 Spring Repository에서 관리하는 것이 아니기에
// 영속성 컨텍스트가 존재하지 않고, 오류가 발생합니다.
사실 service layer에서 Transaction설정을 잘 해주면 해결되는 간단한 문제지만... 궁금해서 공부해봤습니다.
'JPA' 카테고리의 다른 글
JPA는 왜 기본생성자가 필요할까? (feat. 접근 제어자 범위) (0) | 2025.05.26 |
---|