들어가기 앞서..
스터디를 진행하다, N+1 문제를 직면하게 되었다. Pet과 Visit 쪽에서 조회 쿼리를 날릴 시, 레코드 개수만큼 쿼리문이 더 날라가는 것을 확인했다. 앞서 김영한님의 ORM JPA 강의와 다른 코딩 책을 공부하며 접한적이 있지만, 다시 한번 제대로 정리해보는 시간을 가져보려 한다.
공부하기 앞서, 내가 알던 N+1 문제는 쿼리 실행 시, 개발자의 의도와 다르게 쿼리문이 N 개가 더 나가는 것으로 알고있다. 이번기회로 다시 정리해보겠다.
N+1 ?
앞서 설명했듯 간단하게 설명하자면, 연관 관계에서 발생하는 문제로, 연관관계가 설정된 엔티티를 조회 시 조회된 데이터의 개수(N) 만큼 연관관계 조회 쿼리가 추가로 발생하는 것이다. 이것을 N+1 문제라고 한다.
이러한 문제를 가볍게 여기고 넘어가면 추후에 문제가 발생할 수 있다. 현재 데이터가 적어서 몇개의 쿼리가 나가지 않는다고 안심하여도, 추후 데이터가 수백 수천개가 쌓였을 때, 해당 N+1 문제가 발생하는 조회 쿼리를 실행하게 된다면 수백, 수천개의 쿼리가 나가게 되며 성능이 심각하게 저하될 것이다.
해당 N+1 문제는 언제 발생하고, 어떠한 상황에 발생하는가?
1:N, N:1 과 같은 연관관계를 가진 엔티티를 조회할 때 발생한다.
JPA fetch 전략이 EAGER, LAZY에 따라 언제 발생하는지가 조금 다르다.
EAGER인 경우에는 데이터를 조회하는 것마다 발생하게 되고, LAZY의 경우에는 해당 엔티티만 조회하게 되면 문제가 발생하지 않지만, 하위 엔티티를 같이 조회하게 될 경우 발생하게 된다. 조금 더 자세하게 설명하면 다음과 같다.
EAGER의 경우
- JPQL에서 만든 SQL을 통해 데이터 조회
- JPA에서 Fetch 전략이 EAGER이기에, 즉시로딩으로 하위에 있는 엔티티까지 추가적으로 조회
- 그렇기에 N+1 문제 발생
LAZY의 경우
- JPQL에서 만든 SQL을 통해 데이터 조회
- JPA에서 Fetch 전략이 LAZY이기에, 지연로딩으로 하위에 있는 엔티티를 직접 작업하는 것이 아니라면 조회X
- 하지만 하위 엔티티를 직접 작업하게 되면, 추가 조회를 해야하기에 N+1 문제 발생
N+1 문제는 어떻게 해결해야 하는가?
크게 해결 방법에는 FetchJoin, EntityGraph, BatchSize가 있다. 이번에는 FetchJoin과 EntityGraph를 한번 알아보겠다.
우선 N+1 문제가 발생하는 예시를 먼저 보겠다.
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Setter
@Entity
public class Owner {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "owner", fetch = FetchType.EAGER)
private List<Pet> pets = new ArrayList<>();
}
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Entity
public class Pet {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne
private Owner owner;
}
@Test
void test() {
List<Pet> pets = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Pet pet = Pet.builder().name("pet" + i).build();
pets.add(pet);
}
petRepository.saveAll(pets);
List<Owner> owners = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Owner owner = Owner.builder().name("owner" + i).build();
owner.setPets(pets);
owners.add(owner);
}
ownerRepository.saveAll(owners);
System.out.println("-------------------------------");
List<Owner> ownerList = ownerRepository.findAll();
}
FetchJoin
위에서 N+1 문제가 발생하는 이유는, 한쪽 테이블만 조회하고 그에 연관된 다른 테이블은 따로 조회하기에 발생하는 것이다. 두 테이블을 Join을 사용하여 한번에 모든 데이터를 조회한다면 N+1 문제는 발생하지 않을 것이다.
이를 해결하기위해서 FetchJoin을 사용하는 것이다.
@Query("select o "
+ "from Owner o "
+ "join fetch o.pets")
List<Owner> findAllJoinFetch();
위와 같이 작성해준 후, 실행해보면 앞서와 다르게 쿼리가 Join 문으로 하나만 나가는 것을 확인할 수 있다.
@EntityGraph
@EntityGraph의 경우는 attributePaths에 같이 조회할 연관 엔티티명을 작성해주면 된다. 여러개를 적어줄 수 있다.
@EntityGraph(attributePaths = {"pets"})
@Query("select o from Owner o")
List<Owner> findAllEntityGraph();
Fetch Join과 다른점이 있다면, FetchJoin은 inner join하지만, EntityGraph는 outer join 한다. 성능 최적화상 inner join이 더 효울적이다.
들어가기 앞서..
스터디를 진행하다, N+1 문제를 직면하게 되었다. Pet과 Visit 쪽에서 조회 쿼리를 날릴 시, 레코드 개수만큼 쿼리문이 더 날라가는 것을 확인했다. 앞서 김영한님의 ORM JPA 강의와 다른 코딩 책을 공부하며 접한적이 있지만, 다시 한번 제대로 정리해보는 시간을 가져보려 한다.
공부하기 앞서, 내가 알던 N+1 문제는 쿼리 실행 시, 개발자의 의도와 다르게 쿼리문이 N 개가 더 나가는 것으로 알고있다. 이번기회로 다시 정리해보겠다.
N+1 ?
앞서 설명했듯 간단하게 설명하자면, 연관 관계에서 발생하는 문제로, 연관관계가 설정된 엔티티를 조회 시 조회된 데이터의 개수(N) 만큼 연관관계 조회 쿼리가 추가로 발생하는 것이다. 이것을 N+1 문제라고 한다.
이러한 문제를 가볍게 여기고 넘어가면 추후에 문제가 발생할 수 있다. 현재 데이터가 적어서 몇개의 쿼리가 나가지 않는다고 안심하여도, 추후 데이터가 수백 수천개가 쌓였을 때, 해당 N+1 문제가 발생하는 조회 쿼리를 실행하게 된다면 수백, 수천개의 쿼리가 나가게 되며 성능이 심각하게 저하될 것이다.
해당 N+1 문제는 언제 발생하고, 어떠한 상황에 발생하는가?
1:N, N:1 과 같은 연관관계를 가진 엔티티를 조회할 때 발생한다.
JPA fetch 전략이 EAGER, LAZY에 따라 언제 발생하는지가 조금 다르다.
EAGER인 경우에는 데이터를 조회하는 것마다 발생하게 되고, LAZY의 경우에는 해당 엔티티만 조회하게 되면 문제가 발생하지 않지만, 하위 엔티티를 같이 조회하게 될 경우 발생하게 된다. 조금 더 자세하게 설명하면 다음과 같다.
EAGER의 경우
- JPQL에서 만든 SQL을 통해 데이터 조회
- JPA에서 Fetch 전략이 EAGER이기에, 즉시로딩으로 하위에 있는 엔티티까지 추가적으로 조회
- 그렇기에 N+1 문제 발생
LAZY의 경우
- JPQL에서 만든 SQL을 통해 데이터 조회
- JPA에서 Fetch 전략이 LAZY이기에, 지연로딩으로 하위에 있는 엔티티를 직접 작업하는 것이 아니라면 조회X
- 하지만 하위 엔티티를 직접 작업하게 되면, 추가 조회를 해야하기에 N+1 문제 발생
N+1 문제는 어떻게 해결해야 하는가?
크게 해결 방법에는 FetchJoin, EntityGraph, BatchSize가 있다. 이번에는 FetchJoin과 EntityGraph를 한번 알아보겠다.
우선 N+1 문제가 발생하는 예시를 먼저 보겠다.
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Setter
@Entity
public class Owner {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "owner", fetch = FetchType.EAGER)
private List<Pet> pets = new ArrayList<>();
}
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Entity
public class Pet {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne
private Owner owner;
}
@Test
void test() {
List<Pet> pets = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Pet pet = Pet.builder().name("pet" + i).build();
pets.add(pet);
}
petRepository.saveAll(pets);
List<Owner> owners = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Owner owner = Owner.builder().name("owner" + i).build();
owner.setPets(pets);
owners.add(owner);
}
ownerRepository.saveAll(owners);
System.out.println("-------------------------------");
List<Owner> ownerList = ownerRepository.findAll();
}
FetchJoin
위에서 N+1 문제가 발생하는 이유는, 한쪽 테이블만 조회하고 그에 연관된 다른 테이블은 따로 조회하기에 발생하는 것이다. 두 테이블을 Join을 사용하여 한번에 모든 데이터를 조회한다면 N+1 문제는 발생하지 않을 것이다.
이를 해결하기위해서 FetchJoin을 사용하는 것이다.
@Query("select o "
+ "from Owner o "
+ "join fetch o.pets")
List<Owner> findAllJoinFetch();
위와 같이 작성해준 후, 실행해보면 앞서와 다르게 쿼리가 Join 문으로 하나만 나가는 것을 확인할 수 있다.
@EntityGraph
@EntityGraph의 경우는 attributePaths에 같이 조회할 연관 엔티티명을 작성해주면 된다. 여러개를 적어줄 수 있다.
@EntityGraph(attributePaths = {"pets"})
@Query("select o from Owner o")
List<Owner> findAllEntityGraph();
Fetch Join과 다른점이 있다면, FetchJoin은 inner join하지만, EntityGraph는 outer join 한다. 성능 최적화상 inner join이 더 효울적이다.