Load Testing

API 부하 테스트는 API가 예상된 사용자 부하와 그 이상의 상황에서 어떻게 동작하는지 검증하는 중요한 성능 테스트 유형이다.

API 부하 테스트의 기본 개념

부하 테스트란 무엇인가?

부하 테스트는 시스템에 점진적으로 부하를 증가시키면서 그 동작을, 분석하는 성능 테스트의 한 유형이다. API 부하 테스트는 특히 API가 다양한 부하 조건에서 어떻게 동작하는지 검증하는 데 초점을 맞춘다.

이런 테스트를 통해 다음과 같은 중요한 정보를 얻을 수 있다:

  • API의 최대 처리 용량(초당 요청 수)
  • 응답 시간과 부하 간의 관계
  • 병목 현상이 발생하는 지점
  • 시스템의 안정성과 확장성
  • 자원 사용률(CPU, 메모리, 네트워크 등)

다른 성능 테스트 유형과의 비교

부하 테스트는 다른 성능 테스트 유형과 목적과 접근 방식에서 차이가 있다:

테스트 유형주요 목적특징
부하 테스트 (Load Testing)예상 부하와 그 이상에서의 동작 검증점진적으로 부하를 증가시키며 안정적 동작 검증
스트레스 테스트 (Stress Testing)시스템의 한계 찾기시스템이 실패할 때까지 극단적인 부하 적용
내구성 테스트 (Endurance Testing)장시간 동안의 안정성 검증지속적인 부하를 장시간 유지하며 메모리 누수 등 검사
스파이크 테스트 (Spike Testing)갑작스러운 부하 증가에 대한 대응력 검증짧은 시간 동안 부하를 급격히 증가시켰다가 감소
볼륨 테스트 (Volume Testing)대량의 데이터 처리 능력 검증데이터베이스 크기나 요청/응답 크기를 증가시키며 테스트

API 부하 테스트가 중요한 이유

API 부하 테스트는 다음과 같은 이유로 API 개발 및 운영에서 필수적이다:

  1. 사용자 경험 보장: 부하가 증가하더라도 일관된 응답 시간을 유지하는지 확인
  2. 시스템 한계 파악: API가 처리할 수 있는 최대 부하를 사전에 파악하여 용량 계획 수립
  3. 확장성 검증: 시스템이 부하 증가에 따라 적절히 확장되는지 검증
  4. 병목 현상 식별: 성능 병목 지점을 찾아 최적화 기회 발견
  5. 다운타임 방지: 갑작스러운 트래픽 증가로 인한 서비스 중단 예방
  6. 비용 최적화: 필요한 인프라 자원을 더 정확하게 예측하여 비용 절감

API 부하 테스트 계획 및 준비

테스트 목표 설정

효과적인 부하 테스트를 위해서는 명확한 목표 설정이 필요하다:

  1. 성능 목표 정의:
    • 최대 허용 응답 시간(예: 95%의 요청이 500ms 이내에 처리)
    • 처리량 목표(예: 초당 1,000개 요청 처리)
    • 오류율 한계(예: 부하 조건에서 1% 미만의 오류)
  2. 부하 패턴 결정:
    • 점진적 증가: 사용자 수나 요청 비율을 점진적으로 증가
    • 일정 부하: 특정 수준의 부하를 일정 시간 동안 유지
    • 계단식 증가: 부하를 단계적으로 증가시키며 각 단계에서 시스템 안정화 시간 제공
  3. 측정 지표 선정:
    • 응답 시간(평균, 중앙값, 95/99 백분위수)
    • 처리량(초당 요청 수, 초당 처리된 데이터양)
    • 오류율 및 오류 유형
    • 자원 사용률(CPU, 메모리, 디스크 I/O, 네트워크)

테스트 시나리오 설계

실제 사용 패턴을 반영한 다양한 테스트 시나리오를 설계해야 한다:

  1. 주요 API 엔드포인트 식별:
    • 사용 빈도가 높은 엔드포인트
    • 비즈니스 크리티컬한 엔드포인트
    • 자원 집약적인 엔드포인트
  2. 사용자 흐름 기반 시나리오:
    • 실제 사용자 행동 패턴을 시뮬레이션
    • 다양한 API 호출의 현실적인 조합
    • 세션 기반 상호작용 모델링
  3. 다양한 데이터 크기 고려:
    • 다양한 크기의 요청/응답 페이로드
    • 데이터 양에 따른 성능 영향 측정
    • 대용량 데이터 처리 시나리오

예시 시나리오:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
시나리오 1: 사용자 인증 및 프로필 조회
- POST /api/auth/login (사용자 인증)
- GET /api/users/profile (프로필 조회)
- 목표 부하: 초당 500 사용자
- 단계적 증가: 1분 간격으로 100명씩 증가

시나리오 2: 제품 검색 및 상세 조회
- GET /api/products?query=keyword (제품 검색)
- GET /api/products/{id} (제품 상세 조회)
- 목표 부하: 초당 1,000 요청
- 지속 시간: 30분

테스트 환경 준비

신뢰할 수 있는 테스트 결과를 얻기 위해 적절한 테스트 환경을 준비해야 한다:

  1. 테스트 환경 선택:
    • 개발/테스트 환경: 초기 테스트 및 디버깅
    • 스테이징 환경: 프로덕션과 유사한 환경에서의 검증
    • 프로덕션 환경: 모니터링 모드로 제한적 테스트(주의 필요)
  2. 테스트 데이터 준비:
    • 충분한 양의 테스트 데이터
    • 다양한 데이터 프로필
    • 데이터 무결성 보장
  3. 모니터링 도구 설정:
    • 서버 모니터링(CPU, 메모리, 디스크, 네트워크)
    • 애플리케이션 성능 모니터링(APM)
    • 로그 수집 및 분석 도구
    • 데이터베이스 모니터링
  4. 외부 의존성 처리:
    • 서드파티 서비스 모킹 또는 샌드박스 환경 사용
    • 결제 게이트웨이 등 중요 서비스의 테스트 모드 활용
    • 외부 API 호출 제한 고려

API 부하 테스트 도구 및 구현

주요 부하 테스트 도구 비교

다양한 부하 테스트 도구 중 API 테스트에 적합한 주요 도구들을 살펴보면:

  1. Apache JMeter:
    • 가장 널리 사용되는 오픈소스 부하 테스트 도구
    • 다양한 프로토콜 지원(HTTP, HTTPS, SOAP, REST, WebSocket 등)
    • GUI 및 CLI 모드 제공
    • 확장 가능한 플러그인 시스템
    • 분산 테스트 지원
  2. k6:
    • 개발자 친화적인 모던 부하 테스트 도구
    • JavaScript로 테스트 스크립트 작성
    • 가볍고 리소스 효율적
    • 클라우드 서비스 통합
    • Grafana와의 통합을 통한 시각화
  3. Locust:
    • Python 기반 부하 테스트 프레임워크
    • 코드로 테스트 시나리오 정의
    • 분산 테스트 지원
    • 실시간 웹 UI 제공
    • 높은 확장성
  4. Gatling:
    • Scala 기반 고성능 부하 테스트 도구
    • 코드 기반 시나리오 작성
    • 뛰어난 보고서 생성
    • 리소스 효율적
    • CI/CD 통합 용이
  5. Postman/Newman:
    • API 개발 도구에서 출발한 테스트 환경
    • 사용자 친화적 인터페이스
    • Newman을 통한 CLI 실행
    • 기존 Postman 컬렉션 활용 가능
    • 다른 전문 부하 테스트 도구에 비해 제한적인 규모
