1. 테스트 배경

웹 애플리케이션에서 데이터베이스 접근은 성능에 매우 큰 영향을 미칩니다. 특히 대규모 트래픽 상황에서 동일한 데이터 요청이 반복된다면, 데이터베이스 부하가 급격히 증가하여 응답 속도가 느려지고 시스템 안정성이 저하될 수 있습니다.

 

이를 해결하기 위해 흔히 사용하는 방법 중 하나가 데이터베이스 인덱싱입니다. 하지만 인덱싱만으로 해결되지 않는 부하 문제를 더 효율적으로 해결하기 위해 캐싱(Caching)을 도입할 수 있습니다.

 

이번 테스트에서는 데이터베이스에 적절한 인덱싱을 설정한 상태에서 캐싱을 추가로 적용했을 때 성능에 어떤 차이가 발생하는지 궁금하여, Spring Boot와 JPA를 사용하여 @Cacheable을 활용한 성능 효과를 Artillery로 측정하고 분석해 보았습니다.


2. 캐싱 적용 전후 비교

테스트는 다음 두 가지 시나리오로 나누어 진행했습니다

  • 캐싱 미적용: 데이터베이스에서 모든 요청을 처리하며, 데이터 캐싱을 사용하지 않는 상태.
  • 캐싱 적용: Spring의 @Cacheable을 사용하여 동일한 요청에 대해 캐싱된 데이터를 반환.

@Cacheable 사용 코드

@Cacheable(value = "customers", key = "#condition.gender + '-' + #pageable.pageNumber")
public Page<CustomerResponse> search(Pageable pageable, CustomerSearchCondition condition) {
    pageable = pageable == null ? PageRequest.of(0, 5) : pageable;
    return customerSearchRepository.search(condition, pageable);
}

3. 캐싱 적용 전후 성능 비교

항목캐싱 적용 전캐싱 적용 후개선 효과

총 요청 수 1563건 2800건 +79%
완료된 시나리오 287개 700개 +143%
평균 RPS (초당 처리량) 21.17 46.42 +119%
중앙값 지연 시간 3963.5ms 1ms -99.97% (거의 즉시 응답)
95th 퍼센타일 지연 시간 8904ms 78ms -99.1%
최대 지연 시간 9994ms 797ms -92%
오류 수 413건 0건 완전 제거

 


4. 결론

캐싱의 효과

  • 요청 처리 속도 향상: 캐싱을 통해 데이터베이스 요청 횟수가 감소하면서 전체 지연 시간이 크게 단축되었습니다. 캐싱된 데이터를 반환하므로 중앙값 기준 지연 시간이 3963.5ms에서 1ms로 개선되었습니다.
  • 처리량 증가: 평균 RPS(초당 요청 처리 수)가 21.17에서 46.42로 약 2배 증가하여 더 많은 요청을 처리할 수 있게 되었습니다.
  • 안정성 향상: 캐싱 미적용 상태에서 데이터베이스 과부하로 인해 발생했던 ETIMEDOUT 오류 413건이 캐싱 적용 후 완전히 사라졌습니다.

시스템 안정성

  • 캐싱 적용은 데이터베이스 부하를 줄여 응답 속도와 시스템 안정성을 동시에 개선할 수 있음을 확인했습니다. 특히, 95th 퍼센타일 지연 시간이 8904ms에서 78ms로 크게 단축되어 사용자 경험도 향상되었습니다.

5. 캐싱 적용 시 유의사항

캐싱은 모든 경우에 적합하지 않으며, 적용 시 주의해야 할 점이 있습니다:

  • 정적 데이터에 적합: 캐싱은 변경되지 않거나 자주 변경되지 않는 데이터(예: 코드 테이블, 인기 상품 등)에 적합합니다.
  • 메모리 관리 필요: JVM 메모리를 사용하는 캐싱(Simple Cache)은 대규모 트래픽 환경에서는 메모리 부족 문제를 유발할 수 있습니다. Redis와 같은 분산 캐싱 솔루션을 고려해야 합니다.
  • TTL(Time To Live) 설정: 캐싱된 데이터의 유효 기간을 적절히 설정하여 오래된 데이터 반환 문제를 방지해야 합니다.

