새소식

Welcome to the tech blog of Junior Backend Developer Seulbin Kang!

Backend

(BE 개발자 4개월차) 현업에서 고민한 DB I/O 줄이기

  • -

👀 들어가기 전에

이전 글에서 "쿼리 최적화는 이미 어느 정도 진행된 상태"라고 적어놨는데, 정작 그 내용을 따로 정리해둔 글이 없었다.💦

결국 다시 돌아가 내가 DB I/O를 어디에서 어떤 논리로 줄여나갔는지에 대한 과정을 적어보려 한다.

 

성능 최적화는 거창한 알고리즘보다 DB왕복만 줄여도 체감 자체가 크게 변화하는 경우가 많다.

이번 글은 그중에서도 쿼리 수를 줄이는 구조적 개선에 집중한 이야기를 다뤄보려한담.


👀 본론

책임의 분리 : 조회 API 하나가 너무 많은 책임을 지고 있다.

조회 API의 책임 시각 자료

 

MVP 단계에서 코드는 일단 되게 만드는 것을 목표로 빠르게 쌓인다.

나도 마찬가지였고 그 결과 하나의 트랜잭션 안에 다음이 한 덩어리로 섞여있었다. (보안 내용으로 예시로 작성한 점 양해부탁드립니답)

  • 사용자 검증 / 권한 검증
  • 페이지 조회
  • 기본 정보 조회
  • 파일 URL 파싱
  • 해시태그 로딩
  • 좋아요 카운트 로딩
  • 반환

겉으로 보면 하나의 전체 조회 API 흐름이지만 실제로는 정책 결정 / 데이터 접근 / 데이터 가공 / 조립이 모두 섞여있는 상태였다.

이 구조에서 문제가 되는 지점은 명확했다.

  • 조회 요구사항이 조금만 바뀌어도 서비스 메서드와 쿼리 호출 흐름이 동시에 변경된다.
  • Repository 호출이 분산되어 있어 쿼리 중복이 쉽게 발생한다.
  • 어떤 단계가 느린지 DB 접근 때문인지 조립 로직 때문인지 분리해서 보기가 어렵다.

즉, 성능 이전에 구조적으로 변경 비용이 높은 조회 API였다.


첫 번째 선택 : 조회 API의 책임을 분리한다.

책임분리 후 Layer별 작업

 

조회 API가 너무 많은 것을 알고 있는 것이 문제였다.

그래서 다음 기준을 세웠다.

  • 서비스 메서드는 흐름과 정책만 책임진다
  • DB를 직접 치는 로직은 한 번에 로딩하는 계층으로 모은다
  • 조회 결과 조립은 어플리케이션 메모리에서 수행한다.

이 기준에 따라 검증 로직을 제외한 모든 조회용 DB 접근을 하나의 Read Transaction 안에서 DataLoader로 묶었다.

이후 서비스 메서드는 이를 제외한 나머지들을 담당하도록 정리했다.

 

이렇게 되면서 서비스는 더 이상 데이터를 어떻게 가져오는지를 알 필요가 없어졌고, 조회에 대한 흐름의 책임만 가지게 되었다.

이 구조 덕분에 이후 조회 API 요구사항이 변경되었을 때, 서비스 로직 수정 없이 데이터 로딩하는 곳만 변경하여 대응할 수 있었다.


두 번째 문제 : 같은 구조의 쿼리가 왜 두번 이상 나가고 있을까?

구조를 정리한 이후, 실제 쿼리 호출 패턴을 로그 기준으로 다시 살펴보았다.

그 과정에서 발견한 문제는 아래와 같다.

게시글의 디테일(예 : 좋아요 수, 조회 수)을 가져오는 과정에서
해당 디테일의 아이디가 다르다는 이유로 같은 형태의 쿼리가 디테일마다 한 번씩 호출되고 있었다.

 

즉,

  • 좋아요 수 조회 쿼리 1회
  • 조회 수 조회 쿼리 1회

처럼 동일한 집합에 대해 Detail정보만 바꿔 쿼리를 반복 호출하는 구조였다.

각 쿼리는 빠를 수 있다. 하지만, 트래픽이 증가하면,

  • 네트워크 왕복
  • DB 실행
  • 결과 매핑

이 비용이 detail의 개수만큼 누적된다.

문제는 조회 시점의 DB 접근 단위가 디테일 기준으로 쪼개져 있다는 점이다.


해결 : 단건 조회를 요청 단위 배치 조회로 바꿔본다.

문제를 정의하고 나니 해결 방향이 보였다.

디테일 하나당 조회하려하지말고, 요청에 필요한 디테일 들을 한 번에 조회한다.

기존에는 다음과 같은 단건 조회 API를 사용하고 있었다. (예시입니다.)

변경 전 좋아요 수 & 조회 수

 

이를 아래와 같은 형태로 변경한 것이다.

변경된 조회

IN 기반 배치 조회로 전환하고 결과는 wallpaperId, detailId, count 형태의 Projection으로 반환받았다.

 

이후 서비스 계층에서 해당 결과를 Map으로 조립하여 다시 사용하도록 만들었다.

DB는 집합 단위로 데이터를 반환하고 어떤 detail이 어떤 wallpaper에 속하는지 어플리케이션에서 조립하도록 책임을 분리했다.

이번 변경으로 인해 조회 시점의 DB 접근 단위를 요청을 기준으로 변경하였고 이에 따라

  • detail의 종류가 늘어나더라도 Repository 메소드나 쿼리 호출 흐름이 늘어나지 않는다.
  • 최적화를 해야할 부분이 배치 조회 쿼리 하나가 된다.

즉 조회 API의 성능과 유지보수성이 구조적으로 안정된 상태가 되어가는 것이다.

성능 개선 결과


👀 마치며..

성능 최적화에는 진짜 정답이 없는 것 같다.

끝도 없이 코드를 검토하고, 쿼리문을 검토하면서 계속해서 고민하다보면

어디를 바꿔야할지 어떻게 바꿔야 좋을지가 감이 오게 되는 것 같다.

 

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.