Skip to content
Being BE
Go back

쿼리 최적화 1편: 대상 API 선정과 성능 베이스라인 측정

Edit page

이번에 포트폴리오 강화 + 쿼리 분석 및 최적화 프로세스를 경험하기 위해 기존 프로젝트를 개선하기로 계획했다.
프로젝트는 이전에 진행했던 위하다 프로젝트(주문제작 케이크 매장의 상담·주문·결제를 한 흐름으로 관리하는 서비스)를 대상으로 선정했다. 위하다 프로젝트

해당 포스트에서 테스트 및 개선은 배포환경이 아닌 로컬 어플리케이션 기준으로 실시되었습니다!

분석 대상 선정하기

우선 테스트&개선 대상 API를 선정하여야 하는데 이때 대상은 아래 기준으로 탐색하였다.

API목록 파악및 분류는 Codex를 사용하였고 아래와 같이 API목록이 추려졌다.

영역API
대시보드 집계GET /seller/dashboard, GET /seller/dashboard/revenue, GET /seller/dashboard/revenue/transactions
주문 목록·캘린더GET /seller/orders, GET /seller/orders/calendar/today, GET /seller/orders/calendar/week, GET /seller/orders/calendar/month
문의·채팅 목록GET /seller/inquiries, GET /seller/inquiries/{inquiryId}/events
문의별 이력GET /seller/inquiries/{inquiryId}/order-form-submissions, GET /seller/inquiries/{inquiryId}/confirmations
스토어 종합 상태GET /seller/store/management-status
여러 설정·공지 관리GET /seller/store/settings, PUT /seller/store/settings, GET /seller/notices, PUT /seller/notices
이미지·갤러리 목록GET /seller/gallery-items, GET /seller/representative-images

이때 단일항목 API는 제외하였는데 목록·집계 API 같이 조회건수가 증가함에 따라 쿼리비용이 높아질수 있는 항목만 우선적으로 추렸다.

이중에서 조회빈도 같은 경우에는 실 사용자 데이터가 없어서 추측성으로 밖에 선정할수 없어 선정기준에서 제외하였고 데이터가 늘어나면서 비용이 커지는 코드 + 반복 조회 발생을 기준으로 판단을 하여 개선 항목을 선정하였다.

  1. GET /seller/inquiries : 스토어의 모든 문의를 모두 가져오고,각 문의의 읽지 않은 항목수도 개별 조회
  2. GET /seller/dashboard : 여러 집계와 문의별 읽지 않은 항목 수를 조회
  3. GET /seller/orders : 누적 주문 목록 모두 조회

로컬 테스트 환경 구성

테스트 환경

로컬 테스트 환경구성은 Spring Boot 어플리케이션은 로컬에서 local-scenario 프로필을 실행하고 localhost:5432의 PostgreSQL에 연결하도록 구성하였다. 기존에는 어플리케이션 인증을 Cognito를 사용하였지만 로컬테스트 토큰 사용하도록 하였으며,자산 저장은 S3대신 로컬 파일시스템을 사용한다.

테스트 데이터 준비

단계한 스토어의 문의문의당 이벤트한 스토어의 주문
데이터 규모1,000건10건1,000건

테스트 데이터 삽입 결과 구매자 : 1000명
채팅방 : 1000개
채팅 메시지: 1만개
채팅 타임라인 이벤트(ex.주문서 발행,결제 요청 등등) : 1만개
주문 : 1000개

Baseline 측정

테스트 도구

테스트 도구로는 k6를 사용하였다. k6를 사용한 가장 큰이유는 이미 사용해본 경험이 있었으며 설치 - 코드작성 - 실행 까지의 프로세스가 다른 도구들에 비해 간편하여 애용하게 되었다.

테스트 시나리오

목적

이번 테스트의 목적은 “최대 몇 명이 접속 가능한지”,“부하를 어느정도까지 견딜수 있는지”보다 현재 데이터에서 각 API가 얼마나 걸리고,요청량이 늘어날때 어떻게 변하는지 기준값을 얻는 것이 목표이다.(쿼리 개선하여 다시 측정 후 비교)

요청량

순서요청량시간목적
예열낮은 요청량30~60초초기 실행 영향 줄이기
1단계1회/초2분기준 응답 시간
2단계3회/초2분요청 증가 시 변화
3단계5회/초2분추가 증가 시 변화

