2차 부하테스트 진행 - 100-hours-a-week/4-bull4zo-wiki GitHub Wiki

MOA 부하 테스트 결과 보고서 (3-tier, V1, 유저 100명 기준)

☁️ 테스트 개요

항목 내용
대상 API 그룹 참여, 투표 생성, 참여, 결과 조회
사용자 수 100명 (JWT 토큰 100개 발급)
테스트 도구 k6
테스트 종류 Spike Test
목적 서비스의 SLO(SLI 기반) 달성 여부 검증 및 병목 탐지

☁️ 테스트 시나리오

Step 1: 그룹 가입 및 투표 생성

create_votes.js
import http from 'k6/http';
import { check } from 'k6';
import { SharedArray } from 'k6/data';

export const options = {
  vus: 100,
  iterations: 100,
};

const BASE_URL = 'BE 주소';
const JWT_TOKENS = new SharedArray('jwt_tokens', () => JSON.parse(open('./tokens.json')).tokens);

export default function () {
  const userId = __VU - 1;
  const token = JWT_TOKENS[userId].token;
  const authHeader = {
  headers: {
    Authorization: `Bearer ${token}`,
  }
};
  
  const joinRes = http.post(`${BASE_URL}/api/v1/groups`, JSON.stringify({ inviteCode: "" }), authHeader);
  check(joinRes, {
    '그룹 가입 성공': (r) => r.status === 000 || r.status === 000,
  });

  const payload = JSON.stringify({
    groupId: 27,
    content: `created by VU${__VU}`,
    anonymous: false,
  });

  const res = http.post(`${BASE_URL}/api/v1/votes/$`, payload, authHeader);

  const success = check(res, {
    [`VU${__VU} 투표 생성 성공`]: (r) => r.status === 201,
  });

  if (!success) {
    console.log(`VU${__VU} vote 생성 실패: ${res.status}, body: ${res.body}`);
  } else {
    const voteId = res.json('data').voteId;
    if (voteId) {
      console.log(`{"voteId": ${voteId}}`);
    }
  }
}
  1. 모든 유저 동시에 초대코드로 그룹에 가입
  2. 모든 유저 동시에 동일한 그룹에 투표 1건 등록
  3. 성공 시 생성된 voteId를 로그로 출력, 저장

총 Throuput: 200

Step 2: 투표 참여 및 결과 조회

submit_votes.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { SharedArray } from 'k6/data';

export const options = {
  vus: 100,
  iterations: 100,
};

const BASE_URL = '';
const JWT_TOKENS = new SharedArray('jwt_tokens', () => JSON.parse(open('./tokens.json')).tokens);
const VOTE_IDS = new SharedArray('vote_ids', () => JSON.parse(open('./vote_ids.json'))); // [{voteId: 123}, ...]

export default function () {
  const userId = __VU - 1;
  const token = JWT_TOKENS[userId].token;
    const authHeader = {
  headers: {
    Authorization: `Bearer ${token}`,
  }
};

  for (const v of VOTE_IDS) {
    const id = v.voteId;
    const submitRes = http.post(`${BASE_URL}/api/v1/votes/$`, JSON.stringify({ userResponse: "" }), authHeader);
    const success = check(submitRes, {
      [`VU${__VU} → ${id} 제출 성공`]: (r) => r.status === 000 || r.status === 000,
    });
    
    if (!success) {
      console.log(`VU${__VU} → ${id} 제출 실패: ${submitRes.status}, body: ${submitRes.body}`);
    }
    
    sleep(0.1);
  }
}
result_check.js
import http from 'k6/http';
import { SharedArray } from 'k6/data';

const BASE_URL = '';

const JWT_TOKENS = new SharedArray('jwt_tokens', () =>
  JSON.parse(open('./tokens.json')).tokens
);

const VOTE_IDS = new SharedArray('vote_ids', () =>
  JSON.parse(open('./vote_ids.json'))
);

export default function () {
  const userId = __VU - 1;
  const token = JWT_TOKENS[userId].token;

  const authHeader = {
    headers: {
      Authorization: `Bearer ${token}`,
    },
  };

  for (const v of VOTE_IDS) {
    const id = v.voteId;
    const res = http.get(`${BASE_URL}/api/v1/votes/$`, authHeader);

    // Optional chaining 없이 방어적 접근
    let count = 'N/A';
    if (
      res &&
      res.json() &&
      res.json().data &&
      Array.isArray(res.json().data.results) &&
      res.json().data.results.length > 0
    ) {
      count = res.json().data.results[0].count;
    }

    console.log(`voteId ${id} → 찬성 : ${count}`);
  }
}
  1. 모든 유저 동시에 전체 voteId에 대해 순차적으로 투표 참여
  2. 한 명의 유저 전체 voteId에 대해 순차적으로 결과 조회
  3. 결과 응답 구조가 정상일 경우, 찬성 투표 수 로그 출력 & 저장

총 Throuput: 10,100

Step 3: 종료된 투표 결과 조회

result_check.js
import http from 'k6/http';
import { SharedArray } from 'k6/data';

const BASE_URL = '';

const JWT_TOKENS = new SharedArray('jwt_tokens', () =>
  JSON.parse(open('./tokens.json')).tokens
);

const VOTE_IDS = new SharedArray('vote_ids', () =>
  JSON.parse(open('./vote_ids.json'))
);

