Performance Testing
성능 테스팅의 기본 원리
성능 테스팅은 시스템이 예상된 부하 조건에서 어떻게 작동하는지 측정하고 평가하는 과정이다. 이는 단순히 ‘시스템이 작동하는가?‘를 넘어 ‘얼마나 효율적으로 작동하는가?‘를 확인하는 것이다.
성능 테스팅의 주요 목적
- 성능 병목 현상 식별: 시스템의 어떤 부분이 전체 성능을 저하시키는지 파악한다.
- 확장성 평가: 시스템이 증가하는 부하에 어떻게 대응하는지 측정한다.
- 사용자 경험 예측: 실제 환경에서 사용자가 경험할 성능 수준을 추정한다.
- 최적화 효과 검증: 성능 개선 작업의 효과를 객관적으로 평가한다.
- 안정성 확인: 지속적인 부하 하에서 시스템의 안정성을 확인한다.
성능 테스팅의 주요 유형
백엔드 성능을 평가하기 위해 다양한 유형의 테스트가 필요하다. 각 테스트는 특정 측면에 초점을 맞추고 있다.
부하 테스트(Load Testing)
부하 테스트는 예상되는 일반적인 사용자 부하에서 시스템의 성능을 평가한다.
구현 방법:
|
|
측정 지표:
- 응답 시간: 평균, 중앙값, 90번째 백분위수, 95번째 백분위수, 99번째 백분위수
- 처리량: 초당 요청 수(RPS)
- 오류율: 총 요청 중 실패한 요청의 비율
- 자원 사용량: CPU, 메모리, 디스크 I/O, 네트워크 대역폭
스트레스 테스트(Stress Testing)
스트레스 테스트는 시스템의 한계를 확인하기 위해 극단적인 부하 조건에서 시스템을 평가한다.
구현 방법:
|
|
관찰 사항:
- 장애 지점: 시스템이 실패하기 시작하는 부하 수준
- 복구 능력: 극단적인 부하 후 시스템이 얼마나 빨리 정상 상태로 돌아오는지
- 오류 처리: 과부하 상황에서 시스템이 오류를 어떻게 처리하는지
- 자원 고갈: 메모리 누수, 연결 풀 고갈, DB 커넥션 한계 등
내구성 테스트(Endurance Testing)
내구성 테스트는 장시간 동안 지속적인 부하에서 시스템의 안정성을 평가한다.
구현 방법:
|
|
관찰 지표:
- 메모리 누수: 시간이 지남에 따라 메모리 사용량이 계속 증가하는지
- 자원 고갈: 데이터베이스 연결, 파일 핸들, 스레드 등의 자원이 고갈되는지
- 성능 저하: 시간이 지남에 따라 응답 시간이 점차 증가하는지
- 오류 발생 패턴: 특정 시간 후에 오류가 증가하기 시작하는지
4스파이크 테스트(Spike Testing)
스파이크 테스트는 갑작스러운 부하 증가에 대한 시스템의 반응을 평가한다.
구현 방법:
|
|
관찰 사항:
- 자동 확장: 클라우드 환경에서 자동 확장 메커니즘이 얼마나 빨리 반응하는지
- 서비스 저하: 부하 급증 중 서비스 품질이 어떻게 저하되는지
- 장애 복구: 스파이크 후 정상 상태로 돌아오는 데 걸리는 시간
- 병목 현상: 부하 급증 중 가장 먼저 병목 현상이 발생하는 시스템 구성 요소
용량 테스트(Capacity Testing)
용량 테스트는 시스템이 처리할 수 있는 최대 부하를 결정한다.
구현 방법:
|
|
측정 지표:
- 최대 처리량: 품질 저하 없이 시스템이 처리할 수 있는 최대 RPS
- 최대 동시 사용자: 시스템이 지원할 수 있는 최대 동시 사용자 수
- 최대 데이터 처리: 시스템이 처리할 수 있는 최대 데이터 볼륨
- 자원 요구 사항: 특정 부하 수준을 지원하는 데 필요한 하드웨어 자원
성능 테스트 구현을 위한 도구
성능 테스트를 효과적으로 구현하기 위해 다양한 도구가 사용된다. 각 도구는 고유한 강점과 특징을 가지고 있다.
1. K6
k6는 현대적이고 개발자 친화적인 부하 테스팅 도구로, JavaScript로 테스트 스크립트를 작성할 수 있다.
특징:
- Go로 작성되어 빠른 실행 속도
- JavaScript ES6 스크립팅
- 확장 가능한 메트릭 및 출력 시스템
- 클라우드 서비스와의 통합
설치 및 기본 사용:
JMeter
Apache JMeter는 다양한 프로토콜과 서버 유형에 대한 부하 테스트를 수행할 수 있는 Java 기반 도구이다.
특징:
- 다양한 프로토콜 지원 (HTTP, HTTPS, JDBC, LDAP, SMTP 등)
- 그래픽 사용자 인터페이스
- 분산 테스트 지원
- 강력한 결과 분석 및 시각화
주요 구성 요소:
- Thread Group: 가상 사용자 그룹 정의
- Samplers: 요청 유형 지정 (HTTP, FTP, JDBC 등)
- Listeners: 결과 수집 및 시각화
- Assertions: 응답 검증
Locust
Locust는 Python으로 작성된 오픈 소스 부하 테스팅 도구로, 사용자 행동을 정의하기 쉽다.
특징:
- Python 기반 스크립팅
- 분산 테스트 지원
- 실시간 웹 UI
- 사용자 행동 시뮬레이션에 중점
기본 사용 예:
|
|
Gatling
Gatling은 Scala로 작성된 고성능 부하 테스팅 도구로, 비동기 처리에 중점을 둔다.
특징:
- 비동기 논블로킹 구조
- DSL을 사용한 시나리오 작성
- 상세한 보고서 생성
- 지속적 통합(CI) 지원
시나리오 예시:
|
|
Artillery
Artillery는 Node.js 기반의 현대적인 부하 테스팅 도구로, 설정이 간단하고 클라우드 네이티브 환경에 적합하다.
특징:
- YAML 기반 설정
- 강력한 시나리오 작성 기능
- 확장 가능한 플러그인 아키텍처
- WebSocket, Socket.io 지원
설정 예시:
|
|
벤치마킹: 성능 기준 확립
벤치마킹은 시스템의 성능 기준을 설정하고 시간이 지남에 따라 변화를 추적하는 과정이다.
벤치마킹의 주요 요소
- 기준 지표 선정:
- 응답 시간(백분위수별)
- 초당 요청 처리량(RPS)
- 오류율
- 리소스 사용률(CPU, 메모리, 디스크 I/O, 네트워크)
- 특정 작업 완료 시간(예: 데이터 처리 작업)
- 벤치마크 시나리오 정의:
- 일반적인 사용자 흐름
- 주요 비즈니스 프로세스
- 고부하 상황에서의 중요 기능
- 벤치마크 환경 표준화:
- 하드웨어 사양
- 운영 체제 및 설정
- 네트워크 조건
- 데이터베이스 상태
벤치마킹 구현 접근 방식
1. 자동화된 벤치마킹 파이프라인:
|
|
2. 벤치마크 결과 시각화:
|
|
지속적 성능 테스팅과 모니터링
성능 테스팅은 일회성 활동이 아니라 지속적인 프로세스의 일부가 되어야 한다.
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 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 포트에서 실행 중입니다.'); });
데이터베이스 성능 모니터링:
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)`); } }); }); });
시스템 수준 모니터링:
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 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와 중앙값의 차이가 크게 나타납니다. 일부 요청에서 심각한 지연이 발생합니다.'); }
엔드포인트별 성능 분석:
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`); });
자원 사용량 분석:
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(' 주의: 메모리 사용량이 지속적으로 증가하고 있습니다. 메모리 누수가 있을 수 있습니다.'); } }
최적화 전략 수립
성능 테스트 결과 분석을 바탕으로 최적화 전략을 수립한다:
- 응답 시간 최적화:
- 데이터베이스 쿼리 최적화 (인덱스 추가, 쿼리 재작성)
- 캐싱 전략 구현 (Redis, 메모리 캐시)
- 비동기 처리 도입 (백그라운드 작업)
- 처리량 개선:
- 수평적 확장 (더 많은 서버 인스턴스)
- 로드 밸런싱 최적화
- 병렬 처리 구현
- 자원 사용 최적화:
- 메모리 누수 해결
- CPU 집약적 작업 최적화
- 디스크 I/O 및 네트워크 최적화
최적화 프로세스 예시:
|
|
고급 성능 테스팅 기법
카오스 엔지니어링
카오스 엔지니어링은 시스템에 의도적으로 장애를 주입하여 회복력을 테스트하는 기법이다.
|
|
사용자 여정 테스트
사용자 여정 테스트는 실제 사용자 행동을 시뮬레이션하여 엔드투엔드 성능을 평가한다.
|
|
성능 회귀 테스트
성능 회귀 테스트는 시간이 지남에 따라 애플리케이션 성능이 어떻게 변화하는지 추적한다.
|
|
비교 스크립트:
|
|
용어 정리
용어 | 설명 |
---|---|