skill/Etc

[JPA] Lazy Loading 사용 할때 성능 문제 : N+1

have a nice day :D 2025. 3. 10. 14:52
반응형
JPA :  N + 1 문제 
원인
- 하나의 부모 엔터티를 조회 하면, 관련된 N개의 자식 엔터티를 개별적으로 조회 하면서 총 N+1 번의 쿼리가 실행
- JPA에서 Lazy Loading을 사용할 때 발생하는 문제

문제발생 예제

@Entity
@Getter @Setter
public class Notice {
    @Id @GeneratedValue
    private Long id;
    
    private String title;

    @OneToMany(mappedBy = "notice", fetch = FetchType.LAZY)
    private List<NoticeFile> noticeFiles = new ArrayList<>();
}

@Entity
@Getter @Setter
public class NoticeFile {
    @Id @GeneratedValue
    private Long id;
    
    private String name;
    
    @ManyToOne
    @JoinColumn(name = "notice_id")
    private Notice notice;
}

문제발생 예제 : 수행 코드

List<Notice> notices = em.createQuery("SELECT n FROM Notice n", Notice.class)
                         .getResultList();

for (Notice notice : notices) {
    System.out.println("Notice: " + notice.getTitle());
    for (NoticeFile noticeFile : notice.getNoticeFiles()) {  // N+1 문제 발생 지점 (Lazy 호출하는 시점에 조회)
        System.out.println("NoticeFile: " + noticeFile.getName());
    }
}


문제발생 예제 : 수행 결과

SELECT * FROM Notice;  -- (1) 부모 엔티티 조회 (1회 실행)

SELECT * FROM NoticeFile WHERE notice_id = ?;  -- (N) 자식 엔티티 조회 (N번 실행)
SELECT * FROM NoticeFile WHERE notice_id = ?;
SELECT * FROM NoticeFile WHERE notice_id = ?;
...

최초 Notice를 조회 하였고, (조회 1번)
상세 NoticeFile에 접근 하려고 하니, Notice의 목록 수 만큼 NoticeFile를 조회 함. (조회  N번)

해결
* 1. fetch join 사용:  @Query("Select p From Parent p JOIN FETCH p.children")
 2. @EntityGraph 활용 :  @EntityGraph(attributePaths="children")
 3. batch_size 설정 :  hibernate.default_batch_fetch_size=100 (application.properties)

해결 1. fetch join 사용 [권장]
- 쿼리 실행 횟수 1회로 줄어들어 성능 최적화
- 즉시 로딩(Eager Loading)과 비슷하지만, JPQL에서만 사용되므로 다른 쿼리에 영향 없음
- 다대일(N:1) 관계에서도 유용 

List<Notice> notices = em.createQuery("SELECT n FROM Notice n JOIN FETCH n.noticeFiles", Notice.class)
                         .getResultList();

해결 1. fetch join 사용 : 수행 결과

SELECT n.*, f.* FROM Notice n
JOIN NoticeFile f ON n.id = f.notice_id;  -- 단일 쿼리로 모든 데이터 조회

 

해결2. @EntityGraph 활용 (Spring Data JPA)
@EntityGraph를 사용하면 JPQL 없이도 Fetch Join 효과를 적용 가능
- JPQL 없이 Fetch Join 효과
- Repository 단에서 해결 가능

@Repository
public interface NoticeRepository extends JpaRepository<Notice, Long> {
    
    @EntityGraph(attributePaths = "noticeFiles") 
    List<Notice> findAll();
}

해결2. @EntityGraph 활용 : 수행 결과

SELECT n.*, f.* FROM Notice n
JOIN NoticeFile f ON n.id = f.notice_id;

 

해결3. hibernate.default_batch_fetch_size 설정
여러 개의 Lazy Loading을 한 번에 가져오도록 Batch Fetching 적용
- 개별 쿼리 실행을 줄이고, 한번의 IN 쿼리로 조회
- Fetch Join을 사용할 수 없는 경우 대안이 될 수 있음

#properties
spring.jpa.properties.hibernate.default_batch_fetch_size=100

해결3. hibernate.default_batch_fetch_size : 수행 결과

SELECT * FROM Notice;
SELECT * FROM NoticeFile WHERE notice_id IN (?,?,? .... ); -- Batch 처리로 한 번에 조회

- default.batch_fetch_size=100 크기를 넘어서면 오류 발생

반응형