도구언어/스크립팅학습 곡선분산 테스트확장성보고서적합한 경우
JMeterXML/GUI, BeanShell, JSR223중간우수매우 높음기본적다양한 프로토콜 테스트, 엔터프라이즈 환경
k6JavaScript낮음우수높음우수개발자 중심 환경, 클라우드 네이티브 앱
LocustPython낮음우수높음기본적Python 개발자, 사용자 행동 시뮬레이션
GatlingScala/DSL중간제한적높음매우 우수성능 엔지니어, 복잡한 시나리오
PostmanJavaScript매우 낮음제한적제한적제한적간단한 테스트, API 개발자 친화적

k6를 사용한 부하 테스트 구현 예제

k6는 직관적이고 개발자 친화적인 API 부하 테스트 도구이다. 다음은 k6를 사용한 간단한 부하 테스트 스크립트 예시:

 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
import http from 'k6/http';
import { sleep, check } from 'k6';
import { Rate } from 'k6/metrics';

// 사용자 정의 메트릭 설정
const errorRate = new Rate('errors');

// 테스트 구성
export const options = {
  stages: [
    { duration: '1m', target: 50 },   // 1분 동안 50명의 가상 사용자로 증가
    { duration: '3m', target: 50 },   // 3분 동안 50명의 가상 사용자 유지
    { duration: '1m', target: 100 },  // 1분 동안 100명의 가상 사용자로 증가
    { duration: '5m', target: 100 },  // 5분 동안 100명의 가상 사용자 유지
    { duration: '1m', target: 0 },    // 1분 동안 0명으로 감소 (정상 종료)
  ],
  thresholds: {
    http_req_duration: ['p(95)<500'], // 95%의 요청이 500ms 미만이어야 함
    'http_req_duration{name:login}': ['p(99)<1000'], // 로그인은 99%가 1초 미만
    errors: ['rate<0.01'],           // 1% 미만의 오류율
  },
};

// 테스트 실행 로직
export default function() {
  // 로그인 API 호출
  const loginRes = http.post('https://api.example.com/login', {
    username: 'testuser',
    password: 'password123'
  }, {
    tags: { name: 'login' }
  });
  
  // 응답 검증
  const loginSuccess = check(loginRes, {
    'login status is 200': (r) => r.status === 200,
    'has access token': (r) => r.json('token') !== '',
  });
  
  // 오류 추적
  if (!loginSuccess) {
    errorRate.add(1);
  } else {
    // 인증 토큰 추출
    const token = loginRes.json('token');
    
    // 인증된 API 호출
    const userProfileRes = http.get('https://api.example.com/user/profile', {
      headers: {
        'Authorization': `Bearer ${token}`,
      },
      tags: { name: 'profile' }
    });
    
    // 응답 검증
    check(userProfileRes, {
      'profile status is 200': (r) => r.status === 200,
      'profile has data': (r) => r.json('data') !== null,
    });
    
    // 제품 검색 API 호출
    const searchRes = http.get('https://api.example.com/products?query=mobile&limit=10', {
      headers: {
        'Authorization': `Bearer ${token}`,
      },
      tags: { name: 'search' }
    });
    
    // 응답 검증
    check(searchRes, {
      'search status is 200': (r) => r.status === 200,
      'products returned': (r) => r.json('items').length > 0,
    });
  }
  
  // 사용자 행동 시뮬레이션을 위한 지연
  sleep(Math.random() * 3 + 1); // 1-4초 사이의 랜덤 지연
}

이 스크립트는 다음과 같은 실제 사용자 흐름을 시뮬레이션한다:

  1. 로그인 API 호출
  2. 사용자 프로필 조회
  3. 제품 검색
  4. 각 API 응답 검증
  5. 사용자 행동 간 지연 시간 추가

JMeter를 사용한 부하 테스트 구현

JMeter는 강력한 GUI와 다양한 기능을 제공하는 부하 테스트 도구이다.

JMeter를 사용한 부하 테스트 구현 방법은 다음과 같다:

  1. 테스트 계획 생성:
    • JMeter 실행 및 테스트 계획 생성
    • 스레드 그룹 추가(가상 사용자)
    • HTTP 요청 기본 설정(도메인, 포트 등)
  2. HTTP 요청 구성:
    • 로그인 요청 추가
    • 세션 변수 추출(예: 토큰)
    • 추가 API 요청 구성
  3. 검증 및 모니터링 설정:
    • 응답 어설션 추가
    • 타이머 추가(지연 시간)
    • 결과 수집 리스너 추가
  4. 테스트 실행 및 분석:
    • 테스트 실행
    • 결과 분석 및 보고서 생성

JMeter 테스트 계획의 XML 구조 예시 (간략화):

 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
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0">
  <hashTree>
    <TestPlan guiclass="TestPlanGui" testname="API Load Test Plan">
      <ThreadGroup guiclass="ThreadGroupGui" testname="API Users">
        <stringProp name="ThreadGroup.num_threads">100</stringProp>
        <stringProp name="ThreadGroup.ramp_time">60</stringProp>
        <boolProp name="ThreadGroup.scheduler">true</boolProp>
        <stringProp name="ThreadGroup.duration">300</stringProp>
        
        <!-- HTTP 요청 기본 설정 -->
        <ConfigTestElement guiclass="HttpDefaultsGui" testname="HTTP Request Defaults">
          <stringProp name="HTTPSampler.domain">api.example.com</stringProp>
          <stringProp name="HTTPSampler.protocol">https</stringProp>
        </ConfigTestElement>
        
        <!-- 로그인 요청 -->
        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testname="Login Request">
          <stringProp name="HTTPSampler.path">/login</stringProp>
          <stringProp name="HTTPSampler.method">POST</stringProp>
          <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
          <elementProp name="HTTPsampler.Arguments">
            <collectionProp name="Arguments.arguments">
              <elementProp name="username">
                <stringProp name="Argument.value">testuser</stringProp>
              </elementProp>
              <elementProp name="password">
                <stringProp name="Argument.value">password123</stringProp>
              </elementProp>
            </collectionProp>
          </elementProp>
          
          <!-- JSON 응답에서 토큰 추출 -->
          <JSONPostProcessor guiclass="JSONPostProcessorGui" testname="Extract Token">
            <stringProp name="JSONPostProcessor.referenceNames">token</stringProp>
            <stringProp name="JSONPostProcessor.jsonPathExprs">$.token</stringProp>
          </JSONPostProcessor>
          
          <!-- 응답 검증 -->
          <ResponseAssertion guiclass="AssertionGui" testname="Check Status">
            <collectionProp name="Asserion.test_strings">
              <stringProp>200</stringProp>
            </collectionProp>
            <stringProp name="Assertion.test_field">Assertion.response_code</stringProp>
          </ResponseAssertion>
        </HTTPSamplerProxy>
        
        <!-- 사용자 프로필 요청 -->
        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testname="Get Profile">
          <stringProp name="HTTPSampler.path">/user/profile</stringProp>
          <stringProp name="HTTPSampler.method">GET</stringProp>
          
          <!-- 헤더 추가 -->
          <HeaderManager guiclass="HeaderPanel" testname="Auth Headers">
            <collectionProp name="HeaderManager.headers">
              <elementProp name="Authorization">
                <stringProp name="Header.value">Bearer ${token}</stringProp>
              </elementProp>
            </collectionProp>
          </HeaderManager>
        </HTTPSamplerProxy>
        
        <!-- 결과 수집 -->
        <ResultCollector guiclass="ViewResultsFullVisualizer" testname="Results Tree"/>
        <ResultCollector guiclass="SummaryReport" testname="Summary Report"/>
        <ResultCollector guiclass="GraphVisualizer" testname="Graph Results"/>
      </ThreadGroup>
    </TestPlan>
  </hashTree>
