N+1 문제는 ORM을 사용해 연관된 엔티티나 객체를 로드할 때 발생하는 성능 문제를 의미한다.
예를 들어, 사용자와 그들의 주문을 표현하는 두 개의 테이블이 있을 때 한 사용자와 그의 모든 주문을 가져오는데 N+1번의 쿼리가 실행된다면 N+1 문제가 발생했다고 볼 수 있다.
지연 로딩은 연관된 엔티티나 객체가 실제로 사용될 때까지 로드하지 않는 방법이다.
대신 프록시 객체나 가짜 객체를 사용해 미리 로드해두고 실제로 해당 객체에 접근할 때 데이터베이스에서 해당 엔티티를 로드한다.
지연 로딩을 사용하면 연관된 엔티티를 모두 로드하는 대신 필요한 엔티티만 로드할 수 있다.
따라서 불필요한 쿼리를 줄일 수 있고, N+1 문제를 해결할 수 있다.
예를 들어 사용자의 정보만 필요하고 주문 정보는 필요하지 않을 때 지연 로딩을 사용하면 사용자 정보만 로드하고 주문 정보는 로드하지 않는다.
이후 주문 정보가 필요할 때만 데이터베이스에서 주문 정보를 로드한다.
지연 로딩을 사용할 때 주의해야 할 점은 실제로 엔티티를 사용하는 시점에서 쿼리가 발생한다는 것이다.
따라서 트랜잭션 범위 밖에서 엔티티를 사용하려고 할 때 LazyInitializationException 같은 문제가 발생할 수 있다.
이런 문제를 피하기 위해서는 트랜잭션 범위 내에서 필요한 엔티티를 모두 로드하거나 지연 로딩 설정을 조정하는 등의 방법을 사용해야 한다.
@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<>();
}
무의미한 쿼리 발생 가능성
지연 로딩을 사용하면 처음에 연관된 엔티티를 로드하지 않기 때문에 쿼리의 수는 줄일 수 있다.
그러나 필요할 때마다 해당 엔티티에 대한 쿼리가 발생한다.
따라서 여러 번의 작은 쿼리가 계속 발생할 수 있어 때로는 이게 N+1 문제를 해결하지 못하고 오히려 성능을 저하시킬 수 있다.
LazyInitializationException 문제
Hibernate와 같은 ORM 프레임워크에서는 지연 로딩된 엔티티에 접근할 때 해당 세션이 이미 종료된 경우 LazyInitializationException
이 발생할 수 있다.
이러한 문제는 트랜잭션 관리나 애플리케이션 로직을 복잡하게 만들 수 있다.
예측성의 저하
지연 로딩을 사용하면 언제 데이터베이스 쿼리가 발생할지 예측하기 어렵다.
따라서 성능 최적화나 디버깅 시 어려움을 겪을 수 있다.
트랜잭션 관리 복잡성 증가
지연 로딩을 사용할 때 필요한 엔티티를 로드하기 위해 트랜잭션을 유지해야 할 수 있다.
이로 인해 트랜잭션 관리가 더 복잡해질 수 있다.
Eager 로딩과의 트레이드오프
지연 로딩과 반대되는 개념인 즉시 로딩(Eager Loading)은 애플리케이션의 로직이나 필요에 따라 더 효과적일 수 있다.
지연 로딩만으로는 모든 성능 문제를 해결하기 어렵기 때문에 상황에 맞게 적절한 로딩 전략을 선택해야 한다.
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);
@BatchSize
애노테이션을 사용하여 한 번의 쿼리로 여러 엔티티를 가져오도록 설정할 수 있다.
@OneToMany(orphanRemoval = true, fetch = FetchType.LAZY)
@JoinColumn(name = "board_id")
@BatchSize(size = 10)
@JsonBackReference
private List<UserBoard> userBoards = new ArrayList<>();