Hibernate의 Lazy 로딩과 페이징 문제, 어떻게 해결할까?

Untitled·2024년 9월 26일
0

JPA

목록 보기
2/3
post-thumbnail

문제 상황

Paging를 사용하다 보면 이런 경험 없으셨나요? @OneToMany(fetch = FetchType.LAZY)로 설정했는데도 연관 엔티티들이 마치 EAGER처럼 즉시 불러와지는 현상,,,, 이게 바로 페이징(Pageable)과 Lazy 로딩이 만나서 생기는 재미있는(?) 문제랍니다.

왜 이런 일이 생기는 걸까?

  • Hibernate의 페이징 처리 방식이 좀 특이하다: Pageable을 쓰면 JPA가 여러 쿼리를 실행하는데, 이때 Hibernate가 "아, 이거 다 필요하겠지?"하고 성능 최적화한다고 EAGER처럼 행동해버리는 것입니다.
  • 트랜잭션 문제: Lazy 로딩한 필드를 쓰려는데 트랜잭션이 이미 끝나버렸다면? 네, 맞아요. LazyInitializationException이 튀어나와요. JPA가 이걸 막으려고 데이터를 한 번에 다 가져오려고 하는 것!

어떻게 해결할 수 있을까?

  • 페치 조인(Fetch Join)을 써보자: JPQL이나 Criteria API에서 페치 조인을 쓰면 필요한 연관 엔티티를 명확하게 "야, 너도 같이 와!"라고 부를 수 있습니다.
  • @EntityGraph: 쿼리 메소드에 @EntityGraph를 붙이면 "이 관계는 즉시 로딩해줘"라고 JPA에게 부탁할 수 있습니다.
  • BatchSize 설정: @BatchSize 어노테이션으로 N+1 문제를 좀 완화시킬 수 있습니다.
  • DTO 프로젝션: 꼭 필요한 데이터만 쏙쏙 뽑아서 가져오면 불필요한 데이터 로딩을 피할 수 있습니다.
  • 트랜잭션 범위를 조절: 서비스 계층에서 트랜잭션을 시작하고 뷰 렌더링이 끝날 때까지 유지하면 LazyInitializationException과 안녕~ 할 수 있습니다.

이렇게 해보면 어떨까?

// 페치 조인 사용하기
@Query("SELECT a FROM ApiLogs a LEFT JOIN FETCH a.details WHERE a.id = :id")
Optional<ApiLogs> findByIdWithDetails(@Param("id") Long id);

// @EntityGraph 활용하기
@EntityGraph(attributePaths = {"details"})
Page<ApiLogs> findAll(Pageable pageable);

// BatchSize 설정하기
@Entity
@BatchSize(size = 100)
public class ApiLogs {
    @OneToMany(mappedBy = "apiLog", fetch = FetchType.LAZY)
    private List<ApiLogDetails> details;
}

// DTO 프로젝션 사용하기
public interface ApiLogSummary {
    Long getId();
    String getEndpoint();
}

@Query("SELECT a.id as id, a.endpoint as endpoint FROM ApiLogs a")
Page<ApiLogSummary> findAllProjectedBy(Pageable pageable);

마무리

Hibernate의 Lazy 로딩과 페이징 사이의 문제, 생각보다 복잡하다. 처음에 분명 Lazy로 설정했는데 n+1쿼리가 쭈르륵 나와서 놀랬다. ,, 휴

fetch join 을 사용하여 해결!!

profile
그저 그런 꾸준히 하고만 싶은 개발자 이야기

0개의 댓글