export default function () {
  const userId = __VU - 1;
  const token = JWT_TOKENS[userId].token;

  const authHeader = {
    headers: {
      Authorization: `Bearer ${token}`,
    },
  };

  for (const v of VOTE_IDS) {
    const id = v.voteId;
    const res = http.get(`${BASE_URL}/api/v1/votes/$`, authHeader);

    // Optional chaining 없이 방어적 접근
    let count = 'N/A';
    if (
      res &&
      res.json() &&
      res.json().data &&
      Array.isArray(res.json().data.results) &&
      res.json().data.results.length > 0
    ) {
      count = res.json().data.results[0].count;
    }

    console.log(`voteId ${id} → 찬성 : ${count}`);
  }
}
  1. 한 명의 유저가 전체 voteId에 대해 순차적으로 결과 조회
  2. 결과 응답 구조가 정상일 경우, 찬성 투표 수 로그 출력 & 저장

☁️ SLO 설정 및 근거

최근 7일 기준 GA 주요 지표

image

총 사용자 수: 73명 총 이벤트 수: 6,900건 1인당 평균 이벤트 수: 약 94.5건 하루 최대 이용자 수: 32명

  • 아직 사용자 수는 적지만, 이벤트 단위 사용량은 높은 편
  • 이벤트성 서비스 특정상 트래픽 폭증 대비 필요

최근 7일 기준 BE APM 주요 지표

평균 지표

image

P95 지표

image

P99 지표

image

API 이름 평균 Latency 설명
TestController#ping 1.95ms 헬스체크용 API (가장 일반적)
AuthController#refreshAccessToken 35ms 토큰 갱신용 (부하 적음)
AuthController#authLogin 215ms 로그인 (부하 낮은 편)
UserController#getUserInfo 158ms 사용자 정보 조회
ImageController#getSignedUrl 735ms 업로드용 URL 발급 (V2 기능, 테스트 제외)
UserController#getJoinedGroups 47ms 마이페이지 진입 등에서 자주 호출
OpenApiController#getSwaggerJson 33ms Swagger 문서용 (테스트에선 제외)
API 이름 역할 P99 Latency Failed Rate 코멘트
VoteController#createVote 투표 등록 2,794ms ❗ 4.1% ❗ 가장 느리고 실패율 높음
VoteController#submitVote 투표 제출 2,175ms 3.9% 등록 후 단계
VoteController#getMyVotes 내 투표 조회 2,869ms 1.1% 마이페이지 진입 등
VoteController#getVoteResult 투표 결과 2,246ms 1.1% 종료된 투표 결과 확인
VoteController#getAllVoteTitles 투표 리스트 555ms 1.1% 리스트 불러오기, 검색 가능성
  • 일반 API 평균 응답 속도는 양호하며 대부분 < 150ms 수준으로 SLO 기준을 만족
  • 그러나 핵심 비즈니스 트랜잭션인 createVote, submitVote는 P99 latency가 2.8초 내외이며, 실패율 또한 4%에 달함
  • 마이페이지 진입 관련 API들(getMyVotes, getJoinedGroups)도 상대적으로 느린 편이므로, 유저 경험 개선을 위해 병목 지점 분석이 필요

서비스 SLO 요약 (v1.4.0 기준)

응답 시간 SLO

지표 SLO 목표 현재 상태 / 설명
P99 latency (전체) ≤ 1.5초 현재 2.8초 → 튜닝 필요
P95 latency (핵심 API) ≤ 800ms 비즈니스 응답 기준
평균 latency (일반 API) ≤ 150ms 현재 만족

안정성 SLO

지표 SLO 목표 참고
전체 실패율 < 1% 현재 4.1% → 장애 원인 점검 필요
주요 트랜잭션 실패율 < 2% createVote, submitVote 대상

처리량/확장성 SLO

지표 SLO 목표 설명
최대 대응 RPS (Spike) ≥ 50 RPS 이벤트성 서비스 기준

☁️ 주요 테스트 결과

투표 생성 (create_votes.js)

지표 수치
요청 수 200
평균 응답 시간 1.33초
P95 응답 시간 2.68초
실패율 0%
CPU 사용률 증가 없음 (평이함)
  • Redis/MySQL 병목 없음. TLS handshaking + Wait time이 전체 시간 중 큰 비중 차지

투표 참여 + 결과 조회 (submit_votes.js + result_check.js)

지표 수치
요청 수 10,100
평균 응답 시간 2.1초
P95 응답 시간 4.3초
실패율 0%
CPU 사용률 100% 도달
  • submitVote, getVoteResult에서 병목 발생. DB 접근량 증가에 따라 app:DB:Redis 비중이 72:24:4로 분산됨

결과 재조회 (result_check.js)

시점 데이터 위치 평균 응답시간
투표 직후 Redis 208ms
종료 이후 MySQL 216ms
  • Redis 캐시가 약간 더 빠르나, DB 접근도 무리 없음

부하 전후 성능 비교

항목 부하 전 부하 후 변화
submitVote 209ms 1,869ms 🔺 약 9배 증가
getVoteResult 94ms 805ms 🔺 약 8.5배 증가
실패율 0% 0% 유지
Redis 비중 0% 4.4% 증가
MySQL 비중 0% 24% 증가

(사진 추가 및 보완 예정)

⚠️ **GitHub.com Fallback** ⚠️