6. 최종 생각

캐싱은 데이터베이스 부하를 줄이고 애플리케이션의 응답 속도를 높이는 데 효과적인 도구입니다. 이번 테스트를 통해 캐싱이 성능 개선에 미치는 영향을 수치로 확인할 수 있었습니다.

  • 적용 사례: 읽기 요청이 많은 API, 동일한 조건으로 반복 호출되는 요청, 외부 API 호출 결과 캐싱 등.
  • 향후 확장 가능성: Simple Cache를 Redis 등 분산 캐시로 확장하여 대규모 트래픽도 안정적으로 처리할 수 있습니다.

데이터베이스 성능 최적화에서 인덱싱은 빠른 조회 속도를 보장하는 중요한 도구입니다. 하지만 인덱스를 많이 생성하면 쓰기 작업(INSERT, UPDATE, DELETE)의 성능이 저하된다고 알려져 있습니다. 이번 글에서는 실제 실험 데이터를 기반으로 이 가정을 테스트한 결과를 공유합니다.


1. 테스트 목적

멀티 컬럼 인덱스와 단일 컬럼 인덱스를 다수 적용했을 때, 쓰기 작업의 성능이 얼마나 저하되는지 확인하는 것이 목적이었습니다. 특히, 데이터베이스에 110만 건 이상의 데이터를 넣고 쓰기 작업을 수행하며, 인덱스가 없는 상태와 있는 상태를 비교해 보았습니다.


2. 테스트 환경

  • 데이터베이스: MySQL 8.0
  • 테이블: Customer 테이블
  • 데이터 크기: 약 110만 건
  • 인덱스 설정:
    • 테스트 A: 기본 키 외에 추가 인덱스 없음
    • 테스트 B: 복합 인덱스 2개 적용
CREATE INDEX idx_customer_name_email ON customer(name, email);
CREATE INDEX idx_customer_phone_grade ON customer(phone, grade);

 

  • 테스트 도구: Artillery를 사용한 부하 테스트
  • 테스트 작업:
    • INSERT: 고객 데이터 삽입
    • UPDATE: 고객 데이터 수정
    • DELETE: 고객 데이터 삭제

3. 테스트 시나리오 및 데이터

요청 예시

config:
  target: 'http://localhost:8080'
  phases:
    - duration: 10
      arrivalRate: 10
    - duration: 20
      arrivalRate: 20
    - duration: 30
      arrivalRate: 30
    - duration: 20
      arrivalRate: 10

scenarios:
  - flow:
      # 1. 로그인 후 토큰 획득
      - post:
          url: "/api/v1/auth/signintest"
          json:
            email: "test2@gmail.com"
            password: "123456789"
          capture:
            - json: "$.data.atk"
              as: "authToken"

      # 2. 고객 정보 수정 (쓰기 작업)
      - patch:
          url: "/api/v1/customers/1000"
          headers:
            Authorization: "Bearer {{ authToken }}"
          json:
            name: "Updated Name {{ $randomInt }}"
            phone: "010-{{ $randomInt }}-{{ $randomInt }}"

      # 3. 고객 삭제 (쓰기 작업)
      - delete:
          url: "/api/v1/customers/{{ $randomInt }}"
          headers:
            Authorization: "Bearer {{ authToken }}"

      # 4. 고객 메모 작성/수정 (쓰기 작업)
      - put:
          url: "/api/v1/customers/100/memo"
          headers:
            Authorization: "Bearer {{ authToken }}"
          json:
            content: "This is a test memo with random value {{ $randomInt }}."
            title: "Test Memo Title {{ $randomInt }}"

 


4. 테스트 결과

1. 지연 시간(Latency) 비교

지표 첫 번째 결과 (인덱스 없음) 두 번째 결과 (인덱스 있음)

최소 지연 시간 (min) 0 ms 0 ms
최대 지연 시간 (max) 109 ms 391 ms
중앙값 (median) 1 ms 2 ms
95퍼센타일 (p95) 77 ms 77 ms
99퍼센타일 (p99) 79 ms 80 ms

