컨트롤러에서 연관 엔티티에 접근했다가 LazyInitializationException이 난 이유
문제 발생
서비스 레이어에서 엔티티를 조회해 컨트롤러로 그대로 반환했는데, 그 엔티티의 연관 엔티티(예: order.getItems())에 접근하는 순간 LazyInitializationException이 발생했습니다.
@Transactional(readOnly = true)
public Order findOrder(Long id) {
return orderRepository.findById(id).orElseThrow();
}
// 컨트롤러
@GetMapping("/orders/{id}")
public OrderDto get(@PathVariable Long id) {
Order order = orderService.findOrder(id);
return new OrderDto(order.getItems()); // 여기서 예외
}원인 분석
JPA의 @ManyToOne/@OneToMany 등은 기본적으로(또는 명시적으로) **지연 로딩(FetchType.LAZY)**으로 설정할 수 있습니다 — 연관 엔티티를 실제로 필요할 때까지 DB에서 가져오지 않고, 접근하는 순간 프록시를 통해 별도 쿼리로 가져옵니다.
이 "접근하는 순간 쿼리"는 영속성 컨텍스트(그리고 그 컨텍스트를 유지하는 DB 세션/트랜잭션)가 아직 열려 있어야만 가능합니다. findOrder() 메서드가 @Transactional로 트랜잭션 범위를 갖고 있지만, 그 메서드가 반환되는 순간 트랜잭션은 커밋되고 영속성 컨텍스트도 닫힙니다. 컨트롤러가 order.getItems()를 호출하는 시점은 이미 트랜잭션 밖이므로, 지연 로딩을 시도할 방법이 없어 LazyInitializationException이 발생합니다.
해결 방안
- 필요한 연관 데이터를 트랜잭션 안에서 미리 로딩합니다 — 서비스 메서드 안에서 명시적으로 접근하거나, JPQL의
FETCH JOIN으로 한 번에 가져옵니다.
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
Order findByIdWithItems(@Param("id") Long id);- 서비스 레이어에서 엔티티가 아니라 DTO로 변환해서 반환합니다 — 트랜잭션이 살아있는 동안(서비스 메서드 안에서) 필요한 필드/연관 데이터를 전부 읽어 DTO에 옮겨 담고, 컨트롤러에는 이미 완성된 순수 데이터만 넘깁니다. 이 패턴이 가장 근본적인 해결책으로 권장되는 경우가 많습니다 — 애초에 엔티티를 트랜잭션 밖으로 내보내지 않는 설계입니다.
Open Session In View(OSIV) 패턴으로 요청 전체 동안 영속성 컨텍스트를 열어두는 옵션도 있지만, Spring Boot 공식 문서 자체가 이 패턴을 기본값으로 켜두면서도 트레이드오프(트랜잭션 경계가 불명확해짐, DB 커넥션을 요청 처리 내내 점유)를 명시적으로 경고합니다 — 편리함 때문에 무심코 의존하기보다 의식적으로 켜고 끄는 것이 좋습니다.- 연관 관계가 대부분 즉시 필요하지 않은 경우가 많으므로,
@ManyToOne도 기본값(EAGER)이 아니라 명시적으로 LAZY를 지정하는 습관이 N+1 쿼리 문제(spring-jpa-n-plus-one-query)도 함께 예방합니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.