</jmeterTestPlan>

Postman/Newman을 사용한 간단한 부하 테스트

Postman은 종합적인 API 개발 및 테스트 환경을 제공하며, Newman을 통해 부하 테스트를 수행할 수 있다:

  1. Postman 컬렉션 생성:
    • API 요청 시퀀스 설정
    • 환경 변수 구성(URL, 인증 정보 등)
    • 테스트 스크립트 작성(응답 검증)
  2. Newman 스크립트 작성:
 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
#!/bin/bash

# 병렬 실행 수
PARALLEL=10

# 반복 횟수
ITERATIONS=100

# 시작 시간 기록
START=$(date +%s)

# 병렬로 Newman 실행
for i in $(seq 1 $PARALLEL); do
  newman run MyAPICollection.json -e TestEnvironment.json -n $ITERATIONS --silent &
done

# 모든 프로세스 완료 대기
wait

# 종료 시간 기록
END=$(date +%s)

# 결과 계산
DIFF=$(( $END - $START ))
TOTAL_REQUESTS=$(( $PARALLEL * $ITERATIONS * $(jq '.item | length' MyAPICollection.json) ))
RPS=$(echo "scale=2; $TOTAL_REQUESTS / $DIFF" | bc)

echo "병렬 프로세스: $PARALLEL"
echo "반복 횟수: $ITERATIONS"
echo "총 요청 수: $TOTAL_REQUESTS"
echo "총 소요 시간: ${DIFF}초"
echo "초당 요청 수(RPS): $RPS"

Postman/Newman은 전문적인 부하 테스트 도구에 비해 기능이 제한적이지만, 이미 Postman을 사용하고 있는 팀에서 간단한 부하 테스트를 수행하기에 적합하다.

API 부하 테스트 실행 및 모니터링

점진적 부하 접근법

부하 테스트를 효과적으로 실행하기 위한 점진적 접근법을 사용한다:

  1. 기준선 설정:
    • 최소 부하에서 API 성능 측정
    • 정상 상태의 응답 시간, 처리량, 자원 사용률 등 기록
    • 이후 테스트의 비교 기준으로 활용
  2. 점진적 부하 증가:
    • 낮은 부하에서 시작하여 점진적으로 증가
    • 각 단계마다 시스템 안정화 시간 부여
    • 비선형적 증가(10, 25, 50, 100, 200 사용자 등)
  3. 한계 지점 식별:
    • 성능 저하 시작 지점 식별
    • 병목 현상 발생 지점 모니터링
    • 오류 발생 임계값 확인
  4. 최적화 및 재테스트:
    • 식별된 문제점 해결
    • 최적화 후 동일한 조건에서 재테스트
    • 개선 사항 검증

실시간 모니터링 설정

부하 테스트 중 다양한 지표를 실시간으로 모니터링하는 방법:

  1. 인프라 모니터링:
    • CPU 사용률
    • 메모리 사용량
    • 디스크 I/O
    • 네트워크 트래픽
    • 도구: Prometheus, Grafana, CloudWatch, Datadog 등
  2. 애플리케이션 모니터링:
    • 응답 시간
    • 오류율
    • 처리량
    • 내부 메소드/함수 실행 시간
    • 도구: New Relic, AppDynamics, Dynatrace 등
  3. 데이터베이스 모니터링:
    • 쿼리 실행 시간
    • 연결 수
    • 캐시 효율성
    • 잠금 경합
    • 도구: PMM, pgAdmin, SQL Server Profiler 등
  4. 모니터링 대시보드 구성:

Grafana를 사용한 모니터링 대시보드 구성 예시:

 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
# Grafana 대시보드 구성 (JSON 형식의 일부)
{
  "dashboard": {
    "title": "API 부하 테스트 모니터링",
    "panels": [
      {
        "title": "API 응답 시간",
        "type": "graph",
        "datasource": "Prometheus",
        "targets": [
          {
            "expr": "histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket{service=\"api\"}[1m])) by (le))",
            "legendFormat": "p95"
          },
          {
            "expr": "histogram_quantile(0.50, sum(rate(http_request_duration_seconds_bucket{service=\"api\"}[1m])) by (le))",
            "legendFormat": "p50"
          }
        ]
      },
      {
        "title": "초당 요청 수",
        "type": "graph",
        "datasource": "Prometheus",
        "targets": [
          {
            "expr": "sum(rate(http_requests_total{service=\"api\"}[1m]))",
            "legendFormat": "RPS"
          }
        ]
      },
      {
        "title": "오류율",
        "type": "graph",
        "datasource": "Prometheus",
        "targets": [
          {
            "expr": "sum(rate(http_requests_total{service=\"api\", code=~\"5..\"}[1m])) / sum(rate(http_requests_total{service=\"api\"}[1m]))",
            "legendFormat": "Error Rate"
          }
        ]
      },
      {
        "title": "CPU 사용률",
        "type": "graph",
        "datasource": "Prometheus",
        "targets": [
          {
            "expr": "avg(rate(process_cpu_seconds_total{service=\"api\"}[1m]) * 100)",
            "legendFormat": "CPU %"
          }
        ]
      },
      {
        "title": "메모리 사용량",
        "type": "graph",
        "datasource": "Prometheus",
        "targets": [
          {
            "expr": "process_resident_memory_bytes{service=\"api\"} / 1024 / 1024",
            "legendFormat": "Memory (MB)"
          }
        ]
      }
    ]
  }
}

API 부하 테스트 결과 분석 및 최적화

주요 성능 지표 분석

부하 테스트 결과를 분석하기 위한 주요 성능 지표:

  1. 응답 시간 분석:
    • 평균 응답 시간: 전체 응답 시간의 평균
    • 응답 시간 백분위수: 50%(중앙값), 90%, 95%, 99% 등
    • 응답 시간 추세: 부하 증가에 따른 응답 시간 변화 패턴
  2. 처리량 분석:
    • 초당 요청 수(RPS): 초당 처리된 요청 수
    • 처리량 한계: 응답 시간 저하 없이 처리할 수 있는 최대 요청 수
    • 처리량 확장성: 리소스 추가에 따른 처리량 증가 패턴
  3. 오류율 분석:
    • 전체 오류율: 전체 요청 중 오류 비율
    • 오류 유형 분포: HTTP 상태 코드별 오류 분포
    • 오류 발생 패턴: 특정 부하 수준에서의 오류 증가 패턴
  4. 자원 사용률 분석:
    • CPU 사용률: 평균 및 최대 CPU 사용량
    • 메모리 사용량: 평균 및 최대 메모리 사용량
    • 네트워크 I/O: 전송 및 수신 데이터량
    • 디스크 I/O: 읽기/쓰기 작업량 및 지연 시간

