728x90

정의

N + 1 보다는 1 + N이라고 볼 수 있는데

한 개의 쿼리에 예상과는 다른 N개의 추가 쿼리가 발생하는 문제다.

 

원인

즉시로딩, 지연로딩, 연관 관계와 상관 없이 언제든 N + 1 문제가 발생할 수 있다

 

즉시로딩의 경우

N:M 관계에서 N을 조회하는 쿼리를 날렸을 때

M을 즉시로딩으로 가져오게 되어있다면

N을 조회할 때 M을 한 번의 쿼리만 날려서 조회할거라 예상할 수 있지만

JPQL은 N을 먼저 조회하는 쿼리를 날린 후에

M을 즉시로딩으로 가져오게 설정된걸 확인하고

M을 가져오는 추가 쿼리를 날리게 된다.

 

일대일 관계라면 쿼리가 한 번이 아닌 2번 발생할 것이고

일대다 관계라면 1 + M개만큼의 쿼리가 발생한다.

 

지연로딩의 경우

N:M 관계에서 N을 조회한 후에

지연로딩으로 가져온 M의 객체들을 사용하려는 경우에는

실제 객체가 아닌 프록시 객체가 존재하는 상태이기 때문에

실제 객체를 조회하기 위해 쿼리문이 M번 더 발생하게 된다.

 

 

해결 방법

확실한 정답은 없지만 가장 간단한 해결 방법은 패치 조인을 사용하는 것이다.

 

기본 패치 타입이 즉시로딩인 경우에는 모두 패치 타입을 지연로딩으로 바꾼 후에

연관된 엔티티를 사용해야하는 경우에는 애초에 패치 조인으로 한 번에 가져오는 것이다.

 

물론 이 방법도 문제점이 존재하는데

대표적인 경우가 페이징 처리시 발생하는 문제다.

 

예를 들어 1페이지 당 10개씩 끊어서 페이징을 처리한다 했을 때

100만개의 데이터가 있다면 이 데이터를 모두 메모리에 가져온 후에

페이징 처리를 하게 되기 때문에 데이터의 양이 적다면 메모리 초과가 발생하진 않겠지만

데이터가 많다면 무조건 메모리 초과가 발생할 것이다.

 

일대일, 다대일 관계라면 몇 개의 데이터를 조회할지 측정이 가능하지만

일대다, 다대다 관계라면 정확한 측정이 어렵다.

 

이런 문제를 해결하기 위해서는 배치 사이즈를 조절하는 방법이 있는데

배치는 지연 로딩을 생각하면 이해하기 쉽다.

 

배치 사이즈가 적용되면 같은 쿼리

 

예를 들어, 배치 사이즈가 100이라면 100만 개의 데이터 중에서 100개의 베치를 가져온 후에

이후의 배치가 필요한 경우에는 IN 쿼리를 한 번 더 날리게 된다.

 

사실 함정이 한 가지 있는데 하이버네이트가 최적화를 해두었기 때문에

배치 사이즈를 100으로 설정했다고 100개씩 끊어서 쿼리를 날리진 않는다.

100
100/2 = 50
50/2 = 25
25/2 = 12
1 ~ 10

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 25, 50, 100]

 

배치 사이즈가 100인 경우에는 위와 같이 미리 여러 개의 prepared statement를 만들어 두는데

기본값인 legacy 전략은 데이터보다 적은 수를, padded 전략은 많은 수를 쿼리로 가져온다.

 

예를 들어, 조회하려는 데이터가 총 32개라면 in(?*25)와 in(?*7) 두 개의 쿼리가 날라가고

padded 전략이라면 in(?*50) 한 개의 쿼리가 날라간다.

 

이렇게 최적화를 해주는 이유는 배치 사이즈가 100이라고 in절을 in(?*1) 부터 in(?*100)까지

다 만들어두는 것은 성능적으로 비효율적이기 때문이다.  

 

'Back-End > Spring' 카테고리의 다른 글

스프링 면접 질문 정리 : 오브젝트와 의존관계  (0) 2024.04.03
[로깅] MDC를 이용한 식별된 로깅 처리  (1) 2024.03.05
슬라이스 테스트  (0) 2023.07.05
예외  (0) 2023.06.15
템플릿 정리  (0) 2023.06.14

+ Recent posts