부하테스트 시나리오 3: 채널방 – 메세지 보내기 - 100-hours-a-week/2-hertz-wiki GitHub Wiki

1. 사용자 시나리오 설명

시나리오 설명: 사용자가 개인채널에 들어가서 상대방에게 메세지를 입력하여 전송

사용자가 메세지 입력란을 통해 메세지를 입력하여 전송 버튼을 클릭. 버튼이 클릭되면 /api/v1/channel-rooms/{channelRoomId}/messages API를 호출하며, 백엔드에서는 전송된 메세지를 DB에 저장합니다.

2. 테스트 개요

항목 내용
테스트 목적 사용자가 상대방과 대화를 나누는 과정에서의 성능 및 부하 검증
테스트 환경 - Monolithic: e2-custom-2-6144 (vCPU 2개, 메모리 4GB)
대상 시스템 백엔드(SpringBoot + MySQL)
테스트 도구 k6
테스트 일시 2025년 5월 22일

2.1 테스트 목적

사용자가 시그널을 받은 상대방과 대화를 나누는 과정에서의 성능 및 부하를 검증하기 위함입니다.

2.2 테스트 API 및 호출 흐름

어플리케이션 API 엔드포인트 메서드 설명
SpringBoot /api/v1/channel-rooms/{channelRoomId}/messages POST 메세지 내용 포함

3. 테스트 조건 및 부하 프로파일

차수 1차 2차 3차
동시 사용자 수 100명 500명 10
총 요청 수 500회 1500회 1000회
요청 간격 1초 1초 1초
부하 생성 패턴 shared-iterations (고정 개수 요청 배분) shared-iterations (고정 개수 요청 배분) shared-iterations (고정 개수 요청 배분)
배치 처리 방식 멀티스레드 활용, 동시 요청 처리 멀티스레드 활용, 동시 요청 처리 멀티스레드 활용, 동시 요청 처리

4. 테스트 결과 요약

🔹 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초 전체 테스트 소요 시간

전체 리소스 사용량

image image 1

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

image 2 image 3

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

image 4
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초 전체 테스트 소요 시간

서버 리소스 부족으로 다운 발생

전체 리소스 사용량

image 5 image 6

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

image 7 image 8
🔹 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초 전체 테스트 소요 시간

전체 리소스 사용량

image 9 image 10

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

image 11 image 12

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

5. 결과 분석

응답시간 분석

  • 동시 요청 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 상 비정상적으로 긴 처리 공백 시간 또한 리소스 부족으로 인한, 메모리 부족, 스레드 부족 등의 문제를 일으켰을 가능성이 높음

6. 개선 방안 제안

  • 현재 서버 자원으로는 100건 이상의 동시 요청을 안정적으로 처리하기 어려우며, 인프라 확장 또는 구조적 최적화가 필요하다.
  • 현재 도커 컨테이너 환경으로의 마이그레이션을 통해 nextjs, springboot, mysql을 각 각의 인스턴스로 분리 예정이므로, 마이그레이션 전에는 사용자 수 100명에 맞춰 인스턴스 리소스를 CPU 2코어, RAM 8GB까지 늘리는 것을 제안한다.

7. 부록

  • 테스트 스크립트: /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)
}
⚠️ **GitHub.com Fallback** ⚠️