조회 성능을 개선해보자 - 1편

WillyByun·2025년 8월 14일

조회 API는 내부에서 무엇을 하고, 병목은 어디서 생기나?

1) 요청 1건이 지나가는 길(End-to-End 경로)

사용자(브라우저/앱)가 GET /api/v1/products?brandId=...&sort=likes&page=0&size=20를 보냈을 때, 보통 이렇게 흐릅니다.

  1. 클라이언트 네트워크: DNS 조회 → TLS 핸드셰이크(최초) → 요청 전송
  2. 엣지/인프라: CDN/리버스 프록시/로드밸런서(접속 유지, 압축, 캐시 여부 판단)
  3. API 서버:
  • Controller: 요청 파싱/검증
  • Service: 비즈니스 로직, 캐시 조회(있다면)
  • Repository/DAO: DB 질의 준비(쿼리/파라미터/페이징)
  1. 데이터 계층:
  • 캐시(HOT 키면 여기서 끝) → 미스 시 DB
  • DB: 인덱스 탐색 → 정렬/조인/필터 → 결과 20건 반환
  1. API 서버 복귀: 객체 매핑(JPA 등) → DTO 변환 → JSON 직렬화 → 압축
  2. 응답 전송: 클라이언트까지 네트워크 왕복

정규화 vs 비정규화, 그리고 내가 겪은 성능 개선

먼저 정규화와 비정규화는 뭘까?

1) 정규화(Normalization)란?

목표는 데이터 중복 최소화와 이상(Anomaly) 방지, 무결성 확보입니다.
대표적으로 1NF(원자성) → 2NF(부분 종속 제거) → 3NF(이행 종속 제거) 순으로 테이블을 쪼개어, 한 사실을 한 곳에만 저장하도록 설계합니다.

장점

  • 무결성 높아 집니다. 한 곳만 업데이트하면 되므로 갱신 이상(Insert/Update/Delete Anomaly)이 줄어듦.
  • 중복이 적어 용량/백업/복구가 유리 저장 공간 효율이 좋아집니다.

단점

  • 조회를 하려면 Join을 걸어야합니다.
  • 읽기 성능이 떨어지고, 조회·정렬·집계에 비용 증가

2) 비정규화(Denormalization)란?

읽기 성능을 올리기 위해 의도적으로 중복을 허용하는 설계입니다. 예: like_count를 product
테이블에 컬럼으로 유지(원본은 좋아요 이벤트 테이블).

장점

  • 조인 없이 단일 테이블에서 바로 정렬/필터가 가능해집니다.
  • 인덱스 최적화 용이: (brand_id, like_count DESC) 같은 복합 인덱스로 쿼리 최적화
  • 일관된 응답 형태: API가 필요한 필드가 한 테이블에 있어 직렬화/네트워크 비용도 절감

단점 (이 부분을 특히 부각)

  • 원본 이벤트와 집계 컬럼 사이에 동기화 지연/오류 위험해집니다.
    (예: 좋아요 등록/취소 시 like_count 업데이트 누락/중복)
  • 쓰기 증폭(Write Amplification): 조회는 빨라지지만, 쓰기 트랜잭션 비용이 커짐
  • 복잡도 증가: 트랜잭션 경계/락/재시도/이벤트 재처리 등 운영 난이도↑
  • 데이터 품질 리스크: 장애/롤백/재처리 시 집계 불일치가 누적될 수 있음

나는 읽기성능을 위해 비정규화를 선택했다

인덱스

“브랜드별 좋아요 순 정렬” 조회를 빠르게 만들고 싶었다

브랜드 좋아요순 쿼리를 살펴보자

  • 인덱스를 볼 때 중점적으로 봐야하는 컬럼은 type, key, rows, extra정도가 있다.
    • type
      • ALL 로되어있을 경우 전체 테이블을 스캔한다. 그렇게되면 쿼리를 읽는 속도가 느려진다
      • 필요한 컬럼의 값만 읽으면 되는데 다른 컬럼값도 읽기 때문
    • key
      • key 값이 NULL로 되어있으면 인덱스를 사용하고 있지 않다는 뜻이다.
    • rows
      • 실제 필요한 행만 읽으면 되지만 여러 row를 같이 읽으면 부하가 발생한다.
-- 복합 인덱스를 걸어준다.
CREATE INDEX idx_product_brand_likes ON product(brand_id, likes);

복합인덱스를 걸어주었더니 이렇게 변경되었다.

그래서 뭐가 개선되었냐!

  • ref는 비고유 인덱스 탐색으로, 인덱스 트리를 로그 시간에 진입(O(log N))한 뒤 매칭되는 키 범위만 순차 스캔(O(k)). 결과적으로 대기열(waiting)과 tail latency(p99/p99.9)가 줄어든다.
  • key를 복합 인덱스로 걸어주었다. WHERE + ORDER BY를 한 번에 커버하여 조회성능을 올린다.
  • rows의 계획의 추정/실제 스캔 행 수가 크게 감소했다. 서버 레이어로 올라오는 레코드 양은 줄어들고, 파싱/매핑/직렬화 비용이 줄어든다.

그런데,,, 마냥 이 방법이 무적이냐 라고 한다면 절대 아니다. 치명적인 단점이 있다. 지금은 product에만 인덱스를 걸었지만, 다른 읽기 타입의 API의 성능을 올리려고 인덱스를 과하게 하면 쓰기성능이 급격하게 떨어진다는 것... 그래서 적절한 인덱스를 걸어주는것이 핵심이다.


2편에서는 Grafana로 K6 부하테스트를 시각화하여 비교분석해보겠다.

profile
웃음 주는 개발자 되기

0개의 댓글