Performance Testing

성능 테스팅의 기본 원리

성능 테스팅은 시스템이 예상된 부하 조건에서 어떻게 작동하는지 측정하고 평가하는 과정이다. 이는 단순히 ‘시스템이 작동하는가?‘를 넘어 ‘얼마나 효율적으로 작동하는가?‘를 확인하는 것이다.

성능 테스팅의 주요 목적

  1. 성능 병목 현상 식별: 시스템의 어떤 부분이 전체 성능을 저하시키는지 파악한다.
  2. 확장성 평가: 시스템이 증가하는 부하에 어떻게 대응하는지 측정한다.
  3. 사용자 경험 예측: 실제 환경에서 사용자가 경험할 성능 수준을 추정한다.
  4. 최적화 효과 검증: 성능 개선 작업의 효과를 객관적으로 평가한다.
  5. 안정성 확인: 지속적인 부하 하에서 시스템의 안정성을 확인한다.

성능 테스팅의 주요 유형

백엔드 성능을 평가하기 위해 다양한 유형의 테스트가 필요하다. 각 테스트는 특정 측면에 초점을 맞추고 있다.

부하 테스트(Load Testing)

부하 테스트는 예상되는 일반적인 사용자 부하에서 시스템의 성능을 평가한다.

구현 방법:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// k6를 사용한 부하 테스트 스크립트 예시
import http from 'k6/http';
import { sleep, check } from 'k6';

export const options = {
  // 점진적으로 부하 증가
  stages: [
    { duration: '2m', target: 100 }, // 2분 동안 100명의 가상 사용자로 증가
    { duration: '5m', target: 100 }, // 5분 동안 100명 유지
    { duration: '2m', target: 0 },   // 2분 동안 0명으로 감소
  ],
  thresholds: {
    // 95%의 요청이 200ms 이내에 완료되어야 함
    http_req_duration: ['p(95)<200'],
    // 오류율 1% 이하
    http_req_failed: ['rate<0.01'],
  },
};

export default function() {
  const response = http.get('https://api.example.com/products');
  
  // 응답 검증
  check(response, {
    'status is 200': (r) => r.status === 200,
    'response time < 500ms': (r) => r.timings.duration < 500,
  });
  
  sleep(1); // 1초 대기
}

측정 지표:

스트레스 테스트(Stress Testing)

스트레스 테스트는 시스템의 한계를 확인하기 위해 극단적인 부하 조건에서 시스템을 평가한다.

구현 방법:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// k6를 사용한 스트레스 테스트 스크립트 예시
import http from 'k6/http';
import { sleep } from 'k6';

export const options = {
  // 극단적인 부하 설정
  stages: [
    { duration: '2m', target: 500 },  // 2분 동안 500명으로 빠르게 증가
    { duration: '5m', target: 1000 }, // 5분 동안 1000명으로 증가
    { duration: '5m', target: 2000 }, // 5분 동안 2000명으로 증가
    { duration: '2m', target: 0 },    // 2분 동안 0명으로 감소
  ],
};

export default function() {
  http.get('https://api.example.com/products');
  http.get('https://api.example.com/categories');
  
  // 복잡한 작업 시뮬레이션
  const payload = { /* 대용량 데이터 */ };
  http.post('https://api.example.com/process', JSON.stringify(payload), {
    headers: { 'Content-Type': 'application/json' },
  });
  
  sleep(0.1); // 0.1초만 대기 (높은 빈도의 요청)
}

관찰 사항:

내구성 테스트(Endurance Testing)

내구성 테스트는 장시간 동안 지속적인 부하에서 시스템의 안정성을 평가한다.

구현 방법:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// k6를 사용한 내구성 테스트 스크립트 예시
import http from 'k6/http';
import { sleep, check } from 'k6';

export const options = {
  // 장시간 테스트 설정
  stages: [
    { duration: '10m', target: 300 }, // 10분 동안 300명으로 증가
    { duration: '8h', target: 300 },  // 8시간 동안 300명 유지
    { duration: '10m', target: 0 },   // 10분 동안 0명으로 감소
  ],
};

export default function() {
  const responses = http.batch([
    ['GET', 'https://api.example.com/dashboard'],
    ['GET', 'https://api.example.com/notifications'],
    ['GET', 'https://api.example.com/user-profile']
  ]);
  
  check(responses[0], {
    'dashboard status is 200': (r) => r.status === 200,
  });
  
  sleep(3 + Math.random() * 5); // 3-8초 사이의 랜덤 대기 (실제 사용자 행동 시뮬레이션)
}

관찰 지표:

4스파이크 테스트(Spike Testing)

스파이크 테스트는 갑작스러운 부하 증가에 대한 시스템의 반응을 평가한다.

구현 방법:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// k6를 사용한 스파이크 테스트 스크립트 예시
import http from 'k6/http';
import { sleep } from 'k6';

export const options = {
  // 갑작스러운 부하 증가 시나리오
  stages: [
    { duration: '1m', target: 50 },    // 1분 동안 50명으로 증가
    { duration: '2m', target: 50 },    // 2분 동안 유지
    { duration: '1m', target: 1000 },  // 1분 동안 1000명으로 급증
    { duration: '2m', target: 1000 },  // 2분 동안 고부하 유지
    { duration: '1m', target: 50 },    // 1분 동안 50명으로 감소
    { duration: '2m', target: 50 },    // 2분 동안 유지
    { duration: '1m', target: 0 },     // 1분 동안 0명으로 감소
  ],
};

export default function() {
  http.get('https://api.example.com/products');
  sleep(1);
}

관찰 사항:

용량 테스트(Capacity Testing)

용량 테스트는 시스템이 처리할 수 있는 최대 부하를 결정한다.

구현 방법:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// k6를 사용한 용량 테스트 스크립트 예시
import http from 'k6/http';
import { sleep } from 'k6';

export const options = {
  // 단계적으로 최대 용량 확인
  stages: [
    { duration: '5m', target: 500 },    // 500명
    { duration: '5m', target: 1000 },   // 1000명
    { duration: '5m', target: 1500 },   // 1500명
    { duration: '5m', target: 2000 },   // 2000명
    { duration: '5m', target: 2500 },   // 2500명
    { duration: '5m', target: 3000 },   // 3000명
  ],
};

