부하테스트 시나리오 3: 채널방 – 메세지 보내기 - 100-hours-a-week/2-hertz-wiki GitHub Wiki
시나리오 설명: 사용자가 개인채널에 들어가서 상대방에게 메세지를 입력하여 전송
사용자가 메세지 입력란을 통해 메세지를 입력하여 전송 버튼을 클릭. 버튼이 클릭되면 /api/v1/channel-rooms/{channelRoomId}/messages
API를 호출하며, 백엔드에서는 전송된 메세지를 DB에 저장합니다.
항목 | 내용 |
---|---|
테스트 목적 | 사용자가 상대방과 대화를 나누는 과정에서의 성능 및 부하 검증 |
테스트 환경 | - Monolithic: e2-custom-2-6144 (vCPU 2개, 메모리 4GB) |
대상 시스템 | 백엔드(SpringBoot + MySQL) |
테스트 도구 | k6 |
테스트 일시 | 2025년 5월 22일 |
사용자가 시그널을 받은 상대방과 대화를 나누는 과정에서의 성능 및 부하를 검증하기 위함입니다.
어플리케이션 | API 엔드포인트 | 메서드 | 설명 |
---|---|---|---|
SpringBoot | /api/v1/channel-rooms/{channelRoomId}/messages | POST | 메세지 내용 포함 |
차수 | 1차 | 2차 | 3차 |
---|---|---|---|
동시 사용자 수 | 100명 | 500명 | 10 |
총 요청 수 | 500회 | 1500회 | 1000회 |
요청 간격 | 1초 | 1초 | 1초 |
부하 생성 패턴 | shared-iterations (고정 개수 요청 배분) | shared-iterations (고정 개수 요청 배분) | shared-iterations (고정 개수 요청 배분) |
배치 처리 방식 | 멀티스레드 활용, 동시 요청 처리 | 멀티스레드 활용, 동시 요청 처리 | 멀티스레드 활용, 동시 요청 처리 |
🔹 1차 테스트 결과
K6 요약
항목 | 값 | 정의 |
---|---|---|
최대 VU 수 (vus_max) | 100 | 동시에 실행된 최대 가상 사용자 수 |
총 요청 수 (http_reqs) | 500 | 전체 테스트 동안 실행된 HTTP 요청 수 |
평균 응답 시간 (avg) | 1.4s | 전체 요청의 평균 응답 시간 |
최대 응답 시간 (max) | 4.71s | 가장 느린 요청의 응답 시간 |
p90 응답 시간 | 2.42s | 90% 요청이 이 시간 이하로 응답됨 |
p95 응답 시간 | 2.81s | 95% 요청이 이 시간 이하로 응답됨 |
에러율 | 0.00% | 실패한 요청 비율 |
성공률 | 100.00% | 응답코드 201 또는 410 만족 비율 |
요청 완료 시간 | 2.43s | 1회 요청당 평균 수행 시간 |
총 테스트 시간 | 약 13.2초 | 전체 테스트 소요 시간 |
전체 리소스 사용량


어플리케이션별 리소스 사용량


APM 결과(API 지연 시간, 호출 수, 에러율)

