JPA를 사용하다 보면 의도지 않았지만 여러 번의 select 문이 순식간에 여러 개가 나타나는 현상을 본 적 있을 것이다. 이러한 현상을 N+1 문제라고 부른다.
N+1이란?
연관관계로 매핑된 엔티티를 조회할 경우 조회된 데이터 개수 (n) 만큼 연관관계의 조회 쿼리가 추가로 발생하여 데이터를 읽어오는 현상
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(length = 10, nullable = false)
private String name;
@OneToMany(mappedBy = "user")
private Set<Article> articles = emptySet();
}
@Entity
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(length = 50, nullable = false)
private String title;
@Column(length = 300, nullable = false)
private String content;
@ManyToOne
private User user;
}
발생 이유
N+1 문제가 발생하는 이유는 JPA가 JPQL을 분석해서 SQL을 생성할 때는 글로벌 fetch 전략을 참고하지 않고 오직 JPQL 자체만을 사용한다.
발생 과정
- findAll()과 같이 여러 Entity를 조회해야 하는 경우 JPQL에서 ‘select * from Table t’ 쿼리를 생성한다.
- DB의 결과를 받아 Table t 엔티티의 인스턴스들을 생성한다.
- Table t와 연관된 DB를 찾아 로드한다.
- 영속성 컨텍스트에 연관 DB가 있는지 확인한다.
- 영속성 컨텍스트에 없다면 ‘select * from {연관 table} where tableName+id =?’ 쿼리를 생성한다.
- 이 과정에서 N+1 문제가 발생한다.
즉시 로딩
fetch = FetchType.EAGER
// User.java
@OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
private Set<Article> articles = emptySet();
// Article.java
@ManyToOne(fetch = FetchType.EAGER)
private User user;
User의 입장에서 즉시 로딩을 사용한다고 했을 때, Article의 모든 list를 다 같이 조회하고 싶을 상황이 생길 수도 있는데 왜 즉시 로딩이 문제가 될까?
User의 select 해왔지만 JPA는 Article에 대해서 EAGER(즉시 로딩)이 걸려있는 것을 보고 select 한 모든 User에 대해서 Article이 있는지를 검색하게 됩니다.
즉, User에 대해 검색하고 싶어서 select 쿼리를 하나 날렸지만(1), 즉시 로딩이 걸려있기 때문에 각각의 User가 가진 Article을 모두 검색한다(N)라는 N+1 문제가 발생하는 것입니다.
지연 로딩
- 연관된 객체를 “사용” 하는 시점에 로딩을 해주는 방식 (fetch = FetchType.Lazy)
- 연관관계에 있는 객체는 프록시 상태로 초기화되지 않은 상태로 존재함
- 필요시에 가져오기 때문에 불필요한 쿼리를 발생시키지 않을 수 있음
N+1의 문제를 지연 로딩으로 문제를 해결할 수 있다.
하지만, 이 방식으로는 N+1문제가 전부 해결되지는 않는다.
처음 find 할 때는 N+1이 발생하지 않지만 추가로 User 검 생후 User의 Article을 사용해야 한다면 이미 캐싱된 User의 Article 프록시에 대한 쿼리가 또 발생하게 된다.
@OneToMany(mappedBy = “user”, fetch = FetchType.Lazy)
Private Set<Article> articles = emptySet();
그렇다면?
지연 로딩에서의 해결책 = fetch join
JPQL을 사용하여 DB에서 데이터를 가져올 때 처음부터 연관된 데이터까지 같이 가져오게 하는 방법이다.
(SQL Join 문을 생각하면 된다. )
별도의 메서드를 만들어줘야 하며 @Query 어노테이션을 사용해서 "join fetch 엔티티. 연관관계_엔티티" 구문을 만들어 주면 된다.
@Query(“SELECT distinct u FROM User u left join fetch u.articles”)
List<User> findAllJPQFetch();
'CS' 카테고리의 다른 글
[CS] 메모리 구조 (0) | 2024.05.08 |
---|---|
[CS] Model1과 Model2 (0) | 2024.05.08 |
[CS] 인터페이스 vs 추상 클래스 (0) | 2024.05.08 |
[CS] 영속성 컨텍스트 (Persustence Context) (0) | 2024.05.08 |
[CS] ORM(Object Relation Mapping) (0) | 2024.05.08 |