진행중인 프로젝트에서 많은 데이터를 필요로 했고, 약 40만개의 데이터를 수집했다. 별 문제 없을 거라 생각했지만, 큰 오산이었다. 기존 무한스크롤로 끊김없이 보이던 리스트는 버벅이기 시작했고, 40만개에 이르자 api 요청 한번에 길게는 6~7초가 걸렸다.
현재 프로젝트에서 사용중인 조회 api는 jpa specification을 이용한 다중 필터, page리턴, 여러 조건에 의한 정렬을 사용한다.
이정도 규모의 데이터를 다뤄본 적이 없기에, 어디에서 시간이 오래 걸리는 지 또한 알 수 없었다. 때문에, 몇가지 가설을 세우고 차례대로 검증해보기로 했다.
- 다수의 onetomany 필드로 인한 많은 수의 쿼리 실행
- 복잡한 필터 구조 + 대용량 데이터로 인한 쿼리 실행 시간 증가
- 대용량 데이터로 인한 정렬 시간 증가
결과부터 말하자면 3번이 정답이었다. 앞으로는 가설을 검증한 과정과, 문제를 어떻게 해결했는지, 그 과정에서 무엇을 얻었는지 적어보려 한다.
조회할 Entity에는 4개의 onetomany field와 하나의 onetoone field가 있었고, 이 부분에서 쿼리가 많이 나갈 수 있다고 생각했다.
하지만 loading 방식을 몰랐기 때문에 한 오해였다.
onetomany 방식은 기본적으로 lazy loading 방식으로 작동하기 때문에, 해당 객체를 조회하지 않는다면 쿼리가 나가지 않았다.
또한 onetoone field는 기본적으로 eager loading 방식이기 때문에 쿼리가 한번만 실행되고, 큰 부하가 걸리지 않았다.
이 부분을 구글링하며 onetoone field를 lazy field 로 해두고 필요한 메소드에서 Entity Graph를 붙여 사용하려 했으나, 왠지 모르게 어떻게 해도 계속해서 eager 방식으로 작동했다.
고민하다 찾아보니, onetoone 매핑에서는 매핑의 주인(mapped by)만 lazy loading을 사용할 수 있었다. 자세한 이유는 조금 더 공부해봐야겠다.
현재 조회 쿼리에서는 10개 이상의 파라미터를 받아 복잡한 필터를 거쳐 페이지 형식으로 리턴하는데, 데이터량이 많아지다 보니 필터 혹은 페이지를 만드는 시간이 증가하는 것이 아닐까 생각했다.
결국 이 부분을 테스트해보다 문제점을 발견했는데, 페이지만 리턴해보고, 필터만 해보고, 필터 후 페이지까지 해봤는데 큰 문제가 없었다. 문제가 되는 부분은 정렬 부분이었다..(QueryDSL로 다 바꿔야 하나 심각하게 고민하고 있었다..)
결론적으로 데이터가 많아지며 정렬에서 시간이 오래 걸린다는 것을 알았다. 실제로 sql query를 직접 날려보며 테스트해보니 정렬만 해도 시간이 4초 이상 걸렸다.
대부분의 개발자가 그렇듯, 오랜 시간 구글링을 통해 해답을 얻을 수 있었는데, 키워드는 보조 인덱스였다.
db에서 인덱스를 사용하면 삽입, 수정, 삭제에는 더 많은 시간이 소요되지만, 조회 시간은 비약적으로 감소시켜줄 수 있었다. 때문에 수정이 잘 되지 않는 칼럼을 인덱스로 선정하면 조회 및 정렬 단계에서 많은 시간적 이득을 볼 수 있었다.
많은 물건 중 필요한 물건은 진행중인 물건이었기에, 진행 상태를 보조 인덱스로 선정하고, 조회를 할 때 where progress = '진행중'과 같은 식으로 범위를 좁힌 후 정렬을 진행하면, 5~6초가 걸리던 쿼리가 0.2초만에 완료되는 것을 볼 수 있었다.
지금껏 겪어보지 못했던 문제를 겪고, 많은 생각을 해보며, 더 잘하는 개발자였다면 어땠을까 하는 생각을 했다. 아마도 훨씬 일찍 문제를 파악하고, 해결했을텐데 더 열심히 성장해야겠다는 동기를 얻었다. 그래도 이렇게 하나씩 깨우치다보면 문제를 마추져도 저번에 그 문제네? 하고 쉽게 해결하는 날이 오지 않을까 하는 생각을 해본다.