5차 요구사항은 쿼리 성능 개선이었다.
주요 요구사항은 다음과 같았다.
- 핵심 테이블에(핵심 기능에서 read/write 되는 테이블) 대량의 데이터를 쌓고 성능을 개선한다.
- 데이터를 10만/100만건 생성한 뒤, 쿼리 성능을 테스트한다.
- 대량의 데이터에서도 기능이 잘 동작할 수 있도록, 쿼리 성능 개선을 위한 인덱스를 설정한다.
우리 서비스 중 요구사항을 받고 가장 고민이었던 API는 바로 "검색"이었다.
데이터의 갯수가 크지 않은 현재 운영 서버에서 평균 API 처리 속도가 10초를 넘어갔었다.
또 100만건의 템플릿 더미데이터를 넣었을 경우에는 평균 26초에서 많게는 35초가 넘어가는 성능을 보였기 때문에 사용성에 있어서 치명적이라고 생각했다.
나 같은 경우에도 사이트 렌더링 속도가 5초 이상 또는 그 이하여도 답답함을 참지 못하는데, 35초면 진즉에 탈주했을 것 같다. ㅋㅋ
따라서 이 API를 개선하는 것이 우리의 가장 큰 목표였다.
현재 발생되는 상태 확인
검색 조건
템플릿 검색은 /templates 엔드포인트에 조건을 통해 이루어진다.
조회 가능한 조건으로는 다음과 같다.
- 회원
- 해당 회원이 작성한 템플릿
- 카테고리
- 해당 카테고리가 등록된 템플릿
- 태그
- 해당 태그를 포함한 템플릿
- 키워드
- 템플릿 제목, 설명, 소스 코드의 내용, 소스 코드의 파일명에 키워드를 포함한 템플릿
조회하는 리소스
템플릿을 조회할 경우 필요한 정보는 각 템플릿의 작성자 정보와 제목을 포함하여 썸네일으로 보일 소스 코드와 템플릿에 등록된 태그 등이 표시되어야 한다.
따라서 발생하는 조회 메서드는 다음과 같았다.
- 템플릿 조회
- 템플릿에 해당하는 태그 조회
- 템플릿의 썸네일인 소스 코드 조회
- 템플릿 작성자 정보 조회
총체적 난국 🥲
사실 기존 API 구현에는 굉장히 많은 문제점이 존재했다.
문제를 보기 전에 먼저 전체적인 템플릿 조회(= 검색) 과정을 살펴보면 다음과 같다.
- 템플릿 조회 ➡️ 1번 발생
- 템플릿 검색 (1번 발생)
- 카테고리 조회 (카테고리 갯수만큼 N번 발생)
- 조회한 템플릿 갯수 조회 (Page 타입의 반환을 위해 total elements 갯수 조회)
- 템플릿에 해당하는 태그 조회 ➡️ 템플릿 갯수만큼 발생
- 템플릿 태그 조회 (템플릿 당 1번 발생)
- 각 템플릿의 태그 조회 (각 템플릿의 태그 갯수만큼 조회)
- 템플릿의 썸네일인 소스 코드 조회 ➡️ 템플릿 갯수만큼 발생
- 템플릿 썸네일 조회 (템플릿 당 1번 발생)
- 각 템플릿의 썸네일인 소스 코드 조회 (각 템플릿의 태그 갯수만큼 조회)
- 템플릿 작성자 정보 조회 ➡️ 템플릿 갯수만큼 발생
- 템플릿 작성 회원 조회 (템플릿 당 1번 발생)
# 1.1 템플릿 검색 (1번 발생)
# 🍀 검색 조건에 따라 다르게 발생
# 1.2 카테고리 조회 (카테고리 갯수만큼 발생)
select
c1_0.id,
m1_0.id,
m1_0.created_at,
# ... 생략
from
category c1_0
join
member m1_0
on m1_0.id=c1_0.member_id
where
c1_0.id=?
# 1.3 템플릿 갯수 조회 (1번 발생)
# 🍀 조건에 따라 다르게 발생
# 2.1 템플릿 태그 조회 (1번 발생)
select
tt1_0.tag_id,
# ... 생략
from
template_tag tt1_0
where
tt1_0.template_id=?
# 2.2 각 템플릿의 태그 조회(태그 갯수만큼 조회)
select
t1_0.id,
# ... 생략
from
tag t1_0
where
t1_0.id=?
# 3.1 썸네일 조회(1번 발생)
select
t1_0.id,
t1_0.created_at,
# ... 생략
from
thumbnail t1_0
where
t1_0.template_id=?
# 3.2 소스코드 조회(소스코드 갯수만큼 발생)
select
sc1_0.id,
sc1_0.content,
sc1_0.created_at,
# ... 생략
from
source_code sc1_0
where
sc1_0.id=?
# 4.1 템플릿 작성 회원 조회(템플릿 갯수만큼 발생)
select
m1_0.id,
m1_0.created_at,
# ... 생략
from
template t1_0
join
member m1_0
on m1_0.id=t1_0.member_id
where
t1_0.id=?
총 템플릿 갯수가 N, 총 카테고리 갯수가 C, 템플릿 당 평균 태그 T, 템플릿 당 평균 소스코드 SC일 때
(1 + C + 1) + N * { (1 + T) + (1 + SC + 1} ➡️ O(N * (C + T + SC))
불필요하게 작성자 정보를 조회하는 것부터 N + 1, 그리고 사용한 쿼리 문의 성능 문제까지...
하나씩 뽀개보자 🔥
1. Outer Serivce로 인한 불필요한 작성자 정보 추가 조회
Template Entity는 다음과 같이 구성되어 있다.
Template이라는 도메인은 우리 서비스의 중심이 되는 매우매우 크고 핵심인 도메인이다.
복잡하고 큰 중심 도메인 때문에 Facade 패턴을 도입하려고 했으나 실상은 Facade 라기보다는 Outer Serivce에 가까웠다.
사실 엔티티를 확인하고 조회 로직을 보면 오잉할 수 있는 것이 있다.
- 템플릿 조회
- 템플릿에 해당하는 태그 조회
- 템플릿의 썸네일인 소스 코드 조회
템플릿 작성자 정보 조회
바로 4번, 템플릿 엔티티가 작성자인 Member를 가지고 있음에도 조회하는 로직이 한번 더 들어간다는 것이다.
OuterService를 구성하면서 생긴 레거시이기 때문에 OuterService를 제거하고 최상위 Facade만 남겼다.
기존 회원을 다시 조회하는 로직을 함께 제거하고 Template 조회 시 fetch join을 통해 Member까지 함께 조회할 수 있도록 개선하였다.
2. N + 1 이 발생하지 않는 곳이 없다.
그 다음으로 개선해야 할 곳은 N + 1이었다.
앞의 쿼리문을 통해 확인할 수 있겠지만 남겨진 3가지 단계 (템플릿 조회, 템플릿에 해당하는 태그 조회, 템플릿의 썸네일인 소스 코드 조회)에서 전부 N + 1이 발생하고 있었다.
따라서 각각 fetch join을 통해 N + 1을 개선하였다.
@Query("""
SELECT tt, t
FROM TemplateTag tt
JOIN FETCH tt.tag t
WHERE tt.id.templateId = :templateId
""")
List<TemplateTag> findAllByTemplate(Long templateId);
개선된 모습은 다음과 같다.
3. 한번에 조회할 수 있으면 한번만 호출하자
검색된 템플릿들의 응답을 만드는 로직은 검색하여 템플릿 목록을 조회하고 makeTemplatesResponse를 호출하는데
makeTemplatesResponse는 조회한 template 하나하나를 순회하면서 템플릿 태그와 썸네일을 다시 조회했었다.
따라서 템플릿 태그와 썸네일은 template 수만큼 호출되었다.
현재 페이징의 기본 설정이 20개씩 조회를 하기 때문에 20 + 20 총 40개의 쿼리가 태그와 썸네일을 조회하는데 발생하는 것이었다.
이 부분을 개선하고자 템플릿 목록을 통해 하나라도 해당하는 리소스를 전체 조회해왔다.
@Query("""
SELECT tt, t
FROM TemplateTag tt
JOIN FETCH tt.tag t
WHERE tt.id.templateId in :templateIds
""")
List<TemplateTag> findAllByTemplateIdsIn(List<Long> templateIds);
그 후 서비스단에서 템플릿에 해당하는 리소스를 필터링하여 찾았다.
최종적으로 쿼리는 총 O(N * (C + T + SC))개에서 3개로 개선되었고 평균 속도는 26초 대에서 16초 대로 개선되었다.
'우아한테크코스 6기 > 4단계' 카테고리의 다른 글
JPA에서 Full Text Index 사용하기 (+ 테스트 시 주의점) (0) | 2024.10.01 |
---|---|
API 성능 개선하기 2탄 (feat. 검색 전문 인덱스 적용하기) (1) | 2024.09.28 |
ApplicationContext vs ServletContext (0) | 2024.09.17 |
Tomcat의 Coyote와 Catalina에 대해 알아보자 (1) | 2024.09.08 |
Tomcat의 전체 구조와 역할에 대해 알아보자 (0) | 2024.09.07 |