From 5b865729f5112033cde7bf147a1b82b6c7b50d61 Mon Sep 17 00:00:00 2001 From: Jugwang Hong <59231743+Hju95@users.noreply.github.com> Date: Mon, 19 Feb 2024 22:36:20 +0900 Subject: [PATCH] =?UTF-8?q?feat=20:2023-11-28-N+1.md=20=5F=ED=99=8D?= =?UTF-8?q?=EC=A3=BC=EA=B4=91=20(#51)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _posts/2023-11-28-N+1.md | 275 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 275 insertions(+) create mode 100644 _posts/2023-11-28-N+1.md diff --git a/_posts/2023-11-28-N+1.md b/_posts/2023-11-28-N+1.md new file mode 100644 index 0000000..6da9a5d --- /dev/null +++ b/_posts/2023-11-28-N+1.md @@ -0,0 +1,275 @@ +--- +layout: post +title: N+1 +author: 홍주광 +categories: 기술세미나 +banner: + image: https://raw.githubusercontent.com/Kernel360/blog-image/main/2023/1128/4_N+1문제란.png + background: "#000" + height: "100vh" + min_height: "38vh" + heading_style: "font-size: 4.25em; font-weight: bold; text-decoration: underline" +tags: [Java, ORM, JPA, N+1, SQL, 최적화] +--- + +안녕하세요. N+1 주제로 발표하게 된 홍주광입니다. + +*사례* 먼저 보시겠습니다. + +![1](https://raw.githubusercontent.com/Kernel360/blog-image/main/2023/1128/1_사례배포전.png) + +클라이언트에서 서버로 초당 5회의 요청을 보냈고 평균 30ms속도로 응답을 받았습니다. + +조금 더 올려볼까요? + +![2](https://raw.githubusercontent.com/Kernel360/blog-image/main/2023/1128/2_사례배포전2.png) + +초당 1000회의 요청을 보내봤습니다. 어떻게 되었을까요? + +![3](https://raw.githubusercontent.com/Kernel360/blog-image/main/2023/1128/3_사례배포전3.png) + +초당 1000회의 요청을 보냈고 평균 4000ms속도의 응답을 받았습니다. 서버는 과부하가 되었습니다. + +왜 이런 현상이 벌어졌을까요? + +바로 이 사례에서는 `N+1` 문제 때문이였습니다. (물론 항상 위 현상이 N+1 문제로 일어나는 것은 아닙니다..) + +그만큼 N+1 문제가 서버에 영향을 줄 수 있다는 뜻입니다. + +## N+1 + +N+1 문제란 무엇일까요? + +![4](https://raw.githubusercontent.com/Kernel360/blog-image/main/2023/1128/4_N+1문제란.png) + +데이터 조회 시, 1개의 쿼리로 요청이 처리될 것이라고 생각했는데 N개의 추가 쿼리가 더 발생하는 문제입니다. + +## N+1 문제 예시 + +N+1 문제의 정의만 듣고는 이해가 어려울 수도 있어 예시를 준비해보았습니다. + +![5](https://raw.githubusercontent.com/Kernel360/blog-image/main/2023/1128/5_N+1사례.png) + +주인와 고양이가 1:N 인 일대다 관계로 있습니다. + +한 명의 주인이 여러 고양이를 키울 수 있습니다. + +주인은 10명이고 각 주인이 10마리의 고양이를 키운다고 가정하고 + +모든 주인을 조회해보겠습니다. + +우리는 모든 주인을 조회하는 쿼리이니 당연히 1개의 쿼리가 나올 것이라고 생각할 것입니다. + +![6](https://raw.githubusercontent.com/Kernel360/blog-image/main/2023/1128/6_N+1사례_쿼리.png) + +그러나 1개의 쿼리가 아닌 총 11개의 쿼리가 더 나왔습니다. + +여기서 조금 더 자세히 짚고 넘어가자면 + +빨간 박스인 예상 쿼리를 보시면 모든 주인을 조회하는 쿼리가 나왔습니다. 10명의 주인이 조회되었습니다. + +노란 박스인 추가 쿼리를 보시면 10개가 더 나왔는데 이것은 고양이의 숫자와 상관없이 10명의 주인이 각 1개의 쿼리가 더 생긴 것입니다. + +즉, + +* 1번주인, 2번주인, 3번주인, 4번주인, 5번주인, 6번주인, 7번주인, 8번주인, 9번주인, 10번주인 + +이 조회 되었고 + +* 1번주인의 고양이를 조회하는 쿼리, 2번주인의 고양이를 조회하는 쿼리, 3번주인의 고양이를 조회하는 쿼리, 4번주인의 고양이를 조회하는 쿼리, 5번주인의 고양이를 조회하는 쿼리, +6번주인의 고양이를 조회하는 쿼리, 7번주인의 고양이를 조회하는 쿼리, 8번주인의 고양이를 조회하는 쿼리, 9번주인의 고양이를 조회하는 쿼리, 10번주인의 고양이를 조회하는 쿼리 + +이렇게 각 주인마다 1개의 쿼리가 추가로 나와 총 10개의 쿼리가 추가로 나오게 된 것입니다.(고양이 수는 중요하지 않음!) + +다른 예시로 + +주인이 5명이고 각 주인이 고양이를 2마리씩 키우고 있다고 하면 + +N+1문제가 발생했을 때는 1(모든 주인 조회 쿼리) + 5(각 주인의 고양이 조회 쿼리) 해서 총 6개의 쿼리가 나오게 됩니다. + +## N+1 발생 이유 + +JPQL 이 처음 쿼리를 만들 때, 연관관계가 있는 엔티티는 신경 쓰지 않고 조회 대상이 되는 엔티티 기준으로만 쿼리를 만들기 때문에 발생합니다. + +이는 JPA 뿐만 아니라 ORM 은 다 발생하는 문제입니다. + +## 글로벌 페치 전략 + +### 즉시로딩 + +즉시로딩(EAGER) 은 부모 엔티티를 조회할 때 연관된 자식 엔티티를 함께 가져오는 방식 + +### 지연로딩 + +지연로딩(LAZY) 은 부모 엔티티를 조회할 때 연관된 자식 엔티티를 필요한 시점까지 로딩하지 않고 지연하여 가져오는 방식 + +## 페치 전략으로 해결 가능할까? + +구글링을 해보면 많은 사람들이 페치 전략을 통해 N+1 문제를 해결하려는 시도를 볼 수 있습니다. + +### 즉시 로딩 페치 전략 + +> 즉시 로딩으로 한번에 불러오면 되는 거 아닌가요? + +``` +@OneToMany(mappedBy = "owner", fetch = FetchType.EAGER) +private List cats = new ArrayList<>(); +``` + +모든 주인을 조회해보겠습니다. + +![7](https://raw.githubusercontent.com/Kernel360/blog-image/main/2023/1128/7_즉시로딩.png) + +여전히 N+1문제가 발생합니다. + +1. JPQL에서 만든 SQL을 통해 데이터를 조회 +2. 이후 JPA에서 Fetch 전략을 가지고 해당 데이터의 연관 관계인 하위 엔티티들을 추가 조회 +3. 2번 과정으로 N+1 문제 발생 + +**즉시로딩으로는 N+1 문제를 해결할 수 없습니다.** + +### 지연 로딩 페치 전략 + +> 지연 로딩으로 하면 한 줄만 나오던데요? + +``` +@OneToMany(mappedBy = "owner", fetch = FetchType.LAZY) +private List cats = new ArrayList<>(); +``` + +모든 주인을 조회해보겠습니다. + +쿼리가 진짜 하나만 나옵니다. 그러나 고양이 이름도 같이 조회하게 되면 + +![8](https://raw.githubusercontent.com/Kernel360/blog-image/main/2023/1128/8_지연로딩.png) + +여전히 N+1 문제가 발생합니다. + +연관된 데이터를 찾지 않는 경우(고양이 이름을 검색하지 않을 때)에는 한 줄로 나옵니다. +-> 그래서 기본적으로 연관관계는 지연로딩(LAZY)가 좋습니다. + +1. JPQL에서 만든 SQL을 통해 데이터를 조회 +2. JPA에서 Fetch 전략을 가지지만, 지연 로딩이기 때문에 추가 조회는 하지 않음 +3. 하지만, 하위 엔티티를 가지고 작업하게 되면 추가 조회가 발생하기 때문에 결국 N+1 문제 발생 + +**임시적으로 나오지 않게 할 수는 있지만 근본적으로 지연로딩도 N+1 문제를 해결할 수 없습니다.** + +둘의 차이는 언제 쿼리를 발생시키냐의 차이입니다. + +## N+1 문제 해결 방법 + +대표적인 해결 방법으로는 3가지가 있습니다. + +* 페치 조인 + +* 엔티티 그래프 + +* 배치 사이즈 + +그 중 페치 조인과 배치 사이즈를 알아보겠습니다. + +### 페치 조인 + +* JPQL에서 성능 최적화를 위해 제공되는 기능 + +* JPQL을 사용하여 DB에서 데이터를 가져올 때 처음부터 연관된 데이터까지 같이 가져오게 하는 방법 + +``` +@Query("select o from Owner join fetch o.cats") +List findAllJoinFetch(); +``` + +![9](https://raw.githubusercontent.com/Kernel360/blog-image/main/2023/1128/9_페치조인.png) + +1개의 쿼리가 나오며 `inner join` 이 사용됩니다. + +### 페치 조인 주의점 + +* JPA 가 제공하는 Paging사용 불가능(OneToMany,ManyToMany 관계) + +* 1:N 관계가 두 개 이상인 경우 사용 불가(OneToMany,ManyToMany 관계) + +* 페치 조인 대상에게 별칭 부여 불가 + +* 중복 데이터 발생 가능성 + +### 배치 사이즈 + +* 하이버네이트가 제공하는 @BatchSize 어노테이션을 이용 + +* 연관된 엔티티를 조회할 때 지정된 size 만큼 SQL의 IN절을 사용해서 조회 + +* 완전한 N+1문제 해결은 아님 + +* 1000번 일어날 N+1 문제를 1번만 더 조회하는 방식으로 성능을 최적화(size=1000) + +``` +select * from user where team_id in (?,?,?) +``` + +배치사이즈를 사용하게 되면 위처럼 in 절로 나오게 됩니다. + +배치사이즈는 무조건 1개의 쿼리가 나오는 것은 아닙니다. + +배치 사이즈의 크기는 프로젝트마다 최적의 사이즈가 다르지만 보통 1000이 MAX 로 둔다고 알려져 있습니다. + +이 부분이 궁금하시면 찾아보시길 권장드립니다. + +![10](https://raw.githubusercontent.com/Kernel360/blog-image/main/2023/1128/10_배치사이즈1.png) + +yml 파일에서 위처럼 설정할 수도 있고 + +![11](https://raw.githubusercontent.com/Kernel360/blog-image/main/2023/1128/11_배치사이즈2.png) + +엔티티에서 바로 설정할 수도 있습니다. + +### 배치사이즈 예시 + +조금 더 이해를 돕기 위해 예시를 들어 설명해보겠습니다. + +``` +@BatchSize(size=5) +@OneToMany(mappedBy = "owner", fetch = FetchType.EAGER) +``` + +![12](https://raw.githubusercontent.com/Kernel360/blog-image/main/2023/1128/12_배치사이즈3.png) + +주인이 10명 고양이가 10명은 일대다 관계에서 사이즈가 5이면 + +처음 조회쿼리 1개 + 배치사이즈로 인한 in절 쿼리(size5) 2개 = 총 3개의 쿼리가 나오게 됩니다. + +``` +@BatchSize(size=20) +@OneToMany(mappedBy = "owner", fetch = FetchType.EAGER) +``` + +사이즈를 20으로 늘리면 어떻게 될까요? + +![13](https://raw.githubusercontent.com/Kernel360/blog-image/main/2023/1128/13_배치사이즈4.png) + +in절 쿼리가 20개까지 커버 가능하니 + +처음 조회쿼리 1개 + 배치사이즈로 인한 in절 쿼리(size20) 1개 = 총 2개의 쿼리가 나오게 됩니다. + +## 정리 + +1. N+1 문제는 데이터 조회 시, 1개의 쿼리로 요청이 처리될 것이라고 생각했는데 N개의 추가 쿼리가 더 발생하는 문제이다. +2. N+1 문제의 발생 원인은 JPQL 이 처음 쿼리를 만들 때 연관관계가 있는 엔티티는 신경 안쓰고 조회 대상이 되는 엔티티 기준으로만 쿼리를 만들기 때문이다. +3. N+1 문제는 글로벌 페치 전략으로 해결할 수 없다. +4. 페치 조인은 JPQL을 사용하여 DB에서 데이터를 가져올 때 처음부터 연관된 데이터까지 같이 가져오게 하는 방법이다. +5. 배치사이즈는 연관된 엔티티를 조회할 때 지정된 size 만큼 SQL의 IN절을 사용해서 조회하는 방법이다. + +N+1 문제는 면접에서 대답을 못하면 절대 안되는 문제로 꼭 숙지하고 가시길 바랍니다. +또한 N+1 문제를 고려하면서 연관관계와 쿼리를 작성하고 본인에게 맞는 방법으로 해결하시길 바랍니다. + +## 출저 + + + + + + + + + +