ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Hibernate Lazy Loading & N+1 문제: JOIN FETCH로 해결한 경험 공유
    카테고리 없음 2024. 12. 3. 14:25

    Hibernate를 사용하여 개발을 진행하다 보면 Lazy Loading과 N+1 문제로 인해 성능 저하를 겪게 되는 경우가 많습니다. 이번 포스팅에서는 이러한 문제를 JOIN FETCH를 사용하여 어떻게 해결했는지에 대한 경험을 공유하려고 합니다. 제가 겪은 문제, 초기 해결 시도, 그리고 최종적으로 JOIN FETCH를 사용해 문제를 해결한 과정을 하나씩 설명드리겠습니다.

    문제 상황: Lazy Loading과 N+1 문제

    문제 개요

    저희 애플리케이션의 Hospital 엔티티는 Equipment, ICU, Emergency와 같은 여러 엔티티와 일대일 관계를 맺고 있었으며, 이러한 연관 관계는 fetch = FetchType.LAZY로 설정되어 있었습니다. 이는 필요할 때만 데이터를 로드하여 성능을 최적화하기 위한 의도였지만, 다음과 같은 두 가지 주요 문제가 발생했습니다:

    1. LazyInitializationException: 데이터를 비동기적으로 처리할 때 세션이 이미 닫힌 상태에서 연관된 엔티티에 접근하려고 하면 LazyInitializationException이 발생했습니다. 이로 인해 특정 워크플로우에서 데이터를 제대로 로드하지 못하는 상황이 발생했습니다.
    2. N+1 쿼리 문제: fetch = FetchType.LAZY 설정으로 인해 연관된 엔티티에 접근할 때마다 개별 쿼리가 발생해, 비효율적인 SQL 쿼리가 폭발적으로 증가했습니다. 이로 인해 성능 병목 현상이 발생했습니다.

    구체적인 문제 상황

    • 비동기 처리 시 Lazy Loading 문제: Hospital 엔티티의 연관된 Equipment, ICU, Emergency 등의 엔티티에 접근하려고 했을 때 세션이 닫힌 상태라 데이터를 로드할 수 없는 문제가 자주 발생했습니다.
    • N+1 쿼리의 성능 저하: Hospital 리스트를 조회한 후 각 병원의 Equipment, ICU, Emergency 정보를 조회할 때 각각의 엔티티에 대해 추가적인 쿼리가 발생했습니다. 병원의 수가 많아질수록 쿼리 수가 기하급수적으로 증가하여 애플리케이션의 성능이 크게 저하되었습니다.

    문제 해결을 위한 시도

    이 문제를 해결하기 위해 여러 가지 접근 방식을 시도해 보았습니다.

    1. FetchType 변경 시도

    처음에는 fetch = FetchType.LAZY 설정을 FetchType.EAGER로 변경하여 연관된 모든 엔티티를 한 번에 로딩하는 방법을 시도했습니다. 이 방법을 통해 Lazy 로딩 문제는 해결할 수 있었으나, 모든 연관 데이터를 무조건 로드해야 했기 때문에 불필요한 데이터까지 로드되면서 메모리 사용량 증가성능 저하를 초래했습니다. 특히 연관된 엔티티의 수가 많거나 크기가 큰 경우, EAGER 로딩은 오히려 더 큰 문제가 될 수 있었습니다.

    2. Hibernate.initialize() 사용 및 세션 관리 개선

    다음으로, Hibernate의 Hibernate.initialize() 메서드를 사용하여 필요한 데이터를 미리 로드하는 방식을 시도했습니다. 이를 통해 비동기 작업에서 세션이 닫히기 전에 데이터를 로드하도록 했습니다. 하지만 이 방식은 코드의 복잡성을 증가시켰으며, N+1 문제는 여전히 해결되지 않았습니다. 또한, 로직이 복잡해지면서 유지보수가 어려워졌습니다.

    3. JOIN FETCH를 사용한 최적화

    최종적으로, JPQL 쿼리에서 JOIN FETCH를 사용하여 필요한 연관 엔티티를 한 번에 로드하는 방식을 선택했습니다. 이를 통해 Lazy 로딩 설정을 유지하면서도, 연관된 엔티티를 효율적으로 로드할 수 있었습니다. 구체적으로는 아래와 같은 쿼리를 작성하여 해결했습니다.

    public interface HospitalRepository extends JpaRepository<Hospital, Long> {
    
        @Query("SELECT h FROM Hospital h " +
               "JOIN FETCH h.equipmentId " +
               "JOIN FETCH h.icuId " +
               "JOIN FETCH h.emergencyId " +
               "WHERE h.city = :city AND h.state = :state")
        List<Hospital> findByCityAndStateWithJoinFetch(@Param("city") String city, @Param("state") String state);
    }

    이 방법을 통해 아래와 같은 이점을 얻을 수 있었습니다:

    • Lazy Loading 문제 해결: JOIN FETCH를 사용하여 연관된 엔티티를 한 번의 쿼리로 로드하므로, 세션이 닫히기 전에 모든 데이터를 안전하게 로드할 수 있었습니다. 이를 통해 비동기 처리에서도 안정적으로 데이터에 접근할 수 있었습니다.
    • N+1 문제 해결: JOIN FETCH를 사용함으로써, Hospital과 연관된 모든 엔티티를 단일 쿼리로 로드하여 개별 쿼리 호출이 반복되지 않도록 하여 성능이 크게 개선되었습니다.

    최종 해결 방법: JOIN FETCH 적용

    필요한 연관 엔티티를 한 번에 로드하기 위해 JOIN FETCH를 사용한 JPQL 쿼리를 작성하였습니다. 이 방식은 Lazy 로딩의 유연함을 유지하면서도, 성능 문제를 효과적으로 해결할 수 있는 최적의 방법이었습니다.

    • 안정적인 데이터 로딩: 비동기 환경에서도 안정적으로 연관 데이터를 로딩할 수 있었습니다.
    • 쿼리 최적화: 연관된 엔티티들을 한 번의 쿼리로 로드하여 불필요한 데이터베이스 요청을 줄임으로써, 애플리케이션 성능을 최적화할 수 있었습니다.

    결론

    Hibernate를 사용할 때 Lazy Loading과 N+1 문제는 자주 마주하게 되는 골칫거리입니다. 이번 경험을 통해, 연관 엔티티를 효율적으로 로딩하기 위한 여러 방법을 시도해보았고, 최종적으로 JOIN FETCH를 통해 문제를 해결할 수 있었습니다. JOIN FETCH는 특히 Lazy 로딩을 유지하면서 성능 문제를 해결하고자 할 때 매우 유용한 도구입니다.

Designed by Tistory.