export default function() {
  // 다양한 엔드포인트에 요청
  http.get('https://api.example.com/dashboard');
  http.get('https://api.example.com/products');
  
  // 데이터 저장 요청
  http.post('https://api.example.com/events', JSON.stringify({
    eventType: 'pageView',
    timestamp: new Date().toISOString()
  }));
  
  sleep(1);
}

측정 지표:

성능 테스트 구현을 위한 도구

성능 테스트를 효과적으로 구현하기 위해 다양한 도구가 사용된다. 각 도구는 고유한 강점과 특징을 가지고 있다.

1. K6

k6는 현대적이고 개발자 친화적인 부하 테스팅 도구로, JavaScript로 테스트 스크립트를 작성할 수 있다.

특징:

설치 및 기본 사용:

1
2
3
4
5
# 설치 (Linux/macOS)
brew install k6

# 간단한 테스트 실행
k6 run test.js

JMeter

Apache JMeter는 다양한 프로토콜과 서버 유형에 대한 부하 테스트를 수행할 수 있는 Java 기반 도구이다.

특징:

주요 구성 요소:

Locust

Locust는 Python으로 작성된 오픈 소스 부하 테스팅 도구로, 사용자 행동을 정의하기 쉽다.

특징:

기본 사용 예:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# locustfile.py
from locust import HttpUser, task, between

class WebsiteUser(HttpUser):
    wait_time = between(1, 5)  # 작업 사이 1-5초 대기
    
    @task
    def view_dashboard(self):
        self.client.get("/dashboard")
    
    @task(3)  # 더 높은 빈도로 실행
    def view_products(self):
        self.client.get("/products")
    
    @task
    def add_to_cart(self):
        self.client.post("/cart", json={
            "product_id": 123,
            "quantity": 1
        })

Gatling

Gatling은 Scala로 작성된 고성능 부하 테스팅 도구로, 비동기 처리에 중점을 둔다.

특징:

시나리오 예시:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// BasicSimulation.scala
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._

class BasicSimulation extends Simulation {
  val httpProtocol = http
    .baseUrl("https://api.example.com")
    .acceptHeader("application/json")
    
  val scn = scenario("Basic Scenario")
    .exec(http("request_1")
      .get("/products")
      .check(status.is(200)))
    .pause(5)
    .exec(http("request_2")
      .get("/categories")
      .check(status.is(200)))
    
  setUp(
    scn.inject(
      rampUsers(1000).during(60.seconds)
    ).protocols(httpProtocol)
  )
}

Artillery

Artillery는 Node.js 기반의 현대적인 부하 테스팅 도구로, 설정이 간단하고 클라우드 네이티브 환경에 적합하다.

특징:

설정 예시:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# load-test.yml
config:
  target: "https://api.example.com"
  phases:
    - duration: 60
      arrivalRate: 5
      rampTo: 50
    - duration: 120
      arrivalRate: 50
  defaults:
    headers:
      Content-Type: "application/json"
      Accept: "application/json"

scenarios:
  - name: "API 테스트"
    flow:
      - get:
          url: "/products"
          capture:
            - json: "$.products[0].id"
              as: "productId"
      - think: 3
      - get:
          url: "/products/{{ productId }}"
      - post:
          url: "/cart"
          json:
            productId: "{{ productId }}"
            quantity: 1

벤치마킹: 성능 기준 확립

벤치마킹은 시스템의 성능 기준을 설정하고 시간이 지남에 따라 변화를 추적하는 과정이다.

벤치마킹의 주요 요소

  1. 기준 지표 선정:
    • 응답 시간(백분위수별)
    • 초당 요청 처리량(RPS)
    • 오류율
    • 리소스 사용률(CPU, 메모리, 디스크 I/O, 네트워크)
    • 특정 작업 완료 시간(예: 데이터 처리 작업)
  2. 벤치마크 시나리오 정의:
    • 일반적인 사용자 흐름
    • 주요 비즈니스 프로세스
    • 고부하 상황에서의 중요 기능
  3. 벤치마크 환경 표준화:
    • 하드웨어 사양
    • 운영 체제 및 설정
    • 네트워크 조건
    • 데이터베이스 상태

벤치마킹 구현 접근 방식

1. 자동화된 벤치마킹 파이프라인:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// Node.js로 구현된 벤치마킹 스크립트 예시
const { execSync } = require('child_process');
const fs = require('fs');

// 테스트 환경 정보 수집
const environmentInfo = {
  timestamp: new Date().toISOString(),
  gitCommit: execSync('git rev-parse HEAD').toString().trim(),
  nodeVersion: process.version,
  cpuInfo: execSync('lscpu').toString(),
  memoryInfo: execSync('free -h').toString()
};

// 벤치마크 실행
console.log('벤치마크 테스트 시작...');
const startTime = Date.now();

// k6 테스트 실행 및 결과 캡처
const testOutput = execSync('k6 run --out json=results.json benchmark.js').toString();

const endTime = Date.now();
console.log(`벤치마크 테스트 완료 (${(endTime - startTime) / 1000}초 소요)`);

// 결과 분석
const results = JSON.parse(fs.readFileSync('results.json', 'utf8'));

// 결과 저장
const benchmarkResult = {
  environment: environmentInfo,
  metrics: {
    http_req_duration: {
      p95: results.metrics.http_req_duration.values['p(95)'],
      p99: results.metrics.http_req_duration.values['p(99)'],
      avg: results.metrics.http_req_duration.values.avg
    },
    http_reqs: results.metrics.http_reqs.values.count,
    http_req_failed: results.metrics.http_req_failed.values.passes,
    iterations: results.metrics.iterations.values.count,
    vus: results.metrics.vus.values.max
  },
  timestamp: new Date().toISOString()
};

// 결과를 DB에 저장하거나 파일로 저장
fs.writeFileSync(
  `benchmark-results/${benchmarkResult.timestamp}.json`,
  JSON.stringify(benchmarkResult, null, 2)
);

// 이전 결과와 비교
const previousResults = JSON.parse(
  fs.readFileSync('benchmark-results/latest.json', 'utf8')
);

console.log('성능 변화:');
console.log(`응답 시간 (p95): ${previousResults.metrics.http_req_duration.p95}ms -> ${benchmarkResult.metrics.http_req_duration.p95}ms`);
console.log(`처리량: ${previousResults.metrics.http_reqs} -> ${benchmarkResult.metrics.http_reqs} 요청`);

