JPA N+1 문제와 그 해결법

song yuheon·2023년 10월 2일
0

JPA

목록 보기
2/9
post-thumbnail

지연 로딩을 사용하여 N+1 문제 해결하기


1. N+1 문제란?


N+1 문제는 ORM을 사용해 연관된 엔티티나 객체를 로드할 때 발생하는 성능 문제를 의미한다.
예를 들어, 사용자와 그들의 주문을 표현하는 두 개의 테이블이 있을 때 한 사용자와 그의 모든 주문을 가져오는데 N+1번의 쿼리가 실행된다면 N+1 문제가 발생했다고 볼 수 있다.


2. 지연 로딩이란?


지연 로딩은 연관된 엔티티나 객체가 실제로 사용될 때까지 로드하지 않는 방법이다.
대신 프록시 객체나 가짜 객체를 사용해 미리 로드해두고 실제로 해당 객체에 접근할 때 데이터베이스에서 해당 엔티티를 로드한다.


3. 지연 로딩으로 N+1 문제 해결하기


지연 로딩을 사용하면 연관된 엔티티를 모두 로드하는 대신 필요한 엔티티만 로드할 수 있다.
따라서 불필요한 쿼리를 줄일 수 있고, N+1 문제를 해결할 수 있다.

예를 들어 사용자의 정보만 필요하고 주문 정보는 필요하지 않을 때 지연 로딩을 사용하면 사용자 정보만 로드하고 주문 정보는 로드하지 않는다.
이후 주문 정보가 필요할 때만 데이터베이스에서 주문 정보를 로드한다.


4. 주의사항


지연 로딩을 사용할 때 주의해야 할 점은 실제로 엔티티를 사용하는 시점에서 쿼리가 발생한다는 것이다.
따라서 트랜잭션 범위 밖에서 엔티티를 사용하려고 할 때 LazyInitializationException 같은 문제가 발생할 수 있다.
이런 문제를 피하기 위해서는 트랜잭션 범위 내에서 필요한 엔티티를 모두 로드하거나 지연 로딩 설정을 조정하는 등의 방법을 사용해야 한다.


5. 지연 로딩 적용



@Entity
@Getter
@Setter
@Builder
@AllArgsConstructor
@Table(name = "board")
@NoArgsConstructor
public class Board {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "board_id")
    private Long boardId;


    /**
     * Board : userBoard = 1 : n
     */
    @OneToMany(orphanRemoval = true,fetch = FetchType.LAZY )
    @JoinColumn(name = "board_id")
    @JsonBackReference
    private List<UserBoard> userBoards = new ArrayList<>();

    /**
     * Board : BoardColumn = 1 : n
     */

    @OneToMany(orphanRemoval = true,fetch = FetchType.LAZY )
    @JoinColumn(name = "board_id")
    @JsonBackReference
    private List<BoardColumn> boardColumns = new ArrayList<>();

}

6. N+1 문제를 해결하기 위한 지연 로딩의 한계


  • 무의미한 쿼리 발생 가능성
    지연 로딩을 사용하면 처음에 연관된 엔티티를 로드하지 않기 때문에 쿼리의 수는 줄일 수 있다.
    그러나 필요할 때마다 해당 엔티티에 대한 쿼리가 발생한다.
    따라서 여러 번의 작은 쿼리가 계속 발생할 수 있어 때로는 이게 N+1 문제를 해결하지 못하고 오히려 성능을 저하시킬 수 있다.

  • LazyInitializationException 문제
    Hibernate와 같은 ORM 프레임워크에서는 지연 로딩된 엔티티에 접근할 때 해당 세션이 이미 종료된 경우 LazyInitializationException이 발생할 수 있다.
    이러한 문제는 트랜잭션 관리나 애플리케이션 로직을 복잡하게 만들 수 있다.

  • 예측성의 저하
    지연 로딩을 사용하면 언제 데이터베이스 쿼리가 발생할지 예측하기 어렵다.
    따라서 성능 최적화나 디버깅 시 어려움을 겪을 수 있다.

  • 트랜잭션 관리 복잡성 증가
    지연 로딩을 사용할 때 필요한 엔티티를 로드하기 위해 트랜잭션을 유지해야 할 수 있다.
    이로 인해 트랜잭션 관리가 더 복잡해질 수 있다.

  • Eager 로딩과의 트레이드오프
    지연 로딩과 반대되는 개념인 즉시 로딩(Eager Loading)은 애플리케이션의 로직이나 필요에 따라 더 효과적일 수 있다.
    지연 로딩만으로는 모든 성능 문제를 해결하기 어렵기 때문에 상황에 맞게 적절한 로딩 전략을 선택해야 한다.


7. N+1 문제 해결 방법


1. JOIN FETCH 활용

JPQL에서 데이터를 가져올 때 JOIN FETCH를 사용하면 연관된 엔티티를 함께 로딩할 수 있다.

@Query("SELECT b FROM Board b JOIN FETCH b.userBoards WHERE b.boardId = :boardId")
Board findWithUserBoardsById(@Param("boardId") Long boardId);

@Query("SELECT b FROM Board b JOIN FETCH b.boardColumns WHERE b.boardId = :boardId")
Board findWithBoardColumnsById(@Param("boardId") Long boardId);

2. Batch Size 설정

@BatchSize 애노테이션을 사용하여 한 번의 쿼리로 여러 엔티티를 가져오도록 설정할 수 있다.

@OneToMany(orphanRemoval = true, fetch = FetchType.LAZY)
@JoinColumn(name = "board_id")
@BatchSize(size = 10)
@JsonBackReference
private List<UserBoard> userBoards = new ArrayList<>();

profile
backend_Devloper

0개의 댓글