Name | P50 (in ms) | P95 (in ms) | P99 (in ms) | Number of calls | Error Rate (%) |
---|---|---|---|---|---|
POST /api/v1/channel-rooms/{channelRoomId}/messages | 1282.42 | 2746.14 | 3898.25 | 500 | 0.00 |
GET /api/v1/channel-rooms/{channelRoomId} | 254.23 | 1990.61 | 2407.98 | 11 | 0.00 |
GET /api/v1/channel | 18.45 | 1861.76 | 2187.09 | 18 | 0.00 |
SignalMessageRepository.countMessagesBySenderInRoom | 12.58 | 1065.23 | 2097.50 | 500 | 0.00 |
SignalRoomRepository.findChannelRoomsWithPartnerAndLastMessage | 8.05 | 1705.17 | 1991.99 | 18 | 0.00 |
GET /api/v1/tuning | 1174.41 | 1874.23 | 1881.44 | 4 | 0.00 |
SELECT com.hertz.hertz_be.domain.channel.entity.SignalMessage | 12.91 | 58.22 | 1808.34 | 1022 | 0.00 |
GET | 316.65 | 387.06 | 395.30 | 4 | 0.00 |
SignalMessageRepository.findBySignalRoom_Id | 69.16 | 204.51 | 219.18 | 11 | 0.00 |
UserRepository.findByIdAndDeletedAtIsNull | 53.66 | 109.54 | 133.74 | 511 | 0.00 |
SELECT | 19.28 | 61.53 | 119.29 | 560 | 0.00 |
SELECT com.hertz.hertz_be.domain.user.entity.User | 45.11 | 94.50 | 114.45 | 511 | 0.00 |
UserRepository.findById | 34.76 | 76.24 | 100.62 | 508 | 0.00 |
Session.find com.hertz.hertz_be.domain.user.entity.User | 32.33 | 72.29 | 97.55 | 508 | 0.00 |
UserRepository.findByIdWithSentSignalRooms | 23.31 | 67.12 | 96.14 | 511 | 0.00 |
SignalMessageRepository.save | 27.21 | 61.17 | 79.47 | 500 | 0.00 |
Session.persist com.hertz.hertz_be.domain.channel.entity.SignalMessage | 24.99 | 56.66 | 78.52 | 500 | 0.00 |
SignalRoomRepository.findById | 15.03 | 44.35 | 68.23 | 511 | 0.00 |
Session.find com.hertz.hertz_be.domain.channel.entity.SignalRoom | 13.86 | 42.91 | 66.66 | 511 | 0.00 |
SignalMessageRepository.existsBySignalRoomInAndSenderUserNotAndIsReadFalse | 17.10 | 45.45 | 65.53 | 511 | 0.00 |
SignalMessageRepository.findRoomsWithLastSender | 17.97 | 54.22 | 56.29 | 11 | 0.00 |
Transaction.commit | 13.09 | 38.88 | 51.71 | 1022 | 0.00 |
SignalMessageRepository.findById | 6.79 | 27.59 | 38.77 | 500 | 0.00 |
Session.find com.hertz.hertz_be.domain.channel.entity.SignalMessage | 6.46 | 25.79 | 38.61 | 500 | 0.00 |
INSERT tuningdb.signal_message | 8.28 | 27.68 | 36.59 | 500 | 0.00 |
SELECT tuningdb.user | 5.23 | 22.77 | 35.69 | 1519 | 0.00 |
UserInterestsRepository.existsByUser | 14.61 | 30.37 | 31.95 | 4 | 0.00 |
SignalMessageRepository.findBySignalRoomIdAndSenderUserIdOrderBySendAtAsc | 9.10 | 23.14 | 30.93 | 11 | 0.00 |
SELECT tuningdb.signal_room | 4.76 | 16.63 | 28.05 | 1522 | 0.00 |
SELECT tuningdb | 4.49 | 17.13 | 27.02 | 2566 | 0.00 |
TuningRepository.findByUserAndCategory | 16.03 | 22.20 | 22.76 | 4 | 0.00 |
SELECT tuningdb.signal_message | 4.74 | 14.67 | 21.73 | 1542 | 0.00 |
SELECT com.hertz.hertz_be.domain.channel.entity.Tuning | 12.21 | 20.32 | 21.33 | 4 | 0.00 |
SELECT com.hertz.hertz_be.domain.interests.entity.UserInterests | 14.10 | 18.65 | 18.65 | 4 | 0.00 |
TuningRepository.save | 7.62 | 15.91 | 16.90 | 4 | 0.00 |
Session.persist com.hertz.hertz_be.domain.channel.entity.Tuning | 5.48 | 15.35 | 16.65 | 4 | 0.00 |
TuningResultRepository.existsByTuning | 9.77 | 12.63 | 12.90 | 4 | 0.00 |
SELECT tuningdb.user_interests | 6.48 | 10.09 | 10.28 | 4 | 0.00 |
SELECT com.hertz.hertz_be.domain.channel.entity.TuningResult | 7.37 | 9.93 | 10.16 | 4 | 0.00 |
SELECT tuningdb.tuning_result | 3.98 | 5.11 | 5.23 | 4 | 0.00 |
SELECT tuningdb.tuning | 2.25 | 3.60 | 3.68 | 4 | 0.00 |
INSERT tuningdb.tuning | 2.72 | 3.46 | 3.47 | 4 | 0.00 |
SseService.sendPeriodicPings | 0.12 | 0.14 | 0.14 | 6 | 0.00 |
🔹 2차 테스트 결과
K6 요약
항목 | 값 | 정의 |
---|---|---|
최대 VU 수 (vus_max) | 500 | 동시에 실행된 최대 가상 사용자 수 |
총 요청 수 (http_reqs) | 1500 | 전체 테스트 동안 실행된 HTTP 요청 수 |
평균 응답 시간 (avg) | 7.4s | 전체 요청의 평균 응답 시간 |
최대 응답 시간 (max) | 22.52s | 가장 느린 요청의 응답 시간 |
p90 응답 시간 | 11.12s | 90% 요청이 이 시간 이하로 응답됨 |
p95 응답 시간 | 11.75s | 95% 요청이 이 시간 이하로 응답됨 |
에러율 | 0.00% | 실패한 요청 비율 |
성공률 | 100.00% | 응답코드 201 또는 410 만족 비율 |
요청 완료 시간 | 8.55s | 1회 요청당 평균 수행 시간 |
총 테스트 시간 | 약 30.4초 | 전체 테스트 소요 시간 |
❗ 서버 리소스 부족으로 다운 발생
전체 리소스 사용량


