본문 바로가기

Spring

JPA에서 N+1 문제

JPA에서 N+1 문제란?

요청이 1개의 쿼리로 처리 되길 기대했는데, N개의 추가 쿼리가 발생하는 현상!

 

지연 로딩이란?

사용할 때 까지 데이터 로딩을 미루는 현상

 

 

 

N+1문제 예시

만약 아래와 같은 db 테이블들에서

크루들의 정보를 아래와 같이 가져옴

크루_Repository.findAll() -> 쿼리문으로 바꾸면 -> select * from crew

아직 할일 목록들은 사용하지 않아서 프록시 객체가 조회는 하지 않고 가지고만 있음, 여기까지는 쿼리가 1개만 나감

→ 할일 목록 리스트는 지연로딩으로 설정했기 때문!

그뒤 크루 한명당 일을 얼마나 하나 확인하고 싶어졌음

select * from 할일 where 크루_id =  1

select * from 할일 where 크루_id =  2

이런식으로 쿼리문이 발생함

 

이런게 바로 N+1 문제!

해결 방법 -> Fetch Join!

연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능

(연관된 엔티티까지 영속성 컨텍스트에 전부 올려버림)

1) 쿼리문 작성

이번에는 프록시가 아닌 진짜 객체에 할일 목록이 저장됨

반복문을 순회할 때 (1차 캐시에) 데이터가 있어서 DB를 거치지 않고 데이터를 꺼내서 바로 반환하게 됨

최초에 관련된 데이터를 한꺼번에 가져와서 객체화 해줬기 때문! 

db를 거치지 않고 데이터 꺼내서 반환! 즉, 1개의 쿼리로 문제 해결

 

 

 

 

 

 

 

 

 

 

 

쿼리문이 나오는 로그를 보면서도 확인할 수 있음

 

크루와 할일 목록까지 전부 조회해서 가져오게됨

(위의 경우는 크루만 가지고 왔음)

 

N+1문제 해결!!!!

 

그렇다면 지연 로딩말고 즉시로딩을 쓰면 되나???

NO!

즉시 로딩에서도 N+1 문제는 발생할 수 있음

처음 쿼리를 만들 때, 크루에 연관관계가 있는 엔티티는 신경 안쓰고 조회 대싱이 되는 Entity 기준으로만 쿼리를 만듦

JPQL이 즉시 로딩 쿼리를 만들때는...

엔티티를 조회할 때 크루만 가져오게 되는데

이때 크루 엔티티와 연관된 엔티티가 있음을 확인하고

글로벌 패치 전략을 확인하고 fetch가 eager임을 확인하고

즉시 N번의 쿼리가 발생하게 됨...

쿼리 로그로 살펴보면

 

즉시로딩 최대한 사용하지말고, 지연 로딩 + fetch join 을 쓰자!

Spring Data JPA 기준, 데이터가 한꺼번에 많이 필요할 시, fetch join을 함께 쓰라고 하고 권자오딤

 

※ fetch join이란? JPQL에서 성능 최적화를 위해 제공하는 기능

연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능

 

 

그러면 또 드는 생각은 Fetch Join을 무조건 사용하면 N+1문제는 없나?

당연히 문제있음 side effect도 있음

 

대표적인 fetch join 문제상황은

OneToMany 관계에서 페이징 처리할 때, 

수달은 1명이지만 여러개의 할일이 있음

위와 같은 관계 수달 1 : 할일 N -> 한줄로 표현이 불가

위와 같은 db 테이블에서 전체 크루 150명 목록 중 5명만 가져와! 라고 하면

db에서 limit 5를 걸어서 데이터를 가져올 것임

But 고려해야하는 상황이 있음

수달부터 호프까지 5명이 있음, off set과 limit을 사용해서 row 기준으로 데이터를 추출함

이렇게 추출하면 5명을 기대했는데 2명의 데이터만  반환되는 문제가 생김

추가적인 문제는 클로저조회수확인 이외에도 아침일찍 운동 데이터는 누락됨!

꼬재의 데이터가 누락되게 됨

이런 문제들은 JPA가 알아서
fetch join이랑 페이징을 같이 하게 되면 

fetch join한 데이터를 일단 전부 다 가져와서 인메모리 (Ram, Heap 영역)에서 내가 원하는대로 가공하는 작업을 거치게 됨

위의 쿼리 로그와 함께 아래 에러 메세지도 나옴

데이터 전체 full scan해서 가져오고, 메모리에서 페이지 처리해서 문제가 발생한 것임

데이터가 100만건일 때, 이를 다 메모리에서 처리 할 수 없어서 과부하가 옴

그래서 조심하라는 에러

이런 문제 해결 방법은

 

ManyToOne일 때 페이징 처리를 해라! 혹은  @BatchSize()를 사용해라

 

 

 

 

 

 

 

 

JPQL이란? (Java Persistence Query Language)

엔티티를 대상으로 쿼리 작성

 

관련 영상

1. JPA 영속성 컨텍스트, JPA에서 프록시

2. ORM 관련 JDBC, SQL Mapper, ORM

 

 

 

 

 

 

참고

1. fetch join

https://kihwan95.tistory.com/12

 

페치조인(fetch Join )이란

페치(fetch)조인은 SQL에서 사용하는 조인의 종류는 아니고 JPQL에서 성능 최적화를 위해 제공하는 기능입니다. 이것은 연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능인데 join fetch 명령어

kihwan95.tistory.com

2. 오리의 N+1

 

 

 

 

1. DB 최적화

 

 

 

 

 

캐시란?

데이터나 값을 미리 복사해 놓는 임시 저장소

 

언제쓰나?

원본 데이터에 접근하는 시간이 오래 걸리는 경우

값을 다시 계산하는 시간을 절약하고 싶은 경우

 

 

왜쓰나?

캐시에 데이터를 미리 복사해 놓으면

계산이나 접근 시간 없이

 더빠른 속도로 데이터에 접근할 수 있음

 

 

'Spring' 카테고리의 다른 글

QueryDSL  (0) 2023.05.15
JPA의 Transactional  (0) 2023.05.15
Spring Security  (0) 2023.04.28