[데이터베이스] 조회 쿼리 성능 개선을 위해 노력했던 삽질...

Shinny·2022년 4월 11일
1

문제의 발단은 이러한 고민에서부터 시작되었다.

"프론트에서 요청한 데이터가 짜여진 DB 구조상 엄청나게 중첩되어 있을 때, DTO를 여러개 만들어서 프론트에 데이터를 주고 있는데, 이렇게 되면 쿼리가 여러개 날라가게 되잖아. 물론 불필요한 쿼리를 조회하는 것은 아니니 N+1 문제는 아니지만 이걸 하나로 합쳐서 해결할 수는 없을까?"

이러한 고민이다. 그래서 처음에는 JPQL로 해결을 하고자 했다. 하지만 거기에는 치명적인 문제가 있었으니, Left Fetch Join을 2개 이상의 OneToMany 자식 테이블에 선언하다보니

MultipleBagFetchException

이 발생했다. JPA에서 Fetch Join의 조건은 1. ToOne은 몇개든 사용가능, 하지만 2. ToMany 는 1개만 사용가능하다. 물론 N+1문제를 해결하기 위해서는 application.yml파일에서 default_batch_fetch_size 옵션을 줘서 해결할 수 있을 것 같다. 해당 옵션은 지정된 수만큼 in 절에 부모 key를 사용하게 해주니까 쿼리 수행 수가 1/n로 줄어들게 된다. 하지만 이건 N+1 문제가 아니다.

그래서 그 다음에는 MySQL에서 직접 쿼리를 짜는 것으로 생각을 했다. 하지만 나는 프론트가 요청한 특정 필드 값들만 뽑아서 줘야 했기 때문에 MySQL에서 그런 식으로 쿼리문을 짜는 것은 어려웠다.

그래서 QueryDSLDTO Projection이 있다는 이야기를 접하고 바로 QueryDSL을 공부했다. QueryDSL은 SQL과 JPQL을 코드로 작성할 수 있도록 도와주는 빌더 API이다. 이거면 내가 원하는 값들만 서브쿼리를 써서 뽑아올 수 있을 것 같았다. 그런데 계속해서

Nullpointerexception

이 떴다. 하루 이틀 삽질을 하면서 알게 된 것이, QueryDSL은 관계가 2 depth 이상 깊어질 경우 연관 관계를 초기화시켜줘야 한다고 한다. 그렇다. Deep Initializing이 필요한 부분에서 Entity의 필드값에 @QueryInit을 써서 어노테이션을 달아줘야 했던 것이다. 나의 경우 User -> Feed -> FeedDetail -> FeedDetailLoc 이렇게 자식관계로 매핑된 DB 구조였는데 FeedDetailLoc에서 해당 글 작성자인 User까지 타고 올라가려 하니 3번 테이블을 타고 들어가기 때문에 @QueryInit 어노테이션을 달아줘야 했던 것이다.
그런 다음 또 Inner Join으로 맺어줘야 하는 Table을 Left Join으로 해주는 바람에 또 NullPointerException이 떠서 또 거의 반나절을 날렸다. 그리고 나서 문제를 다 해결했다고 생각했는데 아뿔싸... OneToMany와 ManyToOne이 뒤엉킨 10개의 Table을 Join하는 과정에서 당연히 중복되는 데이터들이 반복되어서 뽑히는 현상이 일어났다. 당연히 처음부터 발견했어야 하는 문제였는데 QueryDSL에서 DTO Projection을 쓸 수 있다는 기쁨과 당장의 NullPointerException만 해결하면 된다는 생각에 사로잡혀서 가장 근본적인 것을 놓치고 있었다.

결국 이렇게 JPQL부터 QueryDSL까지 조회 쿼리 수를 줄여보겠다고 안간힘을 썼지만 결국 공부만 많이 하고 이뤄낸 성능 개선은 없었다. 결국 DB 인덱싱을 적용하여 속도면에서 성능 개선을 이뤄내는 방법 밖에 없었다.

(내 프로젝트에서 과감히 사라진 마지막 QueryDSL문. 처음으로 써 본 QueryDSL 적용을 못 해서 슬펐지만 많이 배울 수 있던 시간이었다...)

profile
비즈니스 성장을 함께 고민하는 개발자가 되고 싶습니다.

0개의 댓글