병목 현상 식별

API 성능 병목 현상을 식별하는 방법:

  1. CPU 병목:
    • 증상: 높은 CPU 사용률(80-100%), 응답 시간 증가, 처리량 정체
    • 원인: 비효율적 알고리즘, 과도한 계산, 코드 최적화 부족
    • 진단 도구: 프로파일러, APM 도구, CPU 모니터링
  2. 메모리 병목:
    • 증상: 높은 메모리 사용량, 빈번한 GC(가비지 컬렉션), 성능 저하
    • 원인: 메모리 누수, 과도한 캐싱, 대용량 객체 생성
    • 진단 도구: 힙 덤프 분석, 메모리 프로파일러
  3. 데이터베이스 병목:
    • 증상: 높은 데이터베이스 응답 시간, 연결 풀 포화
    • 원인: 비효율적 쿼리, 부적절한 인덱스, 잠금 경합
    • 진단 도구: 쿼리 분석기, 데이터베이스 모니터링 도구
  4. 네트워크 병목:
    • 증상: 높은 네트워크 지연 시간, 낮은 처리량
    • 원인: 대용량 페이로드, 과도한 요청 수, 네트워크 인프라 제한
    • 진단 도구: 네트워크 분석기, 패킷 캡처 도구
  5. 외부 서비스 병목:
    • 증상: 외부 API 호출 지연, 타임아웃 증가
    • 원인: 외부 서비스 한계, 부적절한 통합 방식
    • 진단 도구: 분산 추적 시스템(Zipkin, Jaeger 등)

최적화 전략 및 권장 사항

식별된 병목 현상에 대한 최적화 전략:

  1. 애플리케이션 레벨 최적화:

    • 코드 최적화: 알고리즘 개선, 루프 최적화, 메모리 사용 효율화
    • 비동기 처리: 블로킹 작업의 비동기 처리로 전환
    • 캐싱 전략: 적절한 캐싱 도입 및 최적화
    • 병렬 처리: 작업의 병렬 처리 구현

    예시 코드 (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
    
    // 최적화 전 - 순차적 처리
    async function processRequest(req, res) {
      const userData = await getUserData(req.userId);
      const productData = await getProductData(req.productId);
      const recommendationData = await getRecommendations(userData, productData);
    
      res.json({
        user: userData,
        product: productData,
        recommendations: recommendationData
      });
    }
    
    // 최적화 후 - 병렬 처리
    async function processRequest(req, res) {
      // 독립적인 작업을 병렬로 실행
      const [userData, productData] = await Promise.all([
        getUserData(req.userId),
        getProductData(req.productId)
      ]);
    
      // 의존적인 작업은 순차적으로 실행
      const recommendationData = await getRecommendations(userData, productData);
    
      res.json({
        user: userData,
        product: productData,
        recommendations: recommendationData
      });
    }
    
  2. 데이터베이스 최적화:

    • 인덱스 최적화: 적절한 인덱스 추가 및 불필요한 인덱스 제거
    • 쿼리 튜닝: 비효율적인 쿼리 개선, 실행 계획 분석
    • 연결 풀 조정: 데이터베이스 연결 풀 크기 최적화
    • 데이터 모델링 개선: 필요에 따라 데이터 모델 수정

    예시 코드 (SQL - 쿼리 최적화):

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    -- 최적화 전
    SELECT o.*, c.name, c.email, p.name, p.price
    FROM orders o
    JOIN customers c ON o.customer_id = c.id
    JOIN products p ON o.product_id = p.id
    WHERE o.created_at > '2023-01-01';
    
    -- 최적화 후
    SELECT o.id, o.order_date, o.total_amount,
           c.name AS customer_name, c.email,
           p.name AS product_name, p.price
    FROM orders o
    JOIN customers c ON o.customer_id = c.id
    JOIN products p ON o.product_id = p.id
    WHERE o.created_at > '2023-01-01'
    LIMIT 100;
    
  3. 캐싱 전략 구현:

    • 응답 캐싱: 자주 요청되는 API 응답 캐싱
    • 데이터 캐싱: 자주 접근하는 데이터 캐싱
    • 분산 캐싱: Redis, Memcached 등을 활용한 분산 캐싱 구현
    • 캐시 무효화 전략: 적절한 캐시 만료 및 무효화 정책 수립

    예시 코드 (Node.js + Redis 캐싱):

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    const redis = require('redis');
    const client = redis.createClient();
    const { promisify } = require('util');
    const getAsync = promisify(client.get).bind(client);
    const setAsync = promisify(client.set).bind(client);
    
    async function getProductDetails(productId) {
      // 캐시에서 데이터 확인
      const cacheKey = `product:${productId}`;
      const cachedData = await getAsync(cacheKey);
    
      if (cachedData) {
        return JSON.parse(cachedData);
      }
    
      // 캐시에 없는 경우 데이터베이스에서 조회
      const product = await db.products.findById(productId);
    
      // 결과를 캐시에 저장 (60초 만료)
      await setAsync(cacheKey, JSON.stringify(product), 'EX', 60);
    
      return product;
    }
    
  4. 인프라 최적화:

    • 수평적 확장: 서버 인스턴스 추가를 통한 부하 분산
    • 수직적 확장: 서버 성능 향상(CPU, 메모리 증설)
    • 로드 밸런싱: 효율적인 부하 분산 전략 구현
    • CDN 활용: 정적 자원 제공을 위한 CDN 구성

    예시 (Docker Compose를 사용한 수평적 확장):

     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
    
    # docker-compose.yml
    version: '3'
    
    services:
      api:
        image: my-api:latest
        deploy:
          replicas: 5  # 인스턴스 수 증가
          resources:
            limits:
              cpus: '0.5'
              memory: 512M
        environment:
          - DB_HOST=db
          - REDIS_HOST=cache
    
      nginx:
        image: nginx:latest
        ports:
          - "80:80"
        volumes:
          - ./nginx.conf:/etc/nginx/nginx.conf
        depends_on:
          - api
    
      db:
        image: postgres:13
    
      cache:
        image: redis:6
    
  5. 아키텍처 수준 최적화:

    • 마이크로서비스 분할: 대규모 모놀리식 API를 작은 마이크로서비스로 분할
    • 비동기 통신: 적절한 상황에서 비동기 메시징으로 전환
    • 서킷 브레이커 패턴: 장애 확산 방지를 위한 서킷 브레이커 도입
    • 백프레셔 메커니즘: 과부하 상황에서의 요청 제어 메커니즘 구현

지속적 부하 테스트 자동화

CI/CD 파이프라인 통합

부하 테스트를 지속적 통합 및 배포 프로세스에 통합하는 방법:

  1. 파이프라인 구성:
    • 정기적 부하 테스트 스케줄링(일간, 주간)
    • 성능 회귀 테스트 자동화
    • 주요 기능 변경 시 부하 테스트 트리거
  2. 테스트 환경 자동화:
    • 인프라스트럭처 코드(IaC)를 통한 테스트 환경 프로비저닝
    • 컨테이너 및 오케스트레이션 도구를 활용한 환경 구성
    • 테스트 데이터 자동 생성 및 관리

GitHub Actions를 활용한 CI/CD 파이프라인 예시:

 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
# .github/workflows/load-test.yml
name: API Load Testing

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]
  schedule:
    - cron: '0 2 * * 1'  # 매주 월요일 02:00 UTC에 실행

