본문으로 건너뛰기
개발 머꼬
개발 노트Spring
hohyeon.dev32

컨트롤러에서 연관 엔티티에 접근했다가 LazyInitializationException이 난 이유

  • #Engineering Note
  • #JPA
  • #Spring

문제 발생

서비스 레이어에서 엔티티를 조회해 컨트롤러로 그대로 반환했는데, 그 엔티티의 연관 엔티티(예: 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이 발생합니다.

해결 방안

  1. 필요한 연관 데이터를 트랜잭션 안에서 미리 로딩합니다 — 서비스 메서드 안에서 명시적으로 접근하거나, JPQL의 FETCH JOIN으로 한 번에 가져옵니다.
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
Order findByIdWithItems(@Param("id") Long id);
  1. 서비스 레이어에서 엔티티가 아니라 DTO로 변환해서 반환합니다 — 트랜잭션이 살아있는 동안(서비스 메서드 안에서) 필요한 필드/연관 데이터를 전부 읽어 DTO에 옮겨 담고, 컨트롤러에는 이미 완성된 순수 데이터만 넘깁니다. 이 패턴이 가장 근본적인 해결책으로 권장되는 경우가 많습니다 — 애초에 엔티티를 트랜잭션 밖으로 내보내지 않는 설계입니다.
  2. Open Session In View(OSIV) 패턴으로 요청 전체 동안 영속성 컨텍스트를 열어두는 옵션도 있지만, Spring Boot 공식 문서 자체가 이 패턴을 기본값으로 켜두면서도 트레이드오프(트랜잭션 경계가 불명확해짐, DB 커넥션을 요청 처리 내내 점유)를 명시적으로 경고합니다 — 편리함 때문에 무심코 의존하기보다 의식적으로 켜고 끄는 것이 좋습니다.
  3. 연관 관계가 대부분 즉시 필요하지 않은 경우가 많으므로, @ManyToOne도 기본값(EAGER)이 아니라 명시적으로 LAZY를 지정하는 습관이 N+1 쿼리 문제(spring-jpa-n-plus-one-query)도 함께 예방합니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.