전체 조회시 속도 개선을 위한 쿼리 최적화를 하기 위해 이전 프로젝트에서 했던 것처럼 fetch join을 도입했다.
현재 프로젝트에는 Pagination이 구현되어 있었고, fetch join을 통해 1:N 연관관계인 테이블들을 조인하려는 시도를 했다.
그러나 이렇게 하면 문제가 발생할 수 있다고 한다. 어떤 문제인지, 그리고 해결법은 무엇일지 알아보았다.
📌쿼리 튜닝시 fetch Join을 쓰는 이유
우리는 보통 일대다 , 다대일 연결시 fetchType을 LAZY, 즉 지연로딩으로 설정한다.
이렇게 되면 한쪽 객체를 조회할 때 연관된 객체는 일단 Proxy 객체로 가져온 후 연관 객체가 실제로 필요할 때 select 쿼리를 따로 날려서 가져온다. 이렇게 하면 굳이 필요 없는 객체의 조회를 막을 수 있지만,
결국 연관된 객체를 조회해야 하는 경우에는 N번만큼의 쿼리가 추가로 발생하는 N+1 문제가 발생할 수 있다.
이렇게 쿼리를 여러번 날리면 조회 속도가 낮아진다.
따라서 이때 fetch join을 사용하면 관련된 테이블도 한번에 조회해서 가져오므로 N+1 문제를 해결할 수 있다.
그러나 이런 fetch join에도 단점이 있다.
📌fetch Join의 단점
1. join 되는 테이블에는 as문을 적용할 수 없다. 따라서 별칭을 사용할 수 없다.
2. 데이터의 갯수가 예상치 못하게 증가해서 나올 수 있다. 컬렉션 * 컬렉션을 가져올 수 없다.
Fetch join은 일다다 조인에서 사용된다.
일대다 조인의 목적은 One을 기준으로 Join하는 것이다.
그러나 DB는 Many를 기준으로 join하므로 데이터가 many의 개수만큼 생성된다.
이처럼 결과 데이터의 증가로 인해, 데이터의 순서가 뒤틀리게 된다.
또 두가지 컬렉션을 묶어서 사용하면 다음과 같은 에러를 만날 수 있다.
org.hibernate.loader.MultipleBagFetchException 예외는
Hibernate에서 발생하는 예외로, 여러 개의 컬렉션을 동시에 가져올 수 없다는 것을 나타낸다.
3. Paging을 사용할 수 없다.
Pagination 에서 limit 기능을 함께 쓰는데, 이걸로 우리는 모든 데이터를 가져오지 않고, 지정된 갯수만큼 가져오면서 메모리의 과부하를 막을 수 있다.
그러나 이 때 fetchjoin을 하게 되면 모든 데이터를 가져와 메모리에 적재 후 애플리케이션단에서 limit작업을 수행한다.
따라서 out of memoryt 위험이 있을 수 있고, Pagination의 장점인 limit 기능도 결론적으로는 사용하지 못하게 되는 것이다.
프로젝트 코드에서도 fetch join 후 Pagination 조회를 시도했을 때
HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!
라는 경고문이 생성되며 작업이 진행되었다.
📌대안점 Batch size
배치 사이즈를 지정해서 가져오면 다음과 같은 where절이 생긴다.
One 테이블의 아이디 엔티티를 지정해준 배치 사이즈만큼 Where절의 in으로 묶어 가져오는 것이다.
여기서 in절에 들어가는 ?,?는 resultrepo 테이블과 ManyToOne으로 연결된 One 테이블의 seq이다.
예를 들어 배치 사이즈가 100 이고, One 테이블의 seq가 1000까지 있다고 해보자
먼저 One 테이블을 조회하고 (+1번의 쿼리)
1000번까지 있는 One 테이블의 seq를 100개씩 끊어서 Many 테이블을 조회한다.(+10번의 쿼리)
원래는1001번 나갔어야 할 쿼리를 Batch size를 지정해 줌으로써 11번으로 줄일 수 있다.
이렇게 하면 N+1 문제를 어느정도 해결하면서 Pagination의 기능도 사용할 수 있다.
배치 사이즈는 application.yml에서 다음과 같이 설정할 수 있다.
jpa:
properties:
hibernate:
default_batch_fetch_size: 지정할 크기
📌Batch size vs. Fetch join vs. 아무것도 안한 쿼리의 차이
조건 :
program(=One) resultRepo(=Many) 연결되어 있는 program(=One)테이블의 페이지네이션 조회
결과 :
🔎아무것도 안한 쿼리
연관되어있는 resultReport(=Many)를 조회하는 쿼리를 program(=One) 엔티티의 갯수만큼 더 날렸다.(N+1)
🔎배치 사이즈를 지정한 쿼리
program을 조회한 후 resultReport를 조회하는 쿼리가 where절로 묶이면서 program의 id로 조회하는 것을 볼 수 있었고, N번에서 1번으로 줄었다.
🔎fetch join까지 더한 쿼리
해당 데이터만 조회하는데 관련 select 쿼리가 1개로 줄면서 program을 조회할 때 테이블에 연관된 테이블의 컬럼까지 한번에 조회하는 것을 볼 수 있었다. (out of memory 문제가 생길 수 있음)
📌조회속도 비교
fetch join을 한 경우와 하지 않은 경우 모두 100개의 데이터에 대해 10번씩 조회를 실행하였다.
두 경우의 조회 시간이 10ms로 거의 비슷했고,
오히려 fetch join을 하지 않고 Batch size만 지정해 준 경우의 최단 조회 시간이 더 짧았다.
사실 데이터 만건정도는 넣어야 유의미한 조회 속도 차이가 나지 않을까 싶다.
추후에 bulk data를 넣어놓고 다시 비교해 볼 예정이다.
📌결론
완벽한 정답은 없다. 모든 방법에는 장단점이 있고, 그를 인지하고 활용할 줄 아는 것 중요하다는 것이 결론이다.❗❗
N+1 문제를 해결하기 위해 fetch join이 하나의 방법이 될 수 있다.
그러나, ORM 을 적절하게 사용하려면 Transaction 에서 필요한 데이터만을 DB 에서 꺼내와서 써야 하는데,
fetch join 은 문맥에서 필요하지않은 데이터까지도 불러올 수 있다는 단점을 가지고 있다.
따라서 검색조건처럼 필터링은 필요하지만 실제 연관관계에 있는 테이블 데이터가 필요하지않은 문맥에서는
join 문을 통해서 쿼리를 생성하거나, Batch size를 지정하는 등 다양한 방법 중 가장 효율적인 방법을 찾아서 쓰는 것이 좋겠다.
'Spring' 카테고리의 다른 글
Spring 프로젝트에서 웹소켓 적용하기~!! 1탄 (5) | 2024.06.10 |
---|---|
Google Oauth2 (0) | 2024.04.02 |
Hibernate spatial이란? / 간단한 쿼리 메서드 (0) | 2023.07.12 |
[프로젝트]알림 기능 구현(SSE, Spring AOP) (4) | 2023.07.03 |
[프로젝트] Spring AOP로 로그 구현 (0) | 2023.06.29 |