N+1 문제란?
의도치 않게 여러개의 Select문이 짧은 시간내에 실행되는 현상
연관관계가 설정된 Entity를 조회할 경우에 조회한 데이터 개수(N)만큼 연관관계의 조회 쿼리가 추가로 발생하는 현상
예시
출처 : https://programmer93.tistory.com/83
FetchType.EAGER
TEAM과 USER 테이블이 위와 같이 1:N 관계를 가질 때
@Entity
public class User {
@Id
@GeneratedValue
private long id;
private String firstName;
private String lastName;
@ManyToOne(fetch = FetchType.EAGER) // 즉시 로딩
@JoinColumn(name = "team_id", nullable = false)
private Team team;
}
@Entity
public class Team {
@Id
@GeneratedValue
private long id;
private String name;
@OneToMany(fetch = FetchType.EAGER)
private List<User> users = new ArrayList<>();
}
@ManyToOne 어노테이션을 활용하여 연관관계 매핑 시, fetch 속성을 FetchType.EAGER (즉시 로딩)으로 설정한 경우
JpaRepository에서 findAll 호출하게 되면
Hibernate: select team0_.id as id1_0_, team0_.name as name2_0_ from team team0_
Hibernate: select users0_.team_id as team_id1_1_0_, users0_.users_id as users_id2_1_0_, user1_.id as id1_2_1_, user1_.first_name as first_na2_2_1_, user1_.last_name as last_nam3_2_1_, user1_.team_id as team_id4_2_1_ from team_users users0_ inner join user user1_ on users0_.users_id=user1_.id where users0_.team_id=?
Hibernate: select users0_.team_id as team_id1_1_0_, users0_.users_id as users_id2_1_0_, user1_.id as id1_2_1_, user1_.first_name as first_na2_2_1_, user1_.last_name as last_nam3_2_1_, user1_.team_id as team_id4_2_1_ from team_users users0_ inner join user user1_ on users0_.users_id=user1_.id where users0_.team_id=?
Hibernate: select users0_.team_id as team_id1_1_0_, users0_.users_id as users_id2_1_0_, user1_.id as id1_2_1_, user1_.first_name as first_na2_2_1_, user1_.last_name as last_nam3_2_1_, user1_.team_id as team_id4_2_1_ from team_users users0_ inner join user user1_ on users0_.users_id=user1_.id where users0_.team_id=?
Hibernate: select users0_.team_id as team_id1_1_0_, users0_.users_id as users_id2_1_0_, user1_.id as id1_2_1_, user1_.first_name as first_na2_2_1_, user1_.last_name as last_nam3_2_1_, user1_.team_id as team_id4_2_1_ from team_users users0_ inner join user user1_ on users0_.users_id=user1_.id where users0_.team_id=?
위와 같이 TEAM을 전체 조회하는 쿼리가 발생하고 그 TEAM에 속한 USER를 조회하는 쿼리들이 파생되어 발생
TEAM이 여러개가 된다면? 조회되는 모든 TEAM에 대해 연관된 USER를 하나하나 조회하는 쿼리가 발생
불필요한 쿼리 증가
FetchType.LAZY
FetchType.EAGER를 FetchType.LAZY로 변경하게 되면
Hibernate: select team0_.id as id1_0_, team0_.name as name2_0_ from team team0_
N+1 문제가 발생하지 않는 점 확인 가능
But
Hibernate: select team0_.id as id1_0_, team0_.name as name2_0_ from team team0_
Hibernate: select users0_.team_id as team_id1_1_0_, users0_.users_id as users_id2_1_0_, user1_.id as id1_2_1_, user1_.first_name as first_na2_2_1_, user1_.last_name as last_nam3_2_1_, user1_.team_id as team_id4_2_1_ from team_users users0_ inner join user user1_ on users0_.users_id=user1_.id where users0_.team_id=?
Hibernate: select users0_.team_id as team_id1_1_0_, users0_.users_id as users_id2_1_0_, user1_.id as id1_2_1_, user1_.first_name as first_na2_2_1_, user1_.last_name as last_nam3_2_1_, user1_.team_id as team_id4_2_1_ from team_users users0_ inner join user user1_ on users0_.users_id=user1_.id where users0_.team_id=?
Hibernate: select users0_.team_id as team_id1_1_0_, users0_.users_id as users_id2_1_0_, user1_.id as id1_2_1_, user1_.first_name as first_na2_2_1_, user1_.last_name as last_nam3_2_1_, user1_.team_id as team_id4_2_1_ from team_users users0_ inner join user user1_ on users0_.users_id=user1_.id where users0_.team_id=?
Hibernate: select users0_.team_id as team_id1_1_0_, users0_.users_id as users_id2_1_0_, user1_.id as id1_2_1_, user1_.first_name as first_na2_2_1_, user1_.last_name as last_nam3_2_1_, user1_.team_id as team_id4_2_1_ from team_users users0_ inner join user user1_ on users0_.users_id=user1_.id where users0_.team_id=?
USER를 탐색할 때 N+1문제가 발생
EAGER와 LAZY는 N+1문제가 발생되는 시점만 다름
Why?
우선 FetcyType.EAGER 일 때의 동작 과정을 살펴보면
1. findAll() 호출 시
select t from Team t 라는 jpql 실행, 해당 구문을 분석한 select * from team 이라는 SQL 생성되어 실행
(Hibernate: select team0_.id as id1_0_, team0_.name as name2_0_ from team team0_)
2. DB의 결과를 받아 team의 Entity 인스턴스 생성
3. team과 연결되어 있는 user도 로딩 필요 >> 영속성 컨텍스트에서 연관된 user가 존재하는 확인
4. 없다면 2에서 만들어진 인스턴스 개수에 맞게 select * from user where team_id=? 라는 SQL생성 후 실행
FetchType.LAZY 라면
3, 4번 과정이 전체 객체 조회 시 발생하는 것이 아닌 user 객체를 사용하는 시점에서 발생
** user 객체를 활용하고 싶다 >> user도 로딩이 필요 >> 3, 4번 과정 반복 **
해결 방법
Fetch Join
jpql을 사용해 DB에서 데이터를 가져올 때 처음부터 연관된 데이터까지 함께 가져오는 방법
(SQL의 Join구문)
@Query 어노테이션을 활용하여 직접 jpql 작성 필요
@Query("select t from Team t join fetch t.users")
findAll() 호출 시
Hibernate: select team0_.id as id1_0_0_, user2_.id as id1_2_1_, team0_.name as name2_0_0_, user2_.first_name as first_na2_2_1_, user2_.last_name as last_nam3_2_1_, user2_.team_id as team_id4_2_1_, users1_.team_id as team_id1_1_0__, users1_.users_id as users_id2_1_0__ from team team0_ inner join team_users users1_ on team0_.id=users1_.team_id inner join user user2_ on users1_.users_id=user2_.id
jpql에서 join fetch 구문은 Inner Join 구문으로 변경되어 실행
EntityGraph
Fetch Join을 jpql이 아닌 어노테이션으로 활용하는 방법
@Override
@EntityGraph(attributePaths = {"team"})
List<Member> findAll();
findAll() 메소드를 Ovveride 하고 @EntityGraph 어노테이션의 attributePaths 속성의 값을 join하고 싶은 객체로 설정
Fetch Join의 경우 따로 Outer Join을 명시하지 않으면 Inner Join 실행
EntityGraph의 경우 기본적으로 Left Outer Join 실행
아우터 조인이기 때문에 필요 이상의 컬럼이 조회될 가능성 존재
Batch Size
N+1 문제가 발생했을 경우에 select * from user where team_id in(?, ?, ?) 방식으로 N+1 문제가 발생하는 바법
application.yml 설정
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 1000
application.properties 설정
spring.jpa.properties.hibernate.default_batch_fetch_size=1000
Entity 설정
@OneToMany(fetch = FetchType.EAGER)
@BatchSize(size = 1000)
원하는 방법대로 골라서 설정하면
Hibernate: select team0_.id as id1_0_, team0_.name as name2_0_ from team team0_
Hibernate: select users0_.team_id as team_id1_1_1_, users0_.users_id as users_id2_1_1_, user1_.id as id1_2_0_, user1_.first_name as first_na2_2_0_, user1_.last_name as last_nam3_2_0_, user1_.team_id as team_id4_2_0_ from team_users users0_ inner join user user1_ on users0_.users_id=user1_.id where users0_.team_id in (?, ?, ?, ?)
위와 같이 in 을 사용한 쿼리 발생
실무에서?
우선 연관관계애 대한 설정이 필요하다면 FetchType을 LAZY로 사용하고 최적화 필요한 부분에 대해서 Fetch Join 사용
기본적인 Batch Size의 값을 1000 이하로 설정 (대부분 DB에서 In절의 최대 개수가 1000개이기 때문)
'끄적 > BE' 카테고리의 다른 글
Servlet (0) | 2023.01.18 |
---|---|
JPA vs MyBatis (0) | 2023.01.11 |
Spring / Spring Boot (0) | 2023.01.09 |
REST API (0) | 2023.01.09 |
HTTP (0) | 2023.01.09 |