jobs:
  load-test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v2
    
    - name: Set up Node.js
      uses: actions/setup-node@v2
      with:
        node-version: '16'
    
    - 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: Deploy test environment
      run: docker-compose -f docker-compose.test.yml up -d
    
    - name: Wait for services to start
      run: sleep 30
    
    - name: Run baseline load test
      run: k6 run --summary-export=baseline-results.json tests/performance/baseline.js
    
    - name: Run stress test
      run: k6 run --summary-export=stress-results.json tests/performance/stress.js
    
    - name: Run endurance test
      run: k6 run --summary-export=endurance-results.json tests/performance/endurance.js
    
    - name: Generate performance report
      run: node scripts/generate-performance-report.js
    
    - name: Archive test results
      uses: actions/upload-artifact@v2
      with:
        name: load-test-results
        path: |
          baseline-results.json
          stress-results.json
          endurance-results.json
          performance-report.html

지속적 성능 모니터링

지속적인 성능 모니터링을 통한 API 성능 관리 방법:

  1. 모니터링 시스템 구성:
    • 실시간 성능 모니터링 설정
    • 주요 성능 지표 대시보드 구성
    • 알림 임계값 설정
  2. 성능 추세 분석:
    • 시간에 따른 성능 변화 추적
    • 주기적인 성능 보고서 생성
    • 배포 전후 성능 비교
  3. 사용자 경험 모니터링:
    • 실제 사용자 모니터링(RUM)
    • 클라이언트 측 성능 지표 수집
    • 사용자 경험 지표(Core Web Vitals 등) 추적

Prometheus와 Grafana를 사용한 지속적 모니터링 구성 예시:

 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
# docker-compose 구성
version: '3'

services:
  prometheus:
    image: prom/prometheus
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090"
  
  grafana:
    image: grafana/grafana
    depends_on:
      - prometheus
    ports:
      - "3000:3000"
    volumes:
      - grafana-storage:/var/lib/grafana
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=secret
  
  node-exporter:
    image: prom/node-exporter
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    command:
      - '--path.procfs=/host/proc'
      - '--path.sysfs=/host/sys'
      - '--collector.filesystem.ignored-mount-points=^/(sys|proc|dev|host|etc)($$|/)'
    ports:
      - "9100:9100"

volumes:
  grafana-storage:

Prometheus 설정 예시:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# prometheus.yml
global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']

  - job_name: 'node'
    static_configs:
      - targets: ['node-exporter:9100']

  - job_name: 'api'
    metrics_path: '/metrics'
    static_configs:
      - targets: ['api:8080']

  # 앱 서비스가 Prometheus 메트릭을 노출한다고 가정
  - job_name: 'applications'
    metrics_path: '/actuator/prometheus'
    static_configs:
      - targets: ['app-service-1:8080', 'app-service-2:8080']

확장성 및 신뢰성 개선 전략

자동 확장(Auto-scaling) 구현

부하 변화에 대응하는 자동 확장 전략:

  1. 수평적 확장 자동화:
    • 클라우드 제공업체의 자동 확장 기능 활용(AWS Auto Scaling, Kubernetes HPA 등)
    • 모니터링 지표 기반 확장 정책 설정
    • 확장 임계값 및 쿨다운 기간 최적화
  2. 리소스 기반 확장 전략:
    • CPU 사용률 기반 확장
    • 메모리 사용량 기반 확장
    • 요청 대기열 길이 기반 확장
  3. 예측적 확장:
    • 과거 사용 패턴 분석을 통한 예측적 확장
    • 시간대별, 요일별, 계절별 트래픽 패턴 학습
    • 예정된 이벤트(마케팅 캠페인, 세일 등)에 대비한 사전 확장

AWS Auto Scaling 구성 예시:

 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
# CloudFormation 템플릿
Resources:
  ApiAutoScalingGroup:
    Type: AWS::AutoScaling::AutoScalingGroup
    Properties:
      LaunchConfigurationName: !Ref ApiLaunchConfig
      MinSize: 2
      MaxSize: 10
      DesiredCapacity: 2
      VPCZoneIdentifier: !Ref Subnets
      TargetGroupARNs:
        - !Ref ApiTargetGroup
      Tags:
        - Key: Name
          Value: API-Server
          PropagateAtLaunch: true
      # 가용 영역에 고르게 분산
      AvailabilityZones:
        - !Select [0, !GetAZs '']
        - !Select [1, !GetAZs '']
  
  ApiScalingPolicy:
    Type: AWS::AutoScaling::ScalingPolicy
    Properties:
      AutoScalingGroupName: !Ref ApiAutoScalingGroup
      PolicyType: TargetTrackingScaling
      TargetTrackingConfiguration:
        PredefinedMetricSpecification:
          PredefinedMetricType: ASGAverageCPUUtilization
        TargetValue: 70.0
        # 급격한 스케일 인/아웃 방지를 위한 쿨다운 기간
        ScaleInCooldown: 300
        ScaleOutCooldown: 60
  
  # 요청 대기열 길이 기반 추가 확장 정책
  ApiQueueLengthScalingPolicy:
    Type: AWS::AutoScaling::ScalingPolicy
    Properties:
      AutoScalingGroupName: !Ref ApiAutoScalingGroup
      PolicyType: TargetTrackingScaling
      TargetTrackingConfiguration:
        CustomizedMetricSpecification:
          MetricName: BacklogPerInstance
          Namespace: AWS/SQS
          Dimensions:
            - Name: QueueName
              Value: !GetAtt ApiQueue.QueueName
          Statistic: Average
        TargetValue: 100.0

탄력성(Resilience) 패턴 구현