분석

  • 최소 지연 시간은 두 결과 모두 동일합니다 (0 ms).
  • 최대 지연 시간에서 큰 차이가 나타납니다:
    • 인덱스가 있는 경우 최대 지연 시간은 391 ms.
    • 인덱스가 없는 경우 최대 지연 시간은 109 ms.
  • 중앙값에서 인덱스가 있는 경우 2 ms, 없는 경우 1 ms로 차이가 있습니다.
  • 95% 및 99% 지연 시간은 거의 동일하게 나타났습니다.

5. 결론

멀티 컬럼 인덱스를 적용하면 최대 지연 시간이 늘어났다는 것을 알았지만 지금 테스트가 뭔가 이상한 거 같아서 다시 한번 검토가 필요한 거 같습니다.

'성능테스트 > 성능 개선' 카테고리의 다른 글

JPA에서 캐싱 활용하여 성능 테스트  (0) 2024.12.20

이번 블로그에서는 Artillery를 이용한 성능 테스트 결과를 JSON 파일로 저장하고, 이를 HTML로 변환하여 시각화하는 방법을 다룹니다.


1. 테스트 결과를 JSON 파일로 저장

Artillery 실행 시 --output 옵션을 사용하여 결과를 JSON 파일로 저장할 수 있습니다. 아래 명령어는 advanced-load-test.yml 설정 파일로 테스트를 실행하고, 결과를 result.json 파일에 저장합니다:

artillery run --output result.json advanced-load-test.yml

result.json 파일은 테스트 결과의 원시 데이터를 담고 있으며, 이 데이터를 활용해 다양한 방식으로 분석하거나 시각화할 수 있습니다.


2. JSON 결과를 HTML로 변환

Artillery는 JSON 결과를 HTML 파일로 변환해 주는 report 명령어를 제공합니다. 이를 사용하면 시각화된 결과를 쉽게 확인할 수 있습니다:

artillery report result.json --output result.html

위 명령어를 실행하면 result.html 파일이 생성됩니다. 이 HTML 파일에는 테스트의 주요 지표(요청 성공률, 응답 시간 통계, 처리량 등)가 포함된 시각화된 결과가 담깁니다.


3. HTML 결과 확인

생성된 result.html 파일을 브라우저에서 열면 테스트 결과를 시각적으로 확인할 수 있습니다. 예를 들어:

open result.html

결과 화면에서는 다음과 같은 정보를 확인할 수 있습니다:

  • 요청 성공률 및 실패율
  • 응답 시간 분포 (평균, 95th 퍼센타일 등)
  • 초당 처리량 (RPS)

4. HTML 결과지 주요 지표 설명

Artillery의 HTML 결과 페이지에는 성능 테스트의 다양한 메트릭이 시각화되어 있습니다. 아래는 주요 지표와 그 의미를 간략히 정리한 내용입니다:


1. Overall Latency Distribution

  • 요청의 응답 시간 분포를 보여주는 섹션입니다. 주요 지표는 다음과 같습니다:이 값들을 통해 시스템의 응답 속도가 어떤 수준으로 유지되고 있는지 파악할 수 있습니다.
    • max: 테스트 중 측정된 가장 긴 응답 시간 (최대값).
    • p95 (95th Percentile): 요청 중 상위 95%가 이 시간보다 빠르게 응답. 즉, 상위 5%의 느린 요청을 제외한 최대 응답 시간.
    • p99 (99th Percentile): 요청 중 상위 99%가 이 시간보다 빠르게 응답. 가장 느린 1% 요청을 제외한 응답 시간을 의미합니다.

2. Summary

  • Test duration: 테스트가 실행된 총 시간 (초 단위).
    • 예: 130 sec는 테스트가 130초 동안 실행되었음을 의미합니다.
  • Scenarios created: 테스트 동안 생성된 사용자 시나리오 수.
    • 예: 1320은 총 1320개의 사용자 시나리오가 생성되었음을 나타냅니다.
  • Scenarios completed: 시나리오가 정상적으로 완료된 횟수.
    • 예: 1320은 생성된 1320개의 시나리오가 모두 성공적으로 완료되었음을 의미합니다. 실패한 시나리오가 있다면 이 수는 작아질 것입니다.

3. Other Key Metrics

  • Requests Per Second (RPS): 초당 처리된 요청 수. 서버의 처리량을 측정합니다.
  • Failures: 실패한 요청 수. 서버에서 오류가 발생했거나 시간 초과된 요청을 포함합니다.
  • Response Time (ms):
    • 요청에 대한 서버의 응답 속도를 밀리초(ms) 단위로 표시.
    • 평균, 최소, 최대 응답 시간과 다양한 퍼센타일 값(예: p95, p99)을 제공합니다.

예시 설명

HTML 결과지에서 아래와 같은 정보가 나왔다면:

  • Test duration: 130초 동안 테스트가 진행됨.
  • Scenarios created: 1320개의 시나리오가 생성됨.
  • Scenarios completed: 모든 시나리오가 정상적으로 완료됨.
  • Overall Latency Distribution:
    • max: 900ms → 가장 느린 응답 시간은 900ms.
    • p95: 150ms → 95%의 요청이 150ms 이내에 응답.
    • p99: 300ms → 99%의 요청이 300ms 이내에 응답.

Artillery 이용한 다양한 테스트 방법입니다.

config:
  target: "<https://jsonplaceholder.typicode.com>"
  phases:
    - duration: 20         # 첫 번째 단계: 20초 동안 5명의 사용자/초 도착
      arrivalRate: 5
      name: "Warm-up phase"
    - duration: 30         # 두 번째 단계: 30초 동안 20명의 사용자/초 도착
      arrivalRate: 20
      name: "Peak traffic"
    - duration: 10         # 세 번째 단계: 10초 동안 트래픽 급감
      arrivalRate: 2
      name: "Traffic drop-off"
    - duration: 60         # 네 번째 단계: 60초 동안 일정한 트래픽 유지
      arrivalRate: 10
      name: "Sustained load"
scenarios:
  - name: "Fetch posts and details" # 첫 번째 시나리오: 게시글 조회 및 상세 정보 확인
    flow:
      - get:
          url: "/posts"
      - get:
          url: "/posts/1"
  - name: "Create and update post" # 두 번째 시나리오: 게시글 생성 후 업데이트
    flow:
      - post:
          url: "/posts"
          json:
            title: "New Post"
            body: "This is a test post created by Artillery."
            userId: 1
      - put:
          url: "/posts/1"
          json:
            title: "Updated Post"
            body: "This is an updated test post."
  - name: "User flow simulation" # 세 번째 시나리오: 사용자 플로우 시뮬레이션
    flow:
      - get:
          url: "/users/1"
      - get:
          url: "/users/1/posts"
      - post:
          url: "/comments"
          json:
            postId: 1
            name: "Artillery User"
            email: "test@example.com"
            body: "This is a test comment."

 

YML 설명

phases

  1. Warm-up phase:
    • 초기 단계에서 적은 부하로 시스템을 "예열"합니다.
  2. Peak traffic:
    • 최대 트래픽을 주어 서버의 한계를 테스트합니다.
  3. Traffic drop-off:
    • 트래픽이 갑자기 줄어드는 상황을 시뮬레이션합니다.
  4. Sustained load:
    • 일정 부하를 주어 장기적인 안정성을 확인합니다.

scenarios

  1. Fetch posts and details:
    • 모든 게시글을 가져오고, 특정 게시글의 상세 정보를 확인합니다.
  2. Create and update post:
    • 새 게시글을 생성한 후, 이를 업데이트합니다.
  3. User flow simulation:
    • 특정 사용자의 데이터를 조회하고, 댓글을 남기는 전체적인 사용자 플로우를 시뮬레이션합니다.

실행 방법

1. 파일 이름을 advanced-load-test.yml로 저장합니다.

 

 

2. Artillery를 사용해 실행합니다

artillery run advanced-load-test.yml

3. artillery run advanced-load-test.yml

실행 결과

 

다음 블로그에서는 실행 결과를 JSON 파일로 저장한 뒤, HTML로 변환하여 테스트 결과를 시각화하는 과정을 공유하겠습니다.

성능 테스트란?

  • 성능 테스트(Performance Testing)는 애플리케이션이 특정 조건에서 얼마나 잘 작동하는지를 평가하기 위한 테스트 방식입니다. 이는 주로 애플리케이션의 속도, 안정성, 확장성, 그리고 자원을 얼마나 효율적으로 사용하는지에 초점을 맞춥니다. 성능 테스트는 다양한 형태로 나뉩니다
  1. 부하 테스트(Load Testing): 시스템이 예상되는 최대 사용자 수에서 어떻게 작동하는지 확인합니다.
  2. 스트레스 테스트(Stress Testing): 시스템이 용량 한계를 초과할 때 어떻게 작동하는지 확인합니다.
  3. 스파이크 테스트(Spike Testing): 갑작스러운 트래픽 급증에 시스템이 어떻게 반응하는지 평가합니다.
  4. 내구성 테스트(Soak Testing): 장시간 동안 일정 부하를 주고 안정성을 점검합니다.

Artillery란?

Artillery는 성능 테스트와 부하 테스트를 수행하기 위한 강력하고 사용하기 쉬운 오픈 소스 툴입니다. Node.js 기반으로 만들어졌으며, 다음과 같은 특징이 있습니다

  • JSON/YAML 파일로 간단하게 시나리오 정의 가능
  • HTTP, WebSocket, Socket.io 등 다양한 프로토콜 지원
  • 클라우드 환경 및 CI/CD 파이프라인에 쉽게 통합 가능

Artillery 설치

Artillery를 사용하려면 Node.js가 설치되어 있어야 합니다.

npm install -g artillery

설치 후, 다음 명령어로 설치가 완료되었는지 확인하세요

artillery --version

 


Artillery 기본 사용법

Artillery는 설정 파일을 기반으로 시나리오를 정의합니다. 기본 설정 파일은 다음과 같은 구조를 가집니다:

config:
  target: "<http://your-api-endpoint.com>"
  phases:
    - duration: 60   # 테스트 실행 시간(초)
      arrivalRate: 10 # 초당 사용자 수
scenarios:
  - flow:
      - get:
          url: "/api/resource"

주요 요소 설명

  1. config: 테스트 환경 설정
    • target: 테스트 대상 URL
    • phases: 테스트 단계 설정 (시간, 사용자 수 등)
  2. scenarios: 사용자가 수행할 시나리오 정의
    • flow: 사용자가 따를 작업의 순서

간단한 HTTP 테스트 시나리오

설정 파일 생성

test.yml 파일을 생성합니다:

config:
  target: "<https://jsonplaceholder.typicode.com>"
  phases:
    - duration: 10
      arrivalRate: 5
scenarios:
  - flow:
      - get:
          url: "/posts"

테스트 실행

다음 명령어로 테스트를 실행합니다:

artillery run test.yml

결과 확인

결과는 실행 후 콘솔에 출력됩니다. 결과 요약에는 다음과 같은 정보가 포함됩니다:

  • 요청 성공/실패 수
  • 응답 시간 통계 (평균, 95th 퍼센타일 등)
  • 초당 처리량(*RPS)

 

용어 정리

RPS (Requests Per Second)

  • 초당 서버로 보내지는 요청(Request)의 수를 의미합니다.
  • HTTP 요청 단위로 측정되며, GET, POST, PUT 등 모든 요청이 포함됩니다.
  • Artillery는 HTTP 요청 단위를 기준으로 결과를 계산하므로, 결과로 나온 초당 처리량은 RPS입니다.

TPS (Transactions Per Second)

  • 초당 처리되는 트랜잭션(Transaction)의 수를 의미합니다.
  • 트랜잭션은 하나의 사용자 작업 단위로, 여러 요청(Request)을 포함할 수 있습니다.
  • 예를 들어, "사용자가 로그인하고 데이터를 조회하는 과정"을 하나의 트랜잭션으로 본다면, 이는 여러 RPS로 구성될 수 있습니다.

 

Artillery 공식 문서: https://www.artillery.io/docs/

+ Recent posts