// 결과 보고서 생성
// ...

2. 벤치마크 결과 시각화:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// 시각화를 위한 데이터 준비 (Node.js 예시)
const fs = require('fs');
const path = require('path');

// 최근 벤치마크 결과 로드
const resultsDir = 'benchmark-results';
const resultFiles = fs.readdirSync(resultsDir)
  .filter(file => file.endsWith('.json'))
  .sort(); // 날짜순 정렬

// 각 결과에서 중요 지표 추출
const trends = resultFiles.map(file => {
  const data = JSON.parse(fs.readFileSync(path.join(resultsDir, file)));
  return {
    timestamp: new Date(data.timestamp),
    p95: data.metrics.http_req_duration.p95,
    p99: data.metrics.http_req_duration.p99,
    throughput: data.metrics.http_reqs,
    errorRate: data.metrics.http_req_failed / data.metrics.http_reqs * 100
  };
});

// Chart.js 데이터 형식으로 변환
const chartData = {
  labels: trends.map(t => t.timestamp.toLocaleDateString()),
  datasets: [
    {
      label: '응답 시간 (p95, ms)',
      data: trends.map(t => t.p95),
      borderColor: 'rgb(75, 192, 192)',
      tension: 0.1
    },
    {
      label: '처리량 (요청)',
      data: trends.map(t => t.throughput),
      borderColor: 'rgb(153, 102, 255)',
      tension: 0.1
    },
    {
      label: '오류율 (%)',
      data: trends.map(t => t.errorRate),
      borderColor: 'rgb(255, 99, 132)',
      tension: 0.1
    }
  ]
};

// 차트 데이터를 HTML로 출력
fs.writeFileSync(
  'benchmark-report.html',
  `<html>
    <head>
      <title>성능 벤치마크 보고서</title>
      <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    </head>
    <body>
      <h1>성능 벤치마크 추이</h1>
      <canvas id="perfChart"></canvas>
      <script>
        const ctx = document.getElementById('perfChart');
        new Chart(ctx, {
          type: 'line',
          data: ${JSON.stringify(chartData)},
          options: {
            responsive: true,
            scales: {
              y: {
                beginAtZero: true
              }
            }
          }
        });
      </script>
    </body>
  </html>`
);

지속적 성능 테스팅과 모니터링

성능 테스팅은 일회성 활동이 아니라 지속적인 프로세스의 일부가 되어야 한다.

CI/CD 파이프라인에 통합

성능 테스팅을 CI/CD 파이프라인에 통합하면 성능 회귀를 조기에 발견할 수 있다.

GitHub Actions를 사용한 예시:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# .github/workflows/performance-tests.yml
name: Performance Tests

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
  schedule:
    - cron: '0 0 * * *'  # 매일 자정에 실행

jobs:
  performance-test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v2
    
    - name: Setup Node.js
      uses: actions/setup-node@v2
      with:
        node-version: '18'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Install k6
      run: |
        curl -L https://github.com/loadimpact/k6/releases/download/v0.37.0/k6-v0.37.0-linux-amd64.tar.gz | tar xzf -
        sudo cp k6-v0.37.0-linux-amd64/k6 /usr/local/bin
    
    - name: Run performance tests
      run: k6 run performance-tests/load-test.js --out json=results.json
    
    - name: Analyze results
      run: node performance-tests/analyze-results.js
    
    - name: Check performance budgets
      run: node performance-tests/check-budget.js
    
    - name: Upload results
      uses: actions/upload-artifact@v2
      with:
        name: performance-results
        path: |
          results.json
          performance-report.html

성능 예산 설정 및 강제화

성능 예산은 애플리케이션의 성능 목표를 명확하게 정의하고 강제한다.

성능 예산 검사 스크립트:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// performance-tests/check-budget.js
const fs = require('fs');

console.log('성능 예산 검사 중...');

// 결과 로드
const results = JSON.parse(fs.readFileSync('results.json', 'utf8'));

// 성능 예산 정의
const budget = {
  http_req_duration: {
    p95: 200,  // p95 응답 시간 200ms 이하
    p99: 500   // p99 응답 시간 500ms 이하
  },
  http_req_failed: {
    rate: 0.01  // 오류율 1% 이하
  },
  http_reqs: {
    rate: 100   // 초당 최소 100 요청 처리
  }
};

// 예산 검사
const violations = [];

if (results.metrics.http_req_duration.values['p(95)'] > budget.http_req_duration.p95) {
  violations.push(`p95 응답 시간 ${results.metrics.http_req_duration.values['p(95)']}ms가 예산 ${budget.http_req_duration.p95}ms를 초과했습니다.`);
}

if (results.metrics.http_req_duration.values['p(99)'] > budget.http_req_duration.p99) {
  violations.push(`p99 응답 시간 ${results.metrics.http_req_duration.values['p(99)']}ms가 예산 ${budget.http_req_duration.p99}ms를 초과했습니다.`);
}

const failRate = results.metrics.http_req_failed.values.rate;
if (failRate > budget.http_req_failed.rate) {
  violations.push(`오류율 ${failRate}가 예산 ${budget.http_req_failed.rate}를 초과했습니다.`);
}

const reqRate = results.metrics.http_reqs.values.rate;
if (reqRate < budget.http_reqs.rate) {
  violations.push(`요청 처리율 ${reqRate}이 예산 ${budget.http_reqs.rate}보다 낮습니다.`);
}

// 위반 사항 보고
if (violations.length > 0) {
  console.error('성능 예산 위반 사항:');
  violations.forEach(v => console.error(` - ${v}`));
  process.exit(1);  // 빌드 실패
} else {
  console.log('모든 성능 예산을 충족했습니다!');
  console.log(`p95 응답 시간: ${results.metrics.http_req_duration.values['p(95)']}ms (예산: ${budget.http_req_duration.p95}ms)`);
  console.log(`p99 응답 시간: ${results.metrics.http_req_duration.values['p(99)']}ms (예산: ${budget.http_req_duration.p99}ms)`);
  console.log(`오류율: ${failRate} (예산: ${budget.http_req_failed.rate})`);
  console.log(`요청 처리율: ${reqRate} (예산: ${budget.http_reqs.rate})`);
}

실시간 성능 모니터링