예열을 하는 이유 : 처음 몇 요청에는 JVM실행 상태, DB 연결 준비,디스크 캐시 등의 영향이 섞일 수 있어 이를 구분하여 측정 가능하도록 합니다.

기대 산출물 및 확인할 지표

지표이번 테스트에서 확인할 것
요청 1건당 SQL 실행 횟수문의·대시보드에서 문의 수만큼 조회가 반복되는가
SQL별 실행 시간과 총 DB 처리 시간시간이 많이 드는 쿼리가 무엇인가
API 응답 시간 p50·p95DB 작업이 실제 응답 시간에 얼마나 영향을 주는가
반환 건수·응답 크기같은 양의 데이터를 반환한 결과끼리 비교하고 있는가
오류율느려지는 과정에서 요청이 실패하는가
실제 요청 수·dropped_iterations계획한 부하가 실제로 들어갔는가

테스트 코드

import http from 'k6/http';  
import { check } from 'k6';  
  
// 테스트 API목록  
const paths = {  
    inquiries: '/seller/inquiries',  
    dashboard: '/seller/dashboard',  
    orders: '/seller/orders',  
};  
  
// 실행시 주입값  
const target = __ENV.TARGET;  
const rate = Number(__ENV.RATE);  
const minItems = Number(__ENV.MIN_ITEMS || 1000);  
const baseUrl = __ENV.BASE_URL || 'http://localhost:8080';  
const token = __ENV.SELLER_TOKEN || 'local-seller-token';  
  
export const options = {  
    scenarios: {  
        seller_query: {  
            executor: 'constant-arrival-rate',  
            rate,  
            timeUnit: '1s',  
            duration: __ENV.DURATION || '2m',  
            preAllocatedVUs: 20,  
            maxVUs: 100,  
        },  
    },  
    thresholds: {  
        checks: ['rate==1'],  
    },  
};  
  
export default function () {  
    const response = http.get(`${baseUrl}${paths[target]}`, {  
        headers: {  
            Authorization: `Bearer ${token}`,  
        },  
    });  
  
    let body = null;  
    if (response.status === 200) {  
        try {  
            body = response.json();  
        } catch (_) {  
            // JSON이 아니면 아래 응답 검증에서 실패  
        }  
    }  
  
    const validData =  
        body !== null &&  
        body.success === true &&  
        (target === 'dashboard'  
            ? body.data !== null &&  
            typeof body.data === 'object' &&  
            !Array.isArray(body.data)  
            : Array.isArray(body.data) && body.data.length >= minItems);  
  
    check(response, {  
        'HTTP 200': (r) => r.status === 200,  
        'expected response data': () => validData,  
    });  
}

테스트 결과

채팅방 조회 종합정리

주요 항목초당 1회초당 3회초당 5회
실제 요청120회361회601회
평균 응답 시간302.90ms255.51ms267.35ms
p50 응답 시간299.22ms241.77ms244.66ms
p95 응답 시간336.84ms328.44ms384.94ms
최대 응답 시간527.22ms491.86ms1.04초
HTTP 실패0건0건0건
응답 검증모두 통과모두 통과모두 통과
수신 데이터93MB280MB467MB

대시보드 조회 종합정리

주요 항목초당 1회초당 3회초당 5회
실제 요청120회361회601회
평균 응답 시간275.00ms226.00ms223.36ms
p50 응답 시간269.80ms218.27ms212.23ms
p95 응답 시간322.66ms271.25ms289.64ms
최대 응답 시간379.30ms572.89ms588.90ms
HTTP 실패0건0건0건
응답 검증모두 통과모두 통과모두 통과
수신 데이터1.4MB4.3MB7.1MB

주문 목록 조회 종합정리

주요 항목초당 1회초당 3회초당 5회
실제 요청121회361회600회
평균 응답 시간37.04ms31.52ms31.79ms
p50 응답 시간37.90ms31.31ms32.38ms
p95 응답 시간45.19ms41.84ms41.41ms
최대 응답 시간51.07ms117.52ms78.19ms
HTTP 실패0건0건0건
응답 검증모두 통과모두 통과모두 통과
수신 데이터71MB213MB353MB

API 테스트에서는 별도로 추가 테스트를 할 이유는 없다고 판단된다.


Edit page