MOA 부하 테스트 결과 보고서 (3-tier, V1, 유저 100명 기준)
항목 |
내용 |
대상 API |
그룹 참여, 투표 생성, 참여, 결과 조회 |
사용자 수 |
100명 (JWT 토큰 100개 발급) |
테스트 도구 |
k6 |
테스트 종류 |
Spike Test |
목적 |
서비스의 SLO(SLI 기반) 달성 여부 검증 및 병목 탐지 |
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건 등록
- 성공 시 생성된 voteId를 로그로 출력, 저장
총 Throuput: 200
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}`);
}
}
- 모든 유저 동시에 전체 voteId에 대해 순차적으로 투표 참여
- 한 명의 유저 전체 voteId에 대해 순차적으로 결과 조회
- 결과 응답 구조가 정상일 경우, 찬성 투표 수 로그 출력 & 저장
총 Throuput: 10,100
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}`);
}
}
- 한 명의 유저가 전체 voteId에 대해 순차적으로 결과 조회
- 결과 응답 구조가 정상일 경우, 찬성 투표 수 로그 출력 & 저장

총 사용자 수: 73명
총 이벤트 수: 6,900건
1인당 평균 이벤트 수: 약 94.5건
하루 최대 이용자 수: 32명
- 아직 사용자 수는 적지만, 이벤트 단위 사용량은 높은 편
- 이벤트성 서비스 특정상 트래픽 폭증 대비 필요
평균 지표

P95 지표

P99 지표

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 목표 |
현재 상태 / 설명 |
P99 latency (전체) |
≤ 1.5초 |
현재 2.8초 → 튜닝 필요 |
P95 latency (핵심 API) |
≤ 800ms |
비즈니스 응답 기준 |
평균 latency (일반 API) |
≤ 150ms |
현재 만족 |
지표 |
SLO 목표 |
참고 |
전체 실패율 |
< 1% |
현재 4.1% → 장애 원인 점검 필요 |
주요 트랜잭션 실패율 |
< 2% |
createVote , submitVote 대상 |
지표 |
SLO 목표 |
설명 |
최대 대응 RPS (Spike) |
≥ 50 RPS |
이벤트성 서비스 기준 |
지표 |
수치 |
요청 수 |
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로 분산됨
시점 |
데이터 위치 |
평균 응답시간 |
투표 직후 |
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% |
증가 |
(사진 추가 및 보완 예정)