성능 테스팅과 함께 실시간 성능 모니터링을 구현하면 실제 환경에서 발생하는 성능 문제를 빠르게 감지할 수 있다.

모니터링 시스템 구축 전략:

  1. 애플리케이션 수준 메트릭 수집:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    
    // Node.js Express 애플리케이션 모니터링 예시
    const express = require('express');
    const promClient = require('prom-client');
    const app = express();
    
    // 메트릭 레지스트리 생성
    const register = new promClient.Registry();
    promClient.collectDefaultMetrics({ register });
    
    // 커스텀 메트릭 정의
    const httpRequestDurationMicroseconds = new promClient.Histogram({
      name: 'http_request_duration_ms',
      help: 'HTTP 요청 처리 시간(ms)',
      labelNames: ['method', 'route', 'status_code'],
      buckets: [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000]
    });
    
    const httpRequestCounter = new promClient.Counter({
      name: 'http_requests_total',
      help: '총 HTTP 요청 수',
      labelNames: ['method', 'route', 'status_code']
    });
    
    register.registerMetric(httpRequestDurationMicroseconds);
    register.registerMetric(httpRequestCounter);
    
    // 미들웨어로 모든 요청 측정
    app.use((req, res, next) => {
      const start = Date.now();
      const path = req.path;
      const method = req.method;
    
      res.on('finish', () => {
        const duration = Date.now() - start;
        const statusCode = res.statusCode;
    
        httpRequestDurationMicroseconds
          .labels(method, path, statusCode)
          .observe(duration);
    
        httpRequestCounter
          .labels(method, path, statusCode)
          .inc();
      });
    
      next();
    });
    
    // Prometheus 메트릭 엔드포인트
    app.get('/metrics', async (req, res) => {
      res.set('Content-Type', register.contentType);
      res.end(await register.metrics());
    });
    
    // 애플리케이션 라우트
    app.get('/api/products', (req, res) => {
      // 제품 목록 반환
    });
    
    app.listen(3000, () => {
      console.log('서버가 3000 포트에서 실행 중입니다.');
    });
    
  2. 데이터베이스 성능 모니터링:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    
    // MongoDB 연결 모니터링 예시
    const mongoose = require('mongoose');
    const promClient = require('prom-client');
    
    const mongoDbOpsCounter = new promClient.Counter({
      name: 'mongodb_operations_total',
      help: 'MongoDB 작업 수',
      labelNames: ['operation', 'collection']
    });
    
    // 몽구스 미들웨어를 사용하여 쿼리 모니터링
    mongoose.plugin(schema => {
      // 쿼리 미들웨어
      ['find', 'findOne', 'findOneAndUpdate', 'findOneAndDelete', 'update', 'updateOne', 'deleteOne'].forEach(method => {
        schema.pre(method, function() {
          this._startTime = Date.now();
        });
    
        schema.post(method, function() {
          const duration = Date.now() - this._startTime;
          const collection = this.mongooseCollection.name;
    
          mongoDbOpsCounter.labels(method, collection).inc();
    
          if (duration > 100) { // 100ms 이상 걸리는 쿼리 로깅
            console.warn(`느린 쿼리 감지: ${method} ${collection} (${duration}ms)`);
          }
        });
      });
    });
    
  3. 시스템 수준 모니터링:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    
    // Node.js 시스템 메트릭 수집 예시
    const os = require('os');
    const promClient = require('prom-client');
    
    // CPU 사용량 게이지
    const cpuUsageGauge = new promClient.Gauge({
      name: 'system_cpu_usage',
      help: 'CPU 사용량 퍼센트'
    });
    
    // 메모리 사용량 게이지
    const memoryUsageGauge = new promClient.Gauge({
      name: 'system_memory_usage_bytes',
      help: '시스템 메모리 사용량(바이트)',
      labelNames: ['type']
    });
    
    // 1분마다 시스템 메트릭 수집
    setInterval(() => {
      // CPU 사용량
      const cpus = os.cpus();
      let totalIdle = 0;
      let totalTick = 0;
    
      cpus.forEach(cpu => {
        for (const type in cpu.times) {
          totalTick += cpu.times[type];
        }
        totalIdle += cpu.times.idle;
      });
    
      const idleRatio = totalIdle / totalTick;
      cpuUsageGauge.set((1 - idleRatio) * 100);
    
      // 메모리 사용량
      const totalMem = os.totalmem();
      const freeMem = os.freemem();
      const usedMem = totalMem - freeMem;
    
      memoryUsageGauge.labels('total').set(totalMem);
      memoryUsageGauge.labels('free').set(freeMem);
      memoryUsageGauge.labels('used').set(usedMem);
    }, 60000);
    

성능 테스트 결과 분석 및 최적화

성능 테스트는 데이터를 수집하는 것에서 그치지 않고, 결과를 분석하여 실제 최적화로 이어져야 한다.

성능 병목 식별