API의 탄력성을 향상시키는 패턴들:

  1. 서킷 브레이커 패턴:

    • 장애가 발생한 서비스로의 호출을 차단하여 연쇄 장애 방지
    • 장애 서비스 자동 복구 지원
    • 대체 응답 또는 기능 제공

    예시 코드 (Node.js + Opossum):

     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
    
    const CircuitBreaker = require('opossum');
    
    // 데이터베이스 조회 함수
    async function getDatabaseData(query) {
      // 데이터베이스 호출 로직
    }
    
    // 서킷 브레이커 구성
    const breaker = new CircuitBreaker(getDatabaseData, {
      failureThreshold: 50,         // 50% 실패율에서 회로 개방
      resetTimeout: 10000,          // 10초 후 반개방 상태로 전환
      timeout: 3000,                // 3초 이상 소요되면 타임아웃
      errorThresholdPercentage: 50, // 50% 오류율에서 회로 개방
      rollingCountTimeout: 60000    // 통계 측정 기간 (60초)
    });
    
    // 대체 함수 등록
    breaker.fallback(() => {
      // 캐시에서 데이터 검색 또는 기본 응답 반환
      return { status: 'fallback', data: cachedData || defaultData };
    });
    
    // 이벤트 핸들러
    breaker.on('open', () => console.log('서킷 브레이커가 열렸습니다. 현재 호출이 차단됩니다.'));
    breaker.on('halfOpen', () => console.log('서킷 브레이커가 반개방 상태입니다. 테스트 호출이 진행됩니다.'));
    breaker.on('close', () => console.log('서킷 브레이커가 닫혔습니다. 정상적인 호출이 가능합니다.'));
    
    // API 엔드포인트에서 사용
    app.get('/api/products/:id', async (req, res) => {
      try {
        const result = await breaker.fire({ productId: req.params.id });
        res.json(result);
      } catch (err) {
        res.status(503).json({ error: '서비스를 일시적으로 사용할 수 없습니다.' });
      }
    });
    
  2. 타임아웃 및 재시도 패턴:

    • 응답 없는 서비스 호출에 대한 타임아웃 설정
    • 지수 백오프를 사용한 재시도 메커니즘
    • 재시도 횟수 및 간격 최적화

    예시 코드 (Java + Resilience4j):

     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
    
    // Resilience4j를 사용한 타임아웃 및 재시도 패턴
    import io.github.resilience4j.retry.Retry;
    import io.github.resilience4j.retry.RetryConfig;
    import io.github.resilience4j.retry.RetryRegistry;
    import io.github.resilience4j.timelimiter.TimeLimiter;
    import io.github.resilience4j.timelimiter.TimeLimiterConfig;
    
    import java.time.Duration;
    import java.util.concurrent.CompletableFuture;
    import java.util.concurrent.TimeoutException;
    import java.util.function.Supplier;
    
    public class ResilientApiClient {
    
        private final ProductService productService;
    
        // 타임리미터 구성
        private final TimeLimiter timeLimiter = TimeLimiter.of(TimeLimiterConfig.custom()
                .timeoutDuration(Duration.ofSeconds(2))
                .build());
    
        // 재시도 구성 - 지수 백오프 적용
        private final Retry retry = RetryRegistry.of(RetryConfig.custom()
                .maxAttempts(3)
                .waitDuration(Duration.ofMillis(500))
                .retryExceptions(TimeoutException.class, RuntimeException.class)
                .ignoreExceptions(IllegalArgumentException.class)
                .build()).retry("productService");
    
        public ResilientApiClient(ProductService productService) {
            this.productService = productService;
        }
    
        public Product getProductWithResilience(Long productId) {
            // 타임리미터와 재시도를 조합한 회복력 있는 호출
            Supplier<CompletableFuture<Product>> futureSupplier = () ->
                    CompletableFuture.supplyAsync(() -> productService.getProduct(productId));
    
            Supplier<Product> resilientSupplier = Retry.decorateSupplier(retry,
                    TimeLimiter.decorateSupplier(timeLimiter, futureSupplier));
    
            try {
                return resilientSupplier.get();
            } catch (Exception e) {
                // 실패 시 대체 응답 또는 예외 처리
                return getProductFromCache(productId);
            }
        }
    
        private Product getProductFromCache(Long productId) {
            // 캐시에서 제품 정보 조회하는 로직
            // ...
        }
    }
    
  3. 벌크헤드 패턴:

    • 리소스 풀을 격리하여 장애 확산 방지
    • 중요 서비스에 전용 리소스 할당
    • 서비스 우선순위 기반 리소스 할당

    예시 구성 (Spring Boot + Resilience4j):

     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
    
    // Bulkhead 패턴 구현
    @Configuration
    public class BulkheadConfig {
    
        @Bean
        public Customizer<Resilience4jBulkheadProvider> defaultBulkheadCustomizer() {
            return provider -> provider.configure(builder -> builder
                    .bulkheadConfig(BulkheadConfig.custom()
                            .maxConcurrentCalls(20)  // 동시 호출 제한
                            .maxWaitDuration(Duration.ofMillis(500))  // 대기 시간 제한
                            .build())
                    .bulkheadAspectOrder(1)
                    .build());
        }
    
        @Bean
        public Customizer<Resilience4jBulkheadProvider> criticalServiceBulkheadCustomizer() {
            return provider -> provider.configure(builder -> builder
                    .bulkheadConfig(BulkheadConfig.custom()
                            .maxConcurrentCalls(50)  // 중요 서비스에 더 많은 동시 호출 허용
                            .maxWaitDuration(Duration.ofMillis(1000))
                            .build(), "criticalService")
                    .build());
        }
    
        @Bean
        public Customizer<Resilience4jBulkheadProvider> lowPriorityBulkheadCustomizer() {
            return provider -> provider.configure(builder -> builder
                    .bulkheadConfig(BulkheadConfig.custom()
                            .maxConcurrentCalls(10)  // 낮은 우선순위 서비스는 제한적 리소스 할당
                            .maxWaitDuration(Duration.ofMillis(200))
                            .build(), "lowPriority")
                    .build());
        }
    }
    
    // 컨트롤러에서 사용
    @RestController
    public class ProductController {
    
        @Bulkhead(name = "criticalService")
        @GetMapping("/api/products/{id}")
        public Product getProduct(@PathVariable Long id) {
            // 제품 조회 로직
        }
    
        @Bulkhead(name = "lowPriority")
        @GetMapping("/api/products/recommendations")
        public List<Product> getRecommendations() {
            // 추천 제품 조회 로직
        }
    }
    
  4. 배압(Back Pressure) 메커니즘:

    • 과부하 상황에서 요청 흐름 제어
    • 요청 제한 또는 거부 전략
    • 요청 우선순위 부여

    예시 코드 (Node.js - express-rate-limit):

     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
    
    const rateLimit = require('express-rate-limit');
    const app = express();
    
    // 기본 API 요청 제한
    const apiLimiter = rateLimit({
      windowMs: 60 * 1000, // 1분
      max: 100, // IP당 최대 100 요청
      standardHeaders: true,
      legacyHeaders: false,
      message: { error: '너무 많은 요청을 보냈습니다. 잠시 후 다시 시도해주세요.' }
    });
    
    // 중요 API에 대한 요청 제한
    const criticalApiLimiter = rateLimit({
      windowMs: 60 * 1000, // 1분
      max: 20, // IP당 최대 20 요청
      standardHeaders: true,
      legacyHeaders: false,
      message: { error: '중요 API에 대한 요청 한도에 도달했습니다.' }
    });
    
    // 인증된 사용자를 위한 요청 제한
    const authenticatedLimiter = rateLimit({
      windowMs: 60 * 1000, // 1분
      max: 500, // 토큰당 최대 500 요청
      keyGenerator: (req) => req.user.id, // 사용자 ID 기반 제한
      standardHeaders: true,
      legacyHeaders: false
    });
    
    // 글로벌 API 제한 적용
    app.use('/api/', apiLimiter);
    
    // 특정 엔드포인트에 제한 적용
    app.use('/api/critical', criticalApiLimiter);
    
    // 인증된 사용자 라우트에 제한 적용
    app.use('/api/user', authenticateMiddleware, authenticatedLimiter);
    

로드 밸런싱 전략