어플리케이션별 리소스 사용량


🔹 3차 테스트 결과
K6 요약
항목 | 값 | 정의 |
---|---|---|
최대 VU 수 (vus_max) | 10 | 동시에 실행된 최대 가상 사용자 수 |
총 요청 수 (http_reqs) | 1000 | 전체 테스트 동안 실행된 HTTP 요청 수 |
평균 응답 시간 (avg) | 24.93ms | 전체 요청의 평균 응답 시간 |
최대 응답 시간 (max) | 107.31ms | 가장 느린 요청의 응답 시간 |
p90 응답 시간 | 33.46ms | 90% 요청이 이 시간 이하로 응답됨 |
p95 응답 시간 | 41.78ms | 95% 요청이 이 시간 이하로 응답됨 |
에러율 | 0.00% | 실패한 요청 비율 |
성공률 | 100.00% | 응답코드 201 또는 410 만족 비율 |
요청 완료 시간 | 약 102초 | 전체 테스트 소요 시간 |
전체 리소스 사용량


어플리케이션별 리소스 사용량


APM 결과(API 지연 시간, 호출 수, 에러율)
Name | P50 (in ms) | P95 (in ms) | P99 (in ms) | Number of calls | Error Rate (%) |
---|---|---|---|---|---|
POST /api/v1/channel-rooms/{channelRoomId}/messages | 20.28 | 39.26 | 58.84 | 1000 | 0.00 |
GET /api/v1/channel-rooms/{channelRoomId} | 34.04 | 45.99 | 46.98 | 6 | 0.00 |
SignalMessageRepository.markAllMessagesAsReadByRoomId | 5.35 | 11.69 | 13.02 | 6 | 0.00 |
UserRepository.findByIdAndDeletedAtIsNull | 3.09 | 7.65 | 12.41 | 1006 | 0.00 |
UPDATE com.hertz.hertz_be.domain.channel.entity.SignalMessage | 4.61 | 10.37 | 11.74 | 6 | 0.00 |
Transaction.commit | 5.16 | 8.58 | 11.58 | 1006 | 0.00 |
SELECT com.hertz.hertz_be.domain.user.entity.User | 2.68 | 6.94 | 11.16 | 1006 | 0.00 |
UPDATE tuningdb.signal_message | 2.80 | 9.06 | 10.35 | 6 | 0.00 |
SignalMessageRepository.findBySignalRoom_Id | 7.56 | 8.31 | 8.47 | 6 | 0.00 |
UserRepository.findById | 2.16 | 4.94 | 8.01 | 1000 | 0.00 |
Session.find com.hertz.hertz_be.domain.user.entity.User | 2.06 | 4.73 | 7.47 | 1000 | 0.00 |
SignalMessageRepository.save | 1.34 | 3.69 | 6.14 | 1000 | 0.00 |
Session.persist com.hertz.hertz_be.domain.channel.entity.SignalMessage | 1.24 | 3.58 | 5.91 | 1000 | 0.00 |
INSERT tuningdb.signal_message | 0.64 | 2.03 | 4.14 | 1000 | 0.00 |
SELECT | 2.36 | 3.91 | 3.95 | 18 | 0.00 |
SignalRoomRepository.findById | 1.26 | 2.54 | 3.74 | 1006 | 0.00 |
Session.find com.hertz.hertz_be.domain.channel.entity.SignalRoom | 1.14 | 2.29 | 3.39 | 1006 | 0.00 |
SELECT tuningdb | 0.61 | 1.69 | 3.39 | 2012 | 0.00 |
SELECT tuningdb.user | 0.59 | 1.68 | 3.37 | 2006 | 0.00 |
SignalMessageRepository.findRoomsWithLastSender | 1.81 | 3.00 | 3.03 | 6 | 0.00 |
GET / | 2.71 | 2.71 | 2.71 | 1 | 0.00 |
SELECT tuningdb.signal_room | 0.66 | 1.47 | 2.62 | 1006 | 0.00 |
SELECT tuningdb.signal_message | 1.74 | 2.24 | 2.25 | 12 | 0.00 |
- 동시 요청 100건에서는 평균 응답 시간 1.4초, 최대 4.71초로 나타났으며,
- 동시 요청 500건에서는 서버가 일시적으로 중단되는 현상이 발생했으며, 평균 7.4초, 최대 22.52초로 매우 높은 응답 시간이 기록됨.
- 반면 동시 요청 10건의 경우, 총 1000건의 요청에도 불구하고 평균 24.93ms, 최대 107.31ms로 매우 빠르고 안정적인 응답을 보여줌.
- 또한 Trace 결과에서는 API 요청 처리와 DB 작업 처리 사이에 약 6초 가량의 공백이 존재하는 구간이 확인됨.
- 이는 DB 락 대기, 비동기 대기 등과 같은 애플리케이션 내부 지연 가능성이 있으며, 사용자 경험에 심각한 영향을 줄 수 있는 지점임.
- 100명 동시 요청 시, CPU 사용률이 90%에 육박하고, 메모리 역시 4GB 전체를 소진하는 등 자원 사용률이 매우 높음.
- 500명 동시 요청 시, CPU와 메모리 사용량이 급증하면서 시스템이 응답 불가 상태에 도달함.
- 반면 10명 동시 요청 시에는 CPU 사용률이 일시적으로 증가하더라도 전반적으로 안정적인 사용률과 빠른 응답 시간을 유지함.
- 모든 테스트에서 HTTP 요청 실패율은 0%, 기능적 오류 없이 정상 응답이 반환되었음.
- 응답 지연은 요청 수 증가에 따른 자원 부족 및 처리 병목으로 판단됨.
- Trace 상 비정상적으로 긴 처리 공백 시간 또한 리소스 부족으로 인한, 메모리 부족, 스레드 부족 등의 문제를 일으켰을 가능성이 높음
- 현재 서버 자원으로는 100건 이상의 동시 요청을 안정적으로 처리하기 어려우며, 인프라 확장 또는 구조적 최적화가 필요하다.
- 현재 도커 컨테이너 환경으로의 마이그레이션을 통해 nextjs, springboot, mysql을 각 각의 인스턴스로 분리 예정이므로, 마이그레이션 전에는 사용자 수 100명에 맞춰 인스턴스 리소스를 CPU 2코어, RAM 8GB까지 늘리는 것을 제안한다.
- 테스트 스크립트:
/home/devops/stress-testing/send-message-test.js
테스트 스크립트
import http from 'k6/http';
import { check, sleep } from 'k6';
export let options = {
vus: 10, // 동시에 실행할 가상 사용자 수
iterations: 1000, // 총 요청 수
};
const BASE_URL = 'https://dev.hertz-tuning.com'; // 실제 도메인/포트로 수정
const CHANNEL_ROOM_ID = '3'; // 테스트할 채널 ID
const ACCESS_TOKEN = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI3IiwiaWF0IjoxNzQ4MzI1NTQ1LCJleHAiOjE3NDk1MzUxNDV9.nzfvKDfzogl0NSLwXBXqUckcbRlp9jxGnxwS8aNsOdQ'; // 실제 유효한 토큰으로 교체
export default function () {
const url = `${BASE_URL}/api/v1/channel-rooms/${CHANNEL_ROOM_ID}/messages`;
const payload = JSON.stringify({
message: '안녕하세요! 반갑습니다.',
});
const params = {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${ACCESS_TOKEN}`,
},
};
const res = http.post(url, payload, params);
check(res, {
'status is 201 or 410': (r) => r.status === 201 || r.status === 410,
});
sleep(1); // 1초 대기 (optional)
}