성능 테스트 결과에서 병목 현상을 식별하는 방법:

  1. 응답 시간 분포 분석:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    
    // 응답 시간 분포 분석 (Node.js 예시)
    const fs = require('fs');
    const results = JSON.parse(fs.readFileSync('results.json', 'utf8'));
    
    // 응답 시간 백분위수 분석
    const p50 = results.metrics.http_req_duration.values['p(50)'];
    const p90 = results.metrics.http_req_duration.values['p(90)'];
    const p95 = results.metrics.http_req_duration.values['p(95)'];
    const p99 = results.metrics.http_req_duration.values['p(99)'];
    
    console.log('응답 시간 분포:');
    console.log(`- 중앙값(p50): ${p50}ms`);
    console.log(`- p90: ${p90}ms`);
    console.log(`- p95: ${p95}ms`);
    console.log(`- p99: ${p99}ms`);
    
    // 응답 시간 편차 분석
    const avg = results.metrics.http_req_duration.values.avg;
    const med = p50;
    const stdDev = results.metrics.http_req_duration.values.std;
    
    console.log(`평균과 중앙값의 차이: ${Math.abs(avg - med).toFixed(2)}ms`);
    console.log(`표준 편차: ${stdDev.toFixed(2)}ms`);
    
    // 높은 편차는 비일관적인 성능을 나타냄
    if (stdDev > avg * 0.5) {
      console.warn('응답 시간의 표준 편차가 높습니다. 일부 요청이 다른 요청보다 훨씬 느립니다.');
    }
    
    // p99와 중앙값의 차이가 큰 경우
    if (p99 > med * 5) {
      console.warn('p99와 중앙값의 차이가 크게 나타납니다. 일부 요청에서 심각한 지연이 발생합니다.');
    }
    
  2. 엔드포인트별 성능 분석:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    
    // 엔드포인트별 성능 분석 (Node.js 예시)
    const urls = {};
    
    results.data.forEach(point => {
      const url = point.tags.url;
      if (!urls[url]) {
        urls[url] = {
          count: 0,
          totalTime: 0,
          times: []
        };
      }
    
      urls[url].count++;
      urls[url].totalTime += point.values.http_req_duration;
      urls[url].times.push(point.values.http_req_duration);
    });
    
    // 각 URL의 성능 통계 계산
    Object.keys(urls).forEach(url => {
      const data = urls[url];
      data.times.sort((a, b) => a - b);
    
      const avgTime = data.totalTime / data.count;
      const medianTime = data.times[Math.floor(data.times.length / 2)];
      const p95Time = data.times[Math.floor(data.times.length * 0.95)];
    
      console.log(`\n엔드포인트: ${url}`);
      console.log(`- 총 요청 수: ${data.count}`);
      console.log(`- 평균 응답 시간: ${avgTime.toFixed(2)}ms`);
      console.log(`- 중앙값 응답 시간: ${medianTime.toFixed(2)}ms`);
      console.log(`- p95 응답 시간: ${p95Time.toFixed(2)}ms`);
    
      // 문제가 있는 엔드포인트 식별
      if (avgTime > 200) {
        console.warn(`  주의: 평균 응답 시간이 높습니다 (${avgTime.toFixed(2)}ms)`);
      }
    
      if (p95Time > avgTime * 3) {
        console.warn(`  주의: 일부 요청의 응답 시간이 매우 깁니다 (p95: ${p95Time.toFixed(2)}ms)`);
      }
    });
    
    // 가장 느린 엔드포인트 식별
    const slowestEndpoints = Object.keys(urls)
      .sort((a, b) => {
        const avgA = urls[a].totalTime / urls[a].count;
        const avgB = urls[b].totalTime / urls[b].count;
        return avgB - avgA;
      })
      .slice(0, 3);
    
    console.log('\n가장 느린 엔드포인트:');
    slowestEndpoints.forEach((url, i) => {
      const avgTime = urls[url].totalTime / urls[url].count;
      console.log(`${i + 1}. ${url} - 평균 ${avgTime.toFixed(2)}ms`);
    });
    
  3. 자원 사용량 분석:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    
    // 자원 사용량 분석 (Node.js 예시)
    // 가정: 외부 모니터링 시스템에서 JSON 형태로 자원 사용량 데이터를 제공
    
    const resourceData = JSON.parse(fs.readFileSync('resource-metrics.json', 'utf8'));
    
    // CPU 사용량 분석
    const cpuData = resourceData.cpu;
    const avgCpuUsage = cpuData.reduce((sum, point) => sum + point.value, 0) / cpuData.length;
    const maxCpuUsage = Math.max(...cpuData.map(point => point.value));
    
    console.log('\nCPU 사용량:');
    console.log(`- 평균: ${avgCpuUsage.toFixed(2)}%`);
    console.log(`- 최대: ${maxCpuUsage.toFixed(2)}%`);
    
    if (maxCpuUsage > 90) {
      console.warn('  주의: CPU 사용량이 90%를 초과했습니다. CPU가 병목일 수 있습니다.');
    }
    
    // 메모리 사용량 분석
    const memData = resourceData.memory;
    const avgMemUsage = memData.reduce((sum, point) => sum + point.value, 0) / memData.length;
    const maxMemUsage = Math.max(...memData.map(point => point.value));
    
    console.log('\n메모리 사용량:');
    console.log(`- 평균: ${(avgMemUsage / 1024 / 1024).toFixed(2)}MB`);
    console.log(`- 최대: ${(maxMemUsage / 1024 / 1024).toFixed(2)}MB`);
    
    // 메모리 증가 추세 분석 (메모리 누수 감지)
    const firstMemUsage = memData[0].value;
    const lastMemUsage = memData[memData.length - 1].value;
    const memoryGrowth = lastMemUsage - firstMemUsage;
    
    if (memoryGrowth > 0) {
      const growthPercent = (memoryGrowth / firstMemUsage * 100).toFixed(2);
      console.log(`- 테스트 기간 동안 메모리 증가: ${growthPercent}%`);
    
      if (growthPercent > 20) {
        console.warn('  주의: 메모리 사용량이 지속적으로 증가하고 있습니다. 메모리 누수가 있을 수 있습니다.');
      }
    }
    

최적화 전략 수립

성능 테스트 결과 분석을 바탕으로 최적화 전략을 수립한다:

  1. 응답 시간 최적화:
    • 데이터베이스 쿼리 최적화 (인덱스 추가, 쿼리 재작성)
    • 캐싱 전략 구현 (Redis, 메모리 캐시)
    • 비동기 처리 도입 (백그라운드 작업)
  2. 처리량 개선:
    • 수평적 확장 (더 많은 서버 인스턴스)
    • 로드 밸런싱 최적화
    • 병렬 처리 구현
  3. 자원 사용 최적화:
    • 메모리 누수 해결
    • CPU 집약적 작업 최적화
    • 디스크 I/O 및 네트워크 최적화

최적화 프로세스 예시:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// 고부하 API 엔드포인트 최적화 계획 예시
/*
 * 문제: /api/reports/generate 엔드포인트가 p95에서 3000ms의 느린 응답 시간을 보임
 * 
 * 분석:
 * 1. DB 쿼리 실행 시간: 1800ms (총 시간의 60%)
 * 2. 데이터 처리: 900ms (총 시간의 30%)
 * 3. 응답 생성: 300ms (총 시간의 10%)
 * 
 * 최적화 계획:
 * 
 * 1단계: DB 쿼리 최적화
 * - 실행 계획 분석: 인덱스 누락 발견
 * - 복합 인덱스 추가: db.collection.createIndex({ user_id: 1, created_at: -1 })
 * - 결과: 쿼리 시간 1800ms -> 400ms (78% 감소)
 * 
 * 2단계: 데이터 처리 최적화
 * - 병렬 처리 구현
 * - 데이터 처리 알고리즘 개선
 * - 결과: 처리 시간 900ms -> 300ms (67% 감소)
 * 
 * 3단계: 캐싱 전략 구현
 * - Redis 캐싱 도입
 * - 일반적인 보고서 결과 사전 계산
 * - 결과: 캐시 히트 시 응답 시간 3000ms -> 50ms (98% 감소)
 * 
 * 전체 결과:
 * - 최적화 전 p95 응답 시간: 3000ms
 * - 최적화 후 p95 응답 시간: 700ms (77% 개선)
 * - 캐시 히트 시 응답 시간: 50ms (98% 개선)
 */