효과적인 부하 분산을 위한 로드 밸런싱 전략:

  1. 알고리즘 선택:
    • 라운드 로빈: 순차적으로 요청 분산, 간단하고 일반적인 경우 효과적
    • 최소 연결: 현재 연결 수가 가장 적은 서버로 요청 전달, 처리 시간이 다양한 요청에 적합
    • 가중치 기반: 서버 용량에 따라 요청 분산, 이종 서버 환경에 적합
    • IP 해시: 클라이언트 IP를 기준으로 요청 라우팅, 세션 유지에 유용
  2. 동적 로드 밸런싱:
    • 서버 상태 모니터링 기반 라우팅
    • 성능 지표에 따른 동적 가중치 조정
    • 지연 시간 기반 로드 밸런싱
  3. 지역 기반 로드 밸런싱:
    • 지리적 근접성 기반 라우팅
    • 지역별 트래픽 관리
    • 글로벌 부하 분산

NGINX를 이용한 로드 밸런싱 구성 예시:

 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
# nginx.conf
http {
    upstream api_servers {
        least_conn;                   # 최소 연결 알고리즘
        server api1.example.com:8080 weight=5;  # 더 강력한 서버에 높은 가중치
        server api2.example.com:8080 weight=3;
        server api3.example.com:8080 weight=1 backup;  # 백업 서버
        
        keepalive 100;  # 연결 유지 풀 크기
    }
    
    # 읽기 전용 작업을 위한 별도 업스트림
    upstream read_api_servers {
        ip_hash;  # 클라이언트 IP 기반 라우팅
        server read1.example.com:8080;
        server read2.example.com:8080;
        server read3.example.com:8080;
    }
    
    # 쓰기 작업을 위한 별도 업스트림
    upstream write_api_servers {
        server write1.example.com:8080;
        server write2.example.com:8080;
    }
    
    server {
        listen 80;
        server_name api.example.com;
        
        # 상태 모니터링 엔드포인트
        location /status {
            stub_status on;
            allow 127.0.0.1;  # 내부 접근만 허용
            deny all;
        }
        
        # 읽기 요청 라우팅
        location ~ ^/api/(products|categories|users)/([0-9]+)$ {
            limit_req zone=api_reqs burst=10;  # 요청 제한
            proxy_pass http://read_api_servers;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_http_version 1.1;
            proxy_set_header Connection "";
            
            # 타임아웃 설정
            proxy_connect_timeout 2s;
            proxy_send_timeout 5s;
            proxy_read_timeout 5s;
        }
        
        # 쓰기 요청 라우팅
        location ~ ^/api/(products|categories|users)$ {
            limit_req zone=api_write_reqs burst=5 nodelay;  # 더 엄격한 제한
            proxy_pass http://write_api_servers;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_http_version 1.1;
            proxy_set_header Connection "";
            
            # 더 긴 타임아웃 (쓰기 작업은 시간이 더 걸릴 수 있음)
            proxy_connect_timeout 3s;
            proxy_send_timeout 10s;
            proxy_read_timeout 10s;
        }
        
        # 기본 요청 라우팅
        location / {
            proxy_pass http://api_servers;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_http_version 1.1;
            proxy_set_header Connection "";
        }
    }
}

고급 API 부하 테스트 시나리오

실제 사용자 행동 시뮬레이션

더 현실적인 부하 테스트를 위한 사용자 행동 시뮬레이션 방법:

  1. 사용자 패턴 분석:
    • 로그 분석을 통한 실제 사용자 행동 패턴 파악
    • 페이지 간 이동 경로 및 체류 시간 분석
    • 시간대별 사용 패턴 분석
  2. 사용자 여정(User Journey) 모델링:
    • 주요 사용자 여정 식별 및 매핑
    • 사용자 유형별 다양한 시나리오 작성
    • 사용자 행동의 확률적 모델링
  3. 현실적인 지연 시간 포함:
    • 사용자 사고 시간(Think Time) 추가
    • 자연스러운 타이핑 속도 및 지연 시간 구현
    • 네트워크 지연 및 변동성 시뮬레이션

k6를 사용한 사용자 행동 시뮬레이션 예제:

  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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
