API 성능 50배 개선기
오늘은 내부 임직원이 사용하는 어드민 제품의 목록조회 기능을 평균 5~8초에서 100ms 미만 응답속도를 가지도록 성능 개선한 이야기를 공유해볼까 한다.
이야기에 앞서, 먼저 해당 제품으로 이뤄지는 업무 절차에 대해 간략히 소개해보겠다.
계좌를 만들기 위해서 고객의 정보를 확인해야 하는 절차가 있다.
그 절차 중 일부 고객은 어드민에서 승인을 해야 계좌를 개설할 수 있는 경우가 있다.
이번 개선기는 계좌개설 과정에서 승인이 필요한 대상들을 불러오는 목록 리스트 성능 개선 이야기이다.
이 업무를 앞으로 '검토 업무'라고 부르겠다.
검토업무는 고객의 계좌개설 대기 시간을 최소화하기 위해 빠르게 검토가 이뤄져야 한다.
검토가 필요하게 되면 검토건이 생성되고, 검토건은 검토대기, 검토 중, 검토완료 3가지 상태가 존재한다.
업무의 핵심은 검토대기의 있는 건들을 빠르게 검토완료하는 것이었다.
해당 업무는 비교적 최근에 새롭게 오픈된 기능이었다.

# 문제 파악
기본적으로 1초 이상 소요되는 API들은 slow alert을 보내도록 슬랙 설정을 해둔 상태였다.
어느 날 모니터링 중, 특정 API들에서 지연이 발생하는 걸 감지했고 살펴보니 이 검토 업무 관련 API들이었다.
검토업무는 속도가 중요하기 때문에, 그냥 지나칠 수 없었다.
게다가 오픈한 지 얼마 안 되어서 데이터 수가 그렇게 많지 않았음에도 벌써부터 적신호가 온다는 것은 개선이 필요하다고 직감하였다.
보다 자세한 원인분석을 위해 메트릭을 상세히 수집하기로 결정했다. Spring actuator에서 제공하는 메트릭 어노테이션을 통해 의심 가는 API들에 쉽게 수행시간 메트릭을 심을 수 있었다.
수집한 메트릭은 프로메테우스를 통해 집계되어 그라파나를 통해 시각화하여 확인했다.
3일 정도 수집하며 확인해 본 결과, 가장 중요한 검토대기의 목록조회가 5~8초 정도 소요되고 있음을 파악할 수 있었다.
# 원인 분석
문제가 되는 API들을 찾았으니, 원인 분석에 들어갔다.
먼저 설명에 앞서 이해를 위해 검토건의 테이블 설계를 살펴보자.

고객제출정보 테이블은 모든 계좌개설을 시도하는 고객에 대하여 생성된다.
검토정보는 검토가 필요한 고객에 대해서만 생성되고 검토 중, 검토완료 상태정보를 저장하는 1:1 구조였다.
여기서 검토정보 테이블은 검토를 진행했을 때만 생성이 됐었다.
즉, 검토대기 건들을 불러오기 위해서는 고객제출정보와 검토정보를 left join 해서 불러오고 있었다.
left join을 하다 보니 인덱스를 이용하지 못하고 풀스캔을 하고 있는 걸 발견하였다.
# 1차 성능 개선 시도 - 쿼리튜닝
풀스캔 하는 부분을 CTE랑 서브쿼리로 인덱스를 잘 태우도록 바꿔봤지만... 눈에 띄는 성능 개선효과를 볼 수 없었다.

설명을 위해 테이블 구조를 간략하게 소개했지만 실제로는 더 많은 테이블들을 조인하고 복잡했다.
그래서 구조적으로 서브쿼리를 사용하지 않을 순 없었다.
(가장 비용이 높은 materialize view를 만드는 부분을 쿼리로 해소하기는 한계가 있었다.)
쿼리튜닝으로는 한계가 있음을 깨닫고 다른 개선 방향을 찾아보았다.
# 2차 개선 시도 - 구조 변경
결국 left join으로 인한 view생성 부분에서 모든 검토정보를 불러오는 게 비용이 크다고 판단했다.
따라서 인덱스를 보다 효율적으로 태울 수 있도록 left join 자체를 없애는 방식으로 구조를 개선해보고자 했다.
left join을 통해 검토대기건을 불러오는 이유는, 검토대기 상태일 때는 검토정보가 존재하지 않았고, 목록을 보여주기 위해선 검토정보 테이블 칼럼 정보들이 필요한 상태였기 때문이다.
따라서 검토대기 상태에서도 검토정보가 생성되도록 구조를 변경하면 큰 개선효과를 가져올 수 있을 것 같았다.
그래서 우선 리스트 조회 쿼리를 inner join으로 바꾸고 실행계획을 확인했다.

눈에 띄게 비용이 줄어든 걸 확인할 수 있었다!
하지만 inner join으로 바꾸는 작업은 기존 설계 구조를 많이 변경해야 하기 때문에, 변경범위가 대고객 부분까지 광범위하게 수정작업이 필요했다.
적은 공수는 아니었지만 드라마틱한 성능 개선 효과가 기대되기에 강행하기로 결정했다.
# 결과
결과적으로 5~8초 걸리던 목록조회가 100ms 이내로 개선되었다!!!

제품을 사용하시는 어드민 분들에게도 좋은 피드백을 받을 수 있었다 ㅎㅎㅎ

제품을 만드는 서버 개발자로서 이상징후를 간과하지 않고 빠르게 캐치, 개선하여 사용자에게 좋은 경험을 줄 수 있었던 보람 있는 경험이었다.