// 최적화 효과 측정을 위한 A/B 테스트 스크립트
const express = require('express');
const app = express();

// 원본 구현
app.get('/api/reports/generate/original', async (req, res) => {
  // 원래 느린 구현
});

// 최적화된 구현
app.get('/api/reports/generate/optimized', async (req, res) => {
  // 최적화된 구현
});

// A/B 테스트를 위한 라우트
app.get('/api/reports/generate', (req, res) => {
  // 50% 확률로 최적화된 엔드포인트로 라우팅
  if (Math.random() < 0.5) {
    req.url = '/api/reports/generate/optimized';
  } else {
    req.url = '/api/reports/generate/original';
  }
  app._router.handle(req, res);
});

// 성능 메트릭 수집
app.use((req, res, next) => {
  const start = Date.now();
  res.on('finish', () => {
    const duration = Date.now() - start;
    const path = req.path;
    
    // 메트릭 저장 로직
    saveMetric(path, duration);
  });
  next();
});

고급 성능 테스팅 기법

카오스 엔지니어링

카오스 엔지니어링은 시스템에 의도적으로 장애를 주입하여 회복력을 테스트하는 기법이다.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// 간단한 카오스 테스트 스크립트 예시
const axios = require('axios');
const { execSync } = require('child_process');

// 다양한 카오스 이벤트 정의
const chaosEvents = [
  // 높은 CPU 부하 생성
  {
    name: 'high-cpu-load',
    execute: () => {
      console.log('CPU 부하 주입 중...');
      // Linux 시스템에서 CPU 부하 생성
      execSync('dd if=/dev/zero of=/dev/null bs=1M count=10000', { timeout: 30000 });
    }
  },
  // 메모리 부하 생성
  {
    name: 'memory-pressure',
    execute: () => {
      console.log('메모리 부하 주입 중...');
      const memoryHogs = [];
      for (let i = 0; i < 20; i++) {
        memoryHogs.push(new Array(1000000).fill('x'));
      }
      // 10초 후 메모리 해제
      setTimeout(() => {
        while (memoryHogs.length) memoryHogs.pop();
      }, 10000);
    }
  },
  // 네트워크 지연 추가
  {
    name: 'network-latency',
    execute: () => {
      console.log('네트워크 지연 주입 중...');
      // Linux에서 네트워크 지연 추가 (root 권한 필요)
      execSync('tc qdisc add dev eth0 root netem delay 200ms', { timeout: 5000 });
      
      // 30초 후 지연 제거
      setTimeout(() => {
        try {
          execSync('tc qdisc del dev eth0 root', { timeout: 5000 });
        } catch (e) {
          console.error('네트워크 지연 제거 실패:', e);
        }
      }, 30000);
    }
  }
];

// 시스템 상태 모니터링 함수
async function monitorSystem() {
  try {
    // 핵심 API 엔드포인트 확인
    const startTime = Date.now();
    const response = await axios.get('https://api.example.com/health');
    const responseTime = Date.now() - startTime;
    
    console.log(`상태 확인: ${response.status}, 응답 시간: ${responseTime}ms`);
    
    return {
      status: response.status,
      responseTime,
      timestamp: new Date().toISOString()
    };
  } catch (error) {
    console.error('모니터링 오류:', error.message);
    return {
      status: error.response?.status || 0,
      responseTime: Date.now() - startTime,
      error: error.message,
      timestamp: new Date().toISOString()
    };
  }
}

// 카오스 테스트 실행
async function runChaosTest() {
  console.log('카오스 테스트 시작...');
  
  // 기준 성능 측정
  console.log('기준 성능 측정 중...');
  const baselineResults = [];
  for (let i = 0; i < 10; i++) {
    baselineResults.push(await monitorSystem());
    await new Promise(resolve => setTimeout(resolve, 1000));
  }
  
  // 카오스 이벤트 무작위 선택
  const selectedEvent = chaosEvents[Math.floor(Math.random() * chaosEvents.length)];
  console.log(`카오스 이벤트 선택: ${selectedEvent.name}`);
  
  // 카오스 이벤트 실행
  selectedEvent.execute();
  
  // 시스템 반응 모니터링
  console.log('시스템 반응 모니터링 중...');
  const chaosResults = [];
  for (let i = 0; i < 20; i++) {
    chaosResults.push(await monitorSystem());
    await new Promise(resolve => setTimeout(resolve, 1000));
  }
  
  // 시스템 복구 확인
  console.log('시스템 복구 확인 중...');
  const recoveryResults = [];
  for (let i = 0; i < 10; i++) {
    recoveryResults.push(await monitorSystem());
    await new Promise(resolve => setTimeout(resolve, 1000));
  }
  
  // 결과 분석
  const baselineAvgResponseTime = baselineResults.reduce((sum, r) => sum + r.responseTime, 0) / baselineResults.length;
  const chaosAvgResponseTime = chaosResults.reduce((sum, r) => sum + r.responseTime, 0) / chaosResults.length;
  const recoveryAvgResponseTime = recoveryResults.reduce((sum, r) => sum + r.responseTime, 0) / recoveryResults.length;
  
  console.log('\n테스트 결과:');
  console.log(`- 기준 평균 응답 시간: ${baselineAvgResponseTime.toFixed(2)}ms`);
  console.log(`- 카오스 중 평균 응답 시간: ${chaosAvgResponseTime.toFixed(2)}ms`);
  console.log(`- 복구 후 평균 응답 시간: ${recoveryAvgResponseTime.toFixed(2)}ms`);
  
  const errorDuringChaos = chaosResults.some(r => r.status !== 200);
  if (errorDuringChaos) {
    console.warn('경고: 카오스 이벤트 중 오류 발생');
  }
  
  const recoveryRatio = recoveryAvgResponseTime / baselineAvgResponseTime;
  if (recoveryRatio > 1.5) {
    console.warn(`경고: 시스템이 완전히 복구되지 않았습니다. 복구 시간이 기준보다 ${((recoveryRatio - 1) * 100).toFixed(0)}% 높습니다.`);
  } else {
    console.log('시스템이 성공적으로 복구되었습니다.');
  }
}

