본문 바로가기
데이터 접근 기술/JPA

N+1 문제 해결

by oncerun 2022. 10. 29.
반응형

JPA를 사용하기 위해 여러 공부를 하는 도중에 문제가 되는 상황을 발견했다.

 

도메인을 확인하고 테이블과 엔티티를 설계하는데 다대다 관계를 조인 테이블로 풀어 조인 테이블을 엔티티로 승격시켜 사용하는 것까지는 좋았다.

 

하나는 양방향 매핑이 필요해서 따로 작업을 해주고 나머지는 단방향 매핑으로 진행하고 나중에 필요하면 양방향으로 변경하자고 맘을 먹고 각종 테스트까지 마친 상태에서 테이블에 데이터를 넣고 객체 그래프 탐색을 통해 모든 데이터를 끌어올려 API로 전달하는 과정에서 문제가 발생했다.

 

간단히 문제가 발생한 부분을 설계하면서 느낀 점을 풀자 하나의 부분 모양이다.

 

LucidChart는 부분 무료라.. 일대일 관계 Crow's Foot이 안되네요 .

 

 

 

현재 A, B에 관한 관계는 OneToOne이 현재 비즈니스 로직적으로 맞았지만 과거 기획에는 일대다 관계가 맞다고 생각했다. 

그래서 우선적으로 OneToOne으로 엔티티 작업을 진행했다.

 

B, C의 양방향은 조금 더 생각할 여지가 있다. B 자체를 조회할 경우가 있는지 조금 더 따져보고 그렇지 않다면

B -> C 단방향으로 진행해도 무리 없어 보인다. 과도한 양방향 매핑은 복잡성만 가중시킨다는 느낌이 든다. 

 

이번 목표는 모든 데이터를 연관하여 전부 가져오는 것이었다.  시작점은 b이었고 A, C, D정보가 전부 필요했다.

 

1) B를 조회 (LAZY에서 EAGER)

 

단순히 B만을 조건을 처리하여 가져왔다. 그 이후 B에서부터 객체 그래프 탐색을 통해 전부 가져와 데이터를 사용하여 응답했다.  (Fetch.type은 toOne은 Lazy로 설정했다. )

 

그 결과는 끔찍했다. B에 연관된 C, A가 프락시 객체가 되어 엔티티 매니저에게 초기화를 하는 과정에서

수많은 SELECT문이 발생했고 이는 곧 성능 저하로 이어졌다. 

 

이 당시 Spring-Data-Jpa를 사용했고 쿼리 메서드로 가볍게 가져왔었다.  그래서 JPQL로 join 해서 가져오자고 생각했다. 

하지만 join문도 결국 fetch.type 설정 때문에 프락시를 가져왔고 효과는 없었다. 

 

그래서 Eager로 타입을 변경하고 조회했지만 하이버네이트가 만든 쿼리를 보면 전혀 예상할 수 없는 쿼리가 발생되었다.

 

그래서 방법을 찾아보았다.

 

2) Fetch join, @BatchSize

 

JPQL에서 성능 최적화를 위해 제공하는 기능으로 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능이다.

 

join fetch 명령어를 사용한다.

 

모든 데이터를 즉시 사용해야하는 상황에서는 지연 로딩이 별 효과가 없었다. 즉시 접근해서 데이터를 세팅한 후 응답했어야 했기 때문이다. fetch.type을 건드는 것은 발생되는 쿼리를 예상할 수 없다는 단점 때문에 사용하지 못했다. 

 

그래서 fetch join을 통해 데이터를 가져와 모두 초기화하는 것으로 했다. 

 

@Query("select distinct b 
          from B b 
          join fetch b.a 
          join fetch b.c as c 
          join fetch c.d")

 

일대다 조인이기 때문에 distinct로 중복되는 b엔티티를 줄여 원하는 수의 결과를 얻었다. 

 

조회 쿼리 한정으로 별칭을 사용했다.  조회 쿼리이기 때문에 조건에 join fetch 대상인 d를 통해 조건문을 사용할까도 고민했지만 이는 불안정한 결괏값을 엔티티에 매핑하는 것과 다름이 없어 b만을 사용했다. 

 

애플리케이션에서 d에 대한 조건으로 필터링하여 처리했다. 

 

BatchSize를 글로벌 설정으로 100 정도 두었고 b <-> c 관계에 대해 사용할 때 성능 최적화로 사용하기로 했다. 

 

여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면, 페치 조인보다는 일반 조인을 사용하고 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적이다.

 

 

Distinct

 

distinct를 활용하여 애플리케이션에서 중복을 줄인다는 말을 다르게 말하면 데이터베이스에서 다 테이블 결과에 맞춘 모든 결과를 전송한다는 말과 같은 말이다.

 

이는 결국 성능에 영향을 미치게 할 수 있다.  

 

다시 생각해보니 컬렉션을 fetch join 하는 것보다 글로벌 batchsize 설정을 통해 지연로딩을 사용하는 것도 나쁘지 않다고 생각이 든다.  

 

한 번에 전부 가져오는 비용과 나눠서 가져오는 비용 중 선택한다면 데이터 수를 고려해야겠지만...

선택은 항상 어렵지만 이를 결국 테스트를 통해 적절하게 선택하는 것이 좋아 보인다. 

 

여기서 이게 좋다 저게 좋다 정할일이 아닌 것 같다는 생각이 든다.

 

 

 

반응형

'데이터 접근 기술 > JPA' 카테고리의 다른 글

JPQL Pagination  (0) 2022.10.30
벌크연산  (0) 2022.10.29
나는 지금 JPQL이 필요하다! (2)  (0) 2022.10.26
나는 지금 JPQL이 필요하다!  (0) 2022.10.26
주말 정리  (0) 2022.10.24

댓글