import http from 'k6/http';
import { sleep, check, group } from 'k6';
import { randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.1.0/index.js';
import { Trend, Rate, Counter } from 'k6/metrics';

// 사용자 정의 메트릭
const searchLatency = new Trend('search_latency');
const cartAddSuccess = new Rate('cart_add_success');
const orderCount = new Counter('order_count');

// 테스트 구성
export const options = {
  scenarios: {
    // 브라우저 사용자 시나리오
    browser_users: {
      executor: 'ramping-vus',  // 점진적으로 사용자 증가
      startVUs: 0,
      stages: [
        { duration: '2m', target: 100 },  // 2분 동안 100명까지 증가
        { duration: '5m', target: 100 },  // 5분 동안 100명 유지
        { duration: '2m', target: 0 },    // 2분 동안 0명으로 감소
      ],
      gracefulRampDown: '30s',
    },
    // 모바일 앱 사용자 시나리오
    mobile_app_users: {
      executor: 'constant-vus',  // 일정한 사용자 수
      vus: 50,
      duration: '10m',
      startTime: '30s',  // 30초 후에 시작
    }
  },
  thresholds: {
    'http_req_duration': ['p(95)<500'],
    'search_latency': ['p(95)<300'],
    'cart_add_success': ['rate>0.95'],
  },
};

// 브라우저 사용자 시뮬레이션
export function browserUsers() {
  let userJourney = Math.random();  // 사용자 여정 무작위 선택
  
  // 로그인 (모든 사용자 공통)
  group('Login', function() {
    let loginRes = http.post('https://api.example.com/login', {
      username: `user_${__VU}@example.com`,
      password: 'password123'
    });
    
    check(loginRes, {
      'login successful': (r) => r.status === 200 && r.json('token') !== '',
    });
    
    // 로그인 후 생각하는 시간
    sleep(randomIntBetween(2, 5));
  });
  
  // 상품 검색 및 브라우징 (80% 사용자)
  if (userJourney < 0.8) {
    group('Product Search', function() {
      // 검색어 배열에서 무작위 선택
      const searchTerms = ['smartphone', 'laptop', 'headphones', 'camera', 'watch'];
      const searchTerm = searchTerms[Math.floor(Math.random() * searchTerms.length)];
      
      let searchStart = new Date().getTime();
      let searchRes = http.get(`https://api.example.com/products?query=${searchTerm}&limit=20`);
      let searchEnd = new Date().getTime();
      
      // 검색 지연 시간 기록
      searchLatency.add(searchEnd - searchStart);
      
      check(searchRes, {
        'search successful': (r) => r.status === 200,
        'search has results': (r) => r.json('items').length > 0,
      });
      
      // 검색 결과 탐색 시간
      sleep(randomIntBetween(3, 8));
      
      // 상품 상세 페이지 조회 (검색 결과에서 무작위 선택)
      if (searchRes.json('items').length > 0) {
        const products = searchRes.json('items');
        const randomProduct = products[Math.floor(Math.random() * products.length)];
        
        let productRes = http.get(`https://api.example.com/products/${randomProduct.id}`);
        
        check(productRes, {
          'product details loaded': (r) => r.status === 200
        });
        
        // 상품 상세 페이지 탐색 시간
        sleep(randomIntBetween(5, 15));
        
        // 관련 상품 확인 (50% 확률)
        if (Math.random() < 0.5) {
          let relatedRes = http.get(`https://api.example.com/products/${randomProduct.id}/related`);
          
          check(relatedRes, {
            'related products loaded': (r) => r.status === 200
          });
          
          sleep(randomIntBetween(3, 7));
        }
        
        // 장바구니에 추가 (60% 확률)
        if (Math.random() < 0.6) {
          let cartRes = http.post('https://api.example.com/cart/items', {
            productId: randomProduct.id,
            quantity: Math.floor(Math.random() * 3) + 1
          });
          
          let cartSuccess = check(cartRes, {
            'added to cart successfully': (r) => r.status === 200 || r.status === 201
          });
          
          cartAddSuccess.add(cartSuccess);
          
          sleep(randomIntBetween(2, 5));
        }
      }
    });
  }
  
  // 장바구니 확인 및 주문 (30% 확률)
  if (Math.random() < 0.3) {
    group('Checkout Process', function() {
      // 장바구니 조회
      let cartRes = http.get('https://api.example.com/cart');
      
      check(cartRes, {
        'cart retrieved': (r) => r.status === 200
      });
      
      sleep(randomIntBetween(3, 8));
      
      // 장바구니에 상품이 있는 경우 결제 진행
      if (cartRes.json('items') && cartRes.json('items').length > 0) {
        // 배송 정보 입력
        let shippingRes = http.post('https://api.example.com/checkout/shipping', {
          address: '123 Test Street',
          city: 'Test City',
          zipCode: '12345',
          country: 'Test Country'
        });
        
        check(shippingRes, {
          'shipping info added': (r) => r.status === 200
        });
        
        sleep(randomIntBetween(5, 10));
        
        // 결제 정보 입력
        let paymentRes = http.post('https://api.example.com/checkout/payment', {
          cardType: 'visa',
          cardNumber: '4111111111111111',
          expiryMonth: '12',
          expiryYear: '2025',
          cvv: '123'
        });
        
        check(paymentRes, {
          'payment info added': (r) => r.status === 200
        });
        
        sleep(randomIntBetween(3, 7));
        
        // 주문 완료
        let orderRes = http.post('https://api.example.com/orders');
        
        let orderSuccess = check(orderRes, {
          'order placed successfully': (r) => r.status === 201
        });
        
        if (orderSuccess) {
          orderCount.add(1);
        }
        
        sleep(randomIntBetween(5, 10));
      }
    });
  }
  
  // 사용자 계정 정보 확인 (20% 확률)
  if (Math.random() < 0.2) {
    group('Account Management', function() {
      let profileRes = http.get('https://api.example.com/user/profile');
      
      check(profileRes, {
        'profile loaded': (r) => r.status === 200
      });
      
      sleep(randomIntBetween(5, 15));
      
      // 주문 이력 확인 (70% 확률)
      if (Math.random() < 0.7) {
        let ordersRes = http.get('https://api.example.com/user/orders');
        
        check(ordersRes, {
          'order history loaded': (r) => r.status === 200
        });
        
        sleep(randomIntBetween(3, 10));
      }
    });
  }
}

// 모바일 앱 사용자 시뮬레이션
export function mobileAppUsers() {
  // 모바일 앱 사용자에 맞는 다른 사용자 패턴 구현
  // ...
}

고급 부하 패턴 테스트

다양한 고급 부하 패턴을 통한 테스트 방법:

  1. 스파이크 테스트:
    • 짧은 시간 동안 급격한 부하 증가 시뮬레이션
    • 순간적인 트래픽 급증에 대한 대응력 검증
    • 회복 시간 측정
  2. 소사(Soak) 테스트:
    • 장시간 동안 지속적인 부하 유지
    • 메모리 누수 및 성능 저하 탐지
    • 장기간 안정성 검증
  3. 카오스 테스트:
    • 무작위 오류 및 장애 주입
    • 서비스 장애 시나리오 테스트
    • 복구 능력 검증

k6를 이용한 스파이크 테스트 예제:

 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
// k6 스파이크 테스트 예제
import http from 'k6/http';
import { sleep } from 'k6';

export const options = {
  scenarios: {
    spike: {
      executor: 'ramping-arrival-rate',
      startRate: 10,         // 초당 10개 요청으로 시작
      timeUnit: '1s',
      preAllocatedVUs: 100,  // 미리 할당된 VU
      maxVUs: 1000,          // 최대 가능 VU
      stages: [
        { duration: '1m', target: 10 },    // 1분 동안 초당 10개 요청 유지
        { duration: '30s', target: 500 },  // 30초 동안 초당 500개 요청으로 급증
        { duration: '2m', target: 500 },   // 2분 동안 초당 500개 요청 유지
        { duration: '30s', target: 10 },   // 30초 동안 초당 10개 요청으로 감소
        { duration: '2m', target: 10 },    // 2분 동안 초당 10개 요청 유지 (회복 시간)
      ],
    },
  },
  thresholds: {
    http_req_duration: ['p(95)<500', 'p(99)<1500'],
    http_req_failed: ['rate<0.1'],  // 10% 미만의 오류율
  },
};

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

API 부하 테스트의 최신 트렌드

클라우드 기반 부하 테스트

클라우드 환경을 활용한 부하 테스트의 장점과 구현 방법:

  1. 확장성: 대규모 부하 테스트를 위한 손쉬운 자원 확장
  2. 글로벌 테스트: 다양한 지역에서의 동시 테스트 가능
  3. 비용 효율성: 필요할 때만 자원 사용 및 비용 지불

클라우드 부하 테스트 서비스:

  • k6 Cloud
  • BlazeMeter
  • Loader.io
  • Gatling FrontLine
  • AWS Device Farm

AI 기반 성능 분석

인공지능과 기계학습을 활용한 성능 테스트 및 분석 방법:

  1. 이상 탐지:
    • 정상 패턴을 학습하여 비정상 동작 감지
    • 자동 경고 시스템 구현
    • 오류의 근본 원인 분석 지원
  2. 예측적 분석:
    • 과거 데이터 기반 성능 예측
    • 용량 계획 최적화
    • 예상 병목 지점 식별
  3. 자동 최적화:
    • 성능 데이터 기반 자동 설정 최적화
    • 자원 할당 자동 조정
    • 테스트 시나리오 자동 생성 및 조정

컨테이너 및 마이크로서비스 환경에서의 부하 테스트

현대적인 컨테이너 및 마이크로서비스 아키텍처에서의 부하 테스트 방법:

  1. 마이크로서비스 환경 테스트 전략:
    • 개별 서비스 및 전체 시스템 테스트 균형
    • 서비스 간 의존성 고려
    • 서비스 메시 환경에서의 테스트
  2. Kubernetes 환경에서의 부하 테스트:
    • 클러스터 내부 또는 외부에서의 테스트
    • 자원 사용량 모니터링
    • 자동 확장 정책 검증
  3. 서비스 메시 통합:
    • Istio, Linkerd 등 서비스 메시와 통합된 테스트
    • 트래픽 미러링을 통한 안전한 테스트
    • 장애 주입 및 회복력 테스트

용어 정리

용어설명

참고 및 출처