runChaosTest().catch(console.error);

사용자 여정 테스트

사용자 여정 테스트는 실제 사용자 행동을 시뮬레이션하여 엔드투엔드 성능을 평가한다.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
// k6를 사용한 사용자 여정 테스트 예시
import { sleep, check, group } from 'k6';
import http from 'k6/http';

export const options = {
  scenarios: {
    shopping_flow: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { duration: '1m', target: 100 },
        { duration: '3m', target: 100 },
        { duration: '1m', target: 0 }
      ],
      gracefulRampDown: '30s'
    }
  },
  thresholds: {
    'group_duration{group:login}': ['p(95)<1000'],
    'group_duration{group:browse_products}': ['p(95)<2000'],
    'group_duration{group:add_to_cart}': ['p(95)<1000'],
    'group_duration{group:checkout}': ['p(95)<3000']
  }
};

export default function() {
  const baseUrl = 'https://api.example.com';
  let userId, token, cartId;
  
  group('login', () => {
    const loginRes = http.post(`${baseUrl}/auth/login`, JSON.stringify({
      email: `user_${__VU}@example.com`,
      password: 'testpassword'
    }), {
      headers: { 'Content-Type': 'application/json' }
    });
    
    check(loginRes, {
      'login successful': (r) => r.status === 200,
      'token received': (r) => r.json('token') !== undefined
    });
    
    token = loginRes.json('token');
    userId = loginRes.json('userId');
  });
  
  sleep(Math.random() * 3 + 1); // 1-4초 대기
  
  group('browse_products', () => {
    const categoriesRes = http.get(`${baseUrl}/categories`, {
      headers: { 'Authorization': `Bearer ${token}` }
    });
    
    check(categoriesRes, {
      'categories loaded': (r) => r.status === 200,
      'has categories': (r) => r.json('items').length > 0
    });
    
    const categoryId = categoriesRes.json('items.0.id');
    
    const productsRes = http.get(`${baseUrl}/categories/${categoryId}/products`, {
      headers: { 'Authorization': `Bearer ${token}` }
    });
    
    check(productsRes, {
      'products loaded': (r) => r.status === 200,
      'has products': (r) => r.json('items').length > 0
    });
    
    const productId = productsRes.json('items.0.id');
    
    const productDetailsRes = http.get(`${baseUrl}/products/${productId}`, {
      headers: { 'Authorization': `Bearer ${token}` }
    });
    
    check(productDetailsRes, {
      'product details loaded': (r) => r.status === 200
    });
  });
  
  sleep(Math.random() * 5 + 2); // 2-7초 대기
  
  group('add_to_cart', () => {
    const createCartRes = http.post(`${baseUrl}/carts`, JSON.stringify({
      userId: userId
    }), {
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${token}`
      }
    });
    
    check(createCartRes, {
      'cart created': (r) => r.status === 201,
      'cart id received': (r) => r.json('id') !== undefined
    });
    
    cartId = createCartRes.json('id');
    
    const addToCartRes = http.post(`${baseUrl}/carts/${cartId}/items`, JSON.stringify({
      productId: 123,
      quantity: 2
    }), {
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${token}`
      }
    });
    
    check(addToCartRes, {
      'product added to cart': (r) => r.status === 200,
      'cart updated': (r) => r.json('items').length > 0
    });
    
    const viewCartRes = http.get(`${baseUrl}/carts/${cartId}`, {
      headers: { 'Authorization': `Bearer ${token}` }
    });
    
    check(viewCartRes, {
      'cart viewed': (r) => r.status === 200,
      'cart has items': (r) => r.json('items').length > 0
    });
  });
  
  sleep(Math.random() * 3 + 1); // 1-4초 대기
  
  group('checkout', () => {
    const checkoutRes = http.post(`${baseUrl}/orders`, JSON.stringify({
      cartId: cartId,
      paymentDetails: {
        method: 'credit_card',
        cardNumber: '4111111111111111',
        expiryMonth: 12,
        expiryYear: 2030,
        cvv: '123'
      },
      shippingAddress: {
        name: 'Test User',
        line1: '123 Test St',
        city: 'Test City',
        state: 'TS',
        postalCode: '12345',
        country: 'Test Country'
      }
    }), {
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${token}`
      }
    });
    
    check(checkoutRes, {
      'order placed': (r) => r.status === 201,
      'order id received': (r) => r.json('orderId') !== undefined
    });
    
    const orderId = checkoutRes.json('orderId');
    
    const orderDetailsRes = http.get(`${baseUrl}/orders/${orderId}`, {
      headers: { 'Authorization': `Bearer ${token}` }
    });
    
    check(orderDetailsRes, {
      'order details loaded': (r) => r.status === 200,
      'order status correct': (r) => r.json('status') === 'processing'
    });
  });
  
  sleep(Math.random() * 5); // 0-5초 대기
}

성능 회귀 테스트

성능 회귀 테스트는 시간이 지남에 따라 애플리케이션 성능이 어떻게 변화하는지 추적한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// GitHub Actions 워크플로우 - 성능 회귀 테스트 예시
// .github/workflows/performance-regression.yml

name: Performance Regression Test

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  performance-test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v2
    
    - name: Setup Node.js
      uses: actions/setup-node@v2
      with:
        node-version: '18'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Install k6
      run: |
        curl -L https://github.com/loadimpact/k6/releases/download/v0.37.0/k6-v0.37.0-linux-amd64.tar.gz | tar xzf -
        sudo cp k6-v0.37.0-linux-amd64/k6 /usr/local/bin
    
    - name: 기준 브랜치 성능 테스트
      if: github.event_name == 'pull_request'
      run: |
        git checkout ${{ github.base_ref }}
        npm run build
        npm run start:test &
        sleep 10
        k6 run --out json=baseline-results.json performance-tests/benchmark.js
        kill $(lsof -t -i:3000)
    
    - name: 현재 브랜치 성능 테스트
      run: |
        git checkout ${{ github.head_ref || github.ref }}
        npm run build
        npm run start:test &
        sleep 10
        k6 run --out json=current-results.json performance-tests/benchmark.js
        kill $(lsof -t -i:3000)
    
    - name: 성능 회귀 분석
      if: github.event_name == 'pull_request'
      run: node performance-tests/compare-results.js
    
    - name: 성능 보고서 생성
      run: node performance-tests/generate-report.js
    
    - name: 성능 보고서 업로드
      uses: actions/upload-artifact@v2
      with:
        name: performance-report
        path: performance-report/

비교 스크립트:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
// performance-tests/compare-results.js
const fs = require('fs');

console.log('성능 회귀 분석 시작…');

// 결과 파일 로드
const baselineResults = JSON.parse(fs.readFileSync('baseline-results.json', 'utf8'));
const currentResults = JSON.parse(fs.readFileSync('current-results.json', 'utf8'));

// 주요 메트릭 비교
const comparison = {
  http_req_duration: {
    baseline: {
      avg: baselineResults.metrics.http_req_duration.values.avg,
      p95: baselineResults.metrics.http_req_duration.values['p(95)']
    },
    current: {
      avg: currentResults.metrics.http_req_duration.values.avg,
      p95: currentResults.metrics.http_req_duration.values['p(95)']
    }
  },
  http_reqs: {
    baseline: baselineResults.metrics.http_reqs.values.rate,
    current: currentResults.metrics.http_reqs.values.rate
  },
  http_req_failed: {
    baseline: baselineResults.metrics.http_req_failed.values.rate,
    current: currentResults.metrics.http_req_failed.values.rate
  }
};

// 성능 변화 계산
const change = {
  http_req_duration: {
    avg: {
      absolute: comparison.http_req_duration.current.avg - comparison.http_req_duration.baseline.avg,
      percentage: (comparison.http_req_duration.current.avg / comparison.http_req_duration.baseline.avg - 1) * 100
    },
    p95: {
      absolute: comparison.http_req_duration.current.p95 - comparison.http_req_duration.baseline.p95,
      percentage: (comparison.http_req_duration.current.p95 / comparison.http_req_duration.baseline.p95 - 1) * 100
    }
  },
  http_reqs: {
    absolute: comparison.http_reqs.current - comparison.http_reqs.baseline,
    percentage: (comparison.http_reqs.current / comparison.http_reqs.baseline - 1) * 100
  },
  http_req_failed: {
    absolute: comparison.http_req_failed.current - comparison.http_req_failed.baseline,
    percentage: comparison.http_req_failed.baseline === 0 
      ? 'N/A' 
      : (comparison.http_req_failed.current / comparison.http_req_failed.baseline - 1) * 100
  }
};

// 결과 출력
console.log('성능 비교 결과:');
console.log('==========================================');
console.log('응답 시간 (평균):');
console.log(`- 기준: ${comparison.http_req_duration.baseline.avg.toFixed(2)}ms`);
console.log(`- 현재: ${comparison.http_req_duration.current.avg.toFixed(2)}ms`);
console.log(`- 변화: ${change.http_req_duration.avg.absolute.toFixed(2)}ms (${change.http_req_duration.avg.percentage.toFixed(2)}%)`);
console.log('');

console.log('응답 시간 (p95):');
console.log(`- 기준: ${comparison.http_req_duration.baseline.p95.toFixed(2)}ms`);
console.log(`- 현재: ${comparison.http_req_duration.current.p95.toFixed(2)}ms`);
console.log(`- 변화: ${change.http_req_duration.p95.absolute.toFixed(2)}ms (${change.http_req_duration.p95.percentage.toFixed(2)}%)`);
console.log('');

console.log('처리량 (초당 요청):');
console.log(`- 기준: ${comparison.http_reqs.baseline.toFixed(2)} req/s`);
console.log(`- 현재: ${comparison.http_reqs.current.toFixed(2)} req/s`);
console.log(`- 변화: ${change.http_reqs.absolute.toFixed(2)} req/s (${change.http_reqs.percentage.toFixed(2)}%)`);
console.log('');

console.log('오류율:');
console.log(`- 기준: ${(comparison.http_req_failed.baseline * 100).toFixed(2)}%`);
console.log(`- 현재: ${(comparison.http_req_failed.current * 100).toFixed(2)}%`);
console.log(`- 변화: ${typeof change.http_req_failed.percentage === 'string' ? 'N/A' : change.http_req_failed.percentage.toFixed(2) + '%'}`);
console.log('==========================================');

// 성능 저하 감지
const performanceIssues = [];

// 평균 응답 시간이 10% 이상 증가
if (change.http_req_duration.avg.percentage > 10) {
  performanceIssues.push(`평균 응답 시간이 ${change.http_req_duration.avg.percentage.toFixed(2)}% 증가했습니다.`);
}

// p95 응답 시간이 15% 이상 증가
if (change.http_req_duration.p95.percentage > 15) {
  performanceIssues.push(`p95 응답 시간이 ${change.http_req_duration.p95.percentage.toFixed(2)}% 증가했습니다.`);
}

// 처리량이 10% 이상 감소
if (change.http_reqs.percentage < -10) {
  performanceIssues.push(`처리량이 ${Math.abs(change.http_reqs.percentage).toFixed(2)}% 감소했습니다.`);
}

// 오류율이 증가
if (typeof change.http_req_failed.percentage === 'number' && change.http_req_failed.percentage > 0) {
  performanceIssues.push(`오류율이 증가했습니다.`);
}

// 성능 저하 감지 시 CI 실패 처리
if (performanceIssues.length > 0) {
  console.error('\n성능 회귀 감지!');
  performanceIssues.forEach(issue => console.error(`- ${issue}`));
  console.error('\n이 변경 사항은 성능 저하를 야기할 수 있습니다. 코드를 검토하고 최적화하십시오.');
  process.exit(1);
} else {
  console.log('\n성능 회귀가 감지되지 않았습니다. 변경 사항이 성능에 부정적인 영향을 미치지 않습니다.');
}

// 결과를 JSON 파일로 저장
fs.writeFileSync(
  'performance-comparison.json',
  JSON.stringify({ comparison, change, issues: performanceIssues }, null, 2)
);

용어 정리

용어설명

참고 및 출처