V1 부하테스트 - 100-hours-a-week/20-real-wiki GitHub Wiki
V1 API 부하테스트
1. 테스트 개요
항목 | 내용 |
---|---|
테스트 목적 | API 시나리오 기반의 성능 및 정합성 검증 |
테스트 대상 시스템 | Spring Boot + MySQL 기반 백엔드 API |
테스트 도구 | Apache JMeter 5.6.3 |
환경 조건 | 부하테스트용 서버 (t3.medium): dev 인스턴스 복제 |
인증 방식 | 테스트용 토큰 (User 수: 1000명) |
1.1. 테스트 준비
- Python Script를 통해 실제 부하테스트 서버에 회원가입 진행 (단일성)
- Python Script를 통해 실제 부하테스트 서버에 1에서 회원가입한
user
를 찾아 실제 로그인 진행 →accessToken
추출 - 추출한
accessToken
을csv
파일로 준비 - JMeter에 추출한
csv
파일을 불러와 토큰 헤더 설정
1.1.1. Python Script
import requests
import csv
BASE_URL = "http://{부하테스트 인스턴스 IP}:8080"
NUM_USERS = 1000
PASSWORD = "securePassword123!"
tokens = []
# 회원가입 (이미 가입되어 있다면 예외 무시)
for i in range(1, NUM_USERS + 1):
email = f"user{i}@example.com"
nickname = f"user{i}"
try:
requests.post(f"http://{부하테스트 인스턴스 IP}/auth/signup", json={
"email": email,
"nickname": nickname,
"password": PASSWORD
})
except Exception:
pass # 이미 존재하는 유저일 경우 무시
# 로그인 및 토큰 저장
for i in range(1, NUM_USERS + 1):
email = f"user{i}@example.com"
resp = requests.post(f"http://{부하테스트 인스턴스 IP}:8080/login", json={
"email": email,
"password": PASSWORD
})
print(i + "번: " + resp)
if resp.status_code == 200:
token = resp.json().get("data", {}).get("accessToken", "")
if token:
tokens.append([token])
else:
print(f"Login failed for user{i}")
# tokens.csv 저장
with open("tokens.csv", "w", newline='') as f:
writer = csv.writer(f)
writer.writerow(["token"]) # CSV 헤더
writer.writerows(tokens)
from google.colab import files
files.download("tokens.csv")
1.1.2. JMeter 설정
API Core Test Plan (Test Plan)
→ Test Plan 내부에서 쓰일 기본적인 변수 정의
${BASE_URL} = load.kakaotech.com
: 부하테스트용 인스턴스의 도메인 이름${BASE_PATH} = /api/v1
: 부하테스트에서 사용할 공통 Path${fixedNoticeId} = 110
: 부하테스트에서 사용할 공지 Id (in 무결성 테스트)${fixedNewsId} = 6
: 부하테스트에서 사용할 뉴스 Id (in 무결성 테스트)
CSV Data Set Config
→ Python Script를 통해 생성한 tokens.csv
를 가져와 각 스레드에 토큰을 주입하는 Config Element
HTTP Header Manager
→ API 요청을 보낼 때, HTTP Header에 위에서 가져온 token
을 넣기 위한 Config Element
HTTP Request Defaults
→ API 요청을 보낼 때, 공통된 프로토콜과 URL을 설정하는 Config Element
2. 테스트 시나리오
시나리오 ID | 시나리오 이름 | 설명 |
---|---|---|
S-1 | 공지 열람 | 공지 목록·상세·댓글 읽기 |
S-2 | 뉴스 열람 | 뉴스 목록·상세·댓글 읽기 |
S-3 | 공지 상호작용 | 공지 목록→상세→댓글 작성→좋아요 토글 |
S-4 | 뉴스 상호작용 | 뉴스 목록→상세→댓글 작성→좋아요 토글 |
S-5 | 챗봇 Q&A | 질문 생성 → AI 답변 수신 시간·정합성 확인 |
S-6 | 댓글 CRUD 무결성 | 댓글 작성 → 삭제 → 사라졌는지 확인 |
S-7 | 공지 조회수 무결성 | 동일 공지에 50VU 동시 GET → 조회수 무결성 검사 |
S-8 | 뉴스 조회수 무결성 | 동일 뉴스에 50VU 동시 GET → 조회수 무결성 검사 |
S-9 | 공지 동시 좋아요 경쟁 | 동일 공지에 50VU 동시 PUT → 카운트 무결성 검사 |
S-10 | 뉴스 동시 좋아요 경쟁 | 동일 뉴스에 50VU 동시 PUT → 카운트 무결성 검사 |
2.1. 성능 테스트
2.1.1. S-1 (공지 열람)
순서 | Method | Endpoint |
---|---|---|
1 | GET | /api/v1/notices |
2 | GET | /api/v1/notices/${noticeId} |
3 | GET | /api/v1/notices/${noticeId}/comments |
2.1.2. S-2 (뉴스 열람)
순서 | Method | Endpoint |
---|---|---|
1 | GET | /api/v1/news |
2 | GET | /api/v1/news/${newsId} |
3 | GET | /api/v1/news/${newsId}/comments |
2.1.3. S-3 (공지 상호작용)
순서 | Method | Endpoint | Response Body |
---|---|---|---|
1 | GET | /api/v1/notices |
x |
2 | GET | /api/v1/notices/${noticesId} |
x |
3 | POST | /api/v1/notices/${noticesId}/comments |
{ “content” : “성능 테스트 댓글 ${__RandomString(5,abcdef)}” |
4 | PUT | /api/v1/notices/${noticesId}/likes |
x |
5 | GET | /api/v1/notices/${noticesId} |
x |
2.1.4. S-4 (뉴스 상호작용)
순서 | Method | Endpoint | Response Body |
---|---|---|---|
1 | GET | /api/v1/news |
x |
2 | GET | /api/v1/news/${newsId} |
x |
3 | POST | /api/v1/news/${newsId}/comments |
{ “content” : “성능 테스트 댓글 ${__RandomString(5,abcdef)}” |
4 | PUT | /api/v1/news/${newsId}/likes |
x |
5 | GET | /api/v1/news/${newsId} |
x |
2.1.5. S-5 (챗봇 Q&A)
순서 | Method | Endpoint | Response Body |
---|---|---|---|
1 | POST | /api/v1/chatbots |
{"question":"멘토링 데이 날짜를 알려줘."} |
2.2. 무결성 검증 테스트
무결성 검증 테스트는 다음과 같은 세 Thread Group으로 진행됩니다.
- Integrity SetUp (setUp Thread Group): 무결성 테스트 시작 전, 데이터가 안전하게 가져와지고 저장되는지 판단하기 위해 초기 값을 불러오기 위한 Thread Group
- Integrity Test (Test Thread Group): 실제 여러 사용자가 동시에 같은 요청을 보냈을 상황을 진행하는 Thread Group
- Integrity TearDown (tearDown Thread Group): 여러 사용자가 모든 요청을 끝내고, 데이터가 무결한 지 파악하기 위한 테스트 이후 값을 불러오기 위한 Thread Group
2.2.1. S-6 (댓글 CRUD 무결성)
Integrity Test (Test Thread group)
순서 | Method | Endpoint | Response Body | 추가 설명 |
---|---|---|---|---|
1 | POST | /api/v1/notices/${noticeId}/comments |
{ "content": "댓글 CRUD 테스트 from ${__threadNum}"} |
공지 댓글 작성 |
2 | GET | /api/v1/notices/${noticeId}/comments |
x | 1번에서 작성한 commentId 가져오기 |
3 | DELETE | /api/v1/notices/${noticeId}/comments/${commentId} |
x | 2번에서 가져온 commentId 삭제 |
5 | POST | /api/v1/news/${newsId}/comments |
{ "content": "댓글 CRUD 테스트 from ${__threadNum}"} |
공지 댓글 작성 |
6 | GET | /api/v1/news/${newsId}/comments |
x | 5번에서 작성한 commentId 가져오기 |
7 | DELETE | /api/v1/news/${newsId}/comments/${commentId} |
x | 6번에서 가져온 commentId 삭제 |
2.2.2. S-7, S-8 (공지/뉴스 조회수 무결성)
Integrity SetUp (setUp Thread group)
순서 | Method | Endpoint | Response Body | 추가 설명 |
---|---|---|---|---|
1 | GET | /api/v1/notices/${noticeId} |
x | 공지 조회 수/좋아요 수 초기 데이터 가져오기 |
2 | GET | /api/v1/news/${newsId} |
x | 뉴스 조회 수/좋아요 수 초기 데이터 가져오기 |
Integrity Test (Test Thread group)
순서 | Method | Endpoint | Response Body | 추가 설명 |
---|---|---|---|---|
1 | GET | /api/v1/notices/${noticeId} |
x | 공지 조회수 무결성 테스트 |
2 | GET | /api/v1/news/${newsId} |
x | 뉴스 조회수 무결성 테스트 |
Integrity TearDown (tearDown Thread Group)
순서 | Method | Endpoint | Response Body | 추가 설명 |
---|---|---|---|---|
1 | GET | /api/v1/notices/${noticeId} |
x | 테스트 이후 공지 조회 수/ 좋아요 수 가져오기 |
2 | GET | /api/v1/news/${newsId} |
x | 테스트 이후 뉴스 조회 수/ 좋아요 수 가져오기 |
2.2.4. S-9, S-10 (공지/뉴스 동시 좋아요 경쟁)
Integrity SetUp (setUp Thread group)
순서 | Method | Endpoint | Response Body | 추가 설명 |
---|---|---|---|---|
1 | GET | /api/v1/notices/${noticeId} |
x | 공지 조회 수/좋아요 수 초기 데이터 가져오기 |
2 | GET | /api/v1/news/${newsId} |
x | 뉴스 조회 수/좋아요 수 초기 데이터 가져오기 |
Integrity Test (Test Thread group)
순서 | Method | Endpoint | Response Body | 추가 설명 |
---|---|---|---|---|
1 | PUT | /api/v1/notices/${noticeId}/likes |
x | 공지 좋아요 무결성 테스트 |
2 | PUT | /api/v1/news/${newsId}/likes |
x | 뉴스 좋아요 무결성 테스트 |
Integrity TearDown (tearDown Thread Group)
순서 | Method | Endpoint | Response Body | 추가 설명 |
---|---|---|---|---|
1 | GET | /api/v1/notices/${noticeId} |
x | 테스트 이후 공지 조회 수/ 좋아요 수 가져오기 |
2 | GET | /api/v1/news/${newsId} |
x | 테스트 이후 뉴스 조회 수/ 좋아요 수 가져오기 |
3. 테스트 종류 및 구성
3.1. Smoke Test (기능 검증)
목적: 배포 직후에 시스템의 핵심 기능이 정상적으로 동작하는지, 빠르게 확인하기 위한 테스트
항목 | 구성 내용 | 구성 이유 |
---|---|---|
Threads | 1명 | 단일 사용자로 기능 정상 여부만 확인 |
Loop | 1회 | 기능 흐름 1회만 수행하여 최소 동작 검증 |
Ramp-Up | 0초 | 즉시 실행하여 빠른 피드백 확보 |
기능 흐름 구성 | 사용자가 사용하는 핵심 시나리오 흐름 위주로 구성 | 실제 사용자가 수행하는 핵심 기능의 정상 동작 여부 확인 |
Assertion | 응답 코드 200 | 최소한의 성공 조건 확인 및 기능 실패 조기 감지 |
Listener | View Results Tree | 즉시 결과 확인 + 오류 시 원인 파악 가능 |
3.2. Baseline Test (기본 부하 테스트)
목적: 시스템이 부하 없이 정상 상태에서 제공할 수 있는 기준 성능(SLA 기준선)을 파악하기 위한 테스트 사용자가 소수로 행동할 때의 평균 응답 속도, 오류율, 시스템 처리 능력을 측정
항목 | 구성 내용 | 구성 이유 |
---|---|---|
Threads | 10명 | 소수 사용자 기준 성능 측정 |
Loop | 5회 | 요청 반복으로 안정된 평균 확보 |
Ramp-Up | 2초 | 사용자들이 2초 뒤에 행동하는 것을 가정 |
Think Time | 1~3초 (Uniform Random Timer ) |
실 사용자 행동 간 시간 반영 |
SLA | Response Code 200 | |
오류율 ≤ 1% | ||
평균 응답 시간 ≤ 700ms | 성공 여부와 서비스 사용자 이용 경험성 증대 | |
Listener | Summary Report | |
Aggregate Reports Tree | 실시간 SLA 확인 및 지표 수집 |
3.3. Load Test (실제 운영 테스트)
목적: 실사용자와 유사한 트래픽 환경에서 시스템의 확장성, 응답 시간 변화, 오류율 증가 여부 등을 파악 평균 응답 속도뿐 아니라 TPS, 에러율, Percentile 응답 시간(P95, P99) 등을 통해 SLA 충족 여부를 측정
항목 | 구성 내용 | 구성 이유 |
---|---|---|
Threads | 40명 | 실제 운영 사용자 수 시뮬레이션 |
Loop | Forever (with duration) | 일정 시간 동안 반복 유지 |
Ramp-Up | 60초 | 갑작스런 트래픽 유입이 아닌 점진적 유입 시뮬레이션 |
Think Time | 1~3초 (Uniform Random Timer ) |
사용자의 입력·대기 시간 반영 (RPS 과장 방지) |
Assertion | Response Code 200 | |
오류율 ≤ 2% | ||
평균 응답 시간 ≤ 1200ms | SLA 기준 만족 여부 확인 | |
Listener | Summary Report | |
Aggregate Reports Tree | Throughput, Percentile 분석 |
3.4. Spike Test (과부하 테스트)
목적: 서버가 감당하기 힘든 트래픽을 보냈을 때, 실제 어느 API 호출에서 부하가 발생하고 에러가 발생하는지 확인하기 위한 테스트
항목 | 구성 내용 | 구성 이유 |
---|---|---|
Threads | 초기 300명 → 400명 추가 → 최종 700명 | 순간 사용자 증가에 따른 서버의 최대 처리 용량 및 안정성 확인 |
Loop | 1회 | 사용자 수 자체가 많아 반복 없이도 충분한 부하 발생 |
Ramp-Up | 초기 300명은 즉시이후 400명은 20초 간격, ramp-up 1초 | 급격한 사용자 유입 시 발생 가능한 부하와 스파이크 상황 재현 |
Think Time | 없음 (지연 없음) | 최대 부하를 걸기 위한 순수 트래픽 집중 환경 구성 |
SLA | Response Code 200 | |
오류율 ≤ 5% | ||
평균 응답 시간 ≤ 2000ms | 정상 응답 비율, 사용자 체감 속도 등을 기준으로 서버 임계점 판단 | |
Hold Duration | 100초 | 최대 부하 상태에서의 지속 처리 능력 확인 |
Stop Configuration | 300명씩 3초 간격으로 종료 | 부하 완화 시점의 서버 안정성 회복 여부 측정 |
Listener | Summary Report | |
Aggregate Report | ||
View Results Tree | 실시간 SLA 만족 여부 확인 및 상세 에러 응답 분석을 위한 지표 확보 |
3.5. 댓글 Integrity Test (댓글 무결성 테스트)
목적: 댓글 기능에 대해 동시에 여러 사용자가 CRUD 작업을 수행해도, 데이터 무결성이 보장되는지 검증 동시성 환경에서 발생할 수 있는 Race Condition, ID 중복, 삭제 누락 등을 직접 확인
항목 | 구성 내용 | 구성 이유 |
---|---|---|
Threads | 50명 | 동시 작성·수정·삭제 시 정합성 검증 |
Loop | 1회 | 각 스레드는 고유한 댓글 흐름을 1회만 수행 |
Ramp-Up | 1초 | 각 스레드가 동시에 API 요청 시작 |
요청 흐름 | POST → DELETE | 사용자 단위 CRUD 흐름을 순차적으로 구성 |
검증 방식 | JSR223 Sampler: 삭제된 ID가 다시 응답에 포함되는지 확인 | 단순 200 응답이 아닌 실제 삭제 여부 확인 |
데이터 처리 | 댓글 내용에 __threadNum 포함 |
각 Thread가 자기 댓글만 추적 가능하도록 유일성 확보 |
3.6. 공지/뉴스 좋아요 Integrity Test (좋아요 무결성 테스트)
목적: 좋아요 기능에 대해 동시에 여러 사용자가 PUT 요청을 수행해도, 데이터 무결성이 보장되는지 검증 동시성 환경에서 발생할 수 있는 좋아요 누락, 요청 무시 등을 확인
항목 | 구성 내용 | 구성 이유 |
---|---|---|
Threads | 50명 | 동시 작성·수정·삭제 시 정합성 검증 |
Loop | 1회 | 각 스레드는 고유한 좋아요 요청을 1회만 수행 |
Ramp-Up | 1초 | 각 스레드가 동시에 API 요청 시작 |
요청 흐름 | 단일 PUT 요청 | 한 스레드가 하나의 PUT을 요청 |
검증 방식 | JSR223 Sampler: 검증 테스트 이전 좋아요 수와 검증 테스트 이후 좋아요 수가 같은지 확인 | 단순 200 응답이 아닌 50개의 쓰레드가 요청을 보냈을 때, 좋아요 수가 변함이 없어야 함 (하나의 유저를 가정) |
3.7. 공지/뉴스 조회수 Integrity Test (조회수 무결성 테스트)
목적: 동시에 여러 사용자가 공지/뉴스 상세 정보를 조회해도, 조회수 데이터의 무결성이 보장되는지 검증
항목 | 구성 내용 | 구성 이유 |
---|---|---|
Threads | 50명 | 실제 사용자의 동시 조회 시나리오를 시뮬레이션 |
Loop | 1회 | 각 사용자가 한 번만 조회하여 정확한 증가량 추적 가능 |
Ramp-Up | 1초 | 각 스레드가 동시에 API 요청 시작 |
초기값 저장 | setUp Thread Group 에서 viewCount 추출 및 props 에 저장 |
테스트 시작 전 기준값 확보 |
최종값 추출 및 검증 | TearDown Thread Group 에서 viewCount 재조회 → JSR223로 증가량 비교 (final - initial == 50 ) |
조회 수의 실제 증가량이 기대값과 일치하는지 정합성 검증 |
Assertion | 응답 코드 200 + viewCount 추출 가능 여부 확인 | API 자체 정상 동작 여부 확인 |
Listener | View Results Tree | 요청 성공/실패 수 및 응답 시간, 무결성 실패 시 추적 가능 |
4. 테스트 진행 결과
4.1. Smoke Test (기능 검증)
API Endpoint | Avg. Resp (ms) | Max Resp (ms) | Error Rate |
---|---|---|---|
Notice → Detail → Comments | 197 | 197 | 0.000% |
News → Detail → Comments | 65 | 65 | 0.000% |
Notice → Detail → Comments → Write Comment → Like | 174 | 174 | 0.000% |
News → Detail → Comments → Write Comment → Like | 159 | 159 | 0.000% |
GET /notices | 80 | 135 | 0.000% |
GET /notices/124 | 32 | 34 | 0.000% |
GET /notices/124/comments | 26 | 30 | 0.000% |
POST /notices/124/comments | 41 | 41 | 0.000% |
PUT /notices/124/likes | 51 | 51 | 0.000% |
GET /news | 25 | 32 | 0.000% |
GET /news/6 | 28 | 31 | 0.000% |
PUT /news/6/likes | 31 | 31 | 0.000% |
POST /chatbots | 9,688 | 9,688 | 0.000% |
Chatbot (대화 흐름 전체) | 9,688 | 9,688 | 0.000% |
TOTAL (전체 평균) | 934 | 9.688 | 0.000% |
검증 성공 조건: 응답 상태코드 200, 오류율 = 0%
모든 핵심 API 요청에 대해 오류가 없었고(Error Rate = 0%), 챗봇을 제외한 다른 단일 API의 평균 응답 시간도 100ms 이내로 우수한 편이며, 기능적으로 정상 작동함을 확인할 수 있음
그러나 아직까지 챗봇에 대한 성능에 대한 개선은 필요
4.2. Baseline Test (기본 부하 테스트)
API Endpoint | Avg. Resp (ms) | Max Resp (ms) | Error Rate | Throughput (req/s) |
---|---|---|---|---|
Notice → Detail → Comments | 6,220 | 8,175 | 0.000% | 0.258 |
News → Detail → Comments | 6,086 | 8,824 | 0.000% | 0.256 |
Notice → Detail → Comments → Write Comment → Like | 10,938 | 13,428 | 0.000% | 0.250 |
News → Detail → Comments → Write Comment → Like | 10,512 | 12,871 | 0.000% | 0.248 |
GET /notices | 81 | 602 | 0.000% | 0.496 |
GET /notices/124 | 99 | 634 | 0.000% | 0.496 |
GET /notices/124/comments | 81 | 829 | 0.000% | 0.704 |
POST /notices/124/comments | 61 | 193 | 0.000% | 0.264 |
PUT /notices/124/likes | 72 | 470 | 0.000% | 0.262 |
GET /news | 69 | 664 | 0.000% | 0.487 |
GET /news/6 | 96 | 850 | 0.000% | 0.483 |
PUT /news/6/likes | 80 | 519 | 0.000% | 0.263 |
POST /chatbots | 9,684 | 11,693 | 0.000% | 0.251 |
Chatbot (대화 흐름 전체) | 9,684 | 11,693 | 0.000% | 0.248 |
TOTAL (전체 평균) | 2,474 | 13,428 | 0.000% | 4.719 |
검증 성공 조건: 응답코드 200, 평균 응답 시간 ≤ 700ms, 오류율 ≤ 1%
챗봇을 제외한 단일 API 평균 응답속도는 70~100ms 사이로 매우 안정적. 그러나 복합적인 시나리오에서 평균적으로 응답 시간이 6초 ~ 10초 이상 걸려, UX에 악영향 가능성 존재
- 병목 구간 후보:
- 댓글 등록, 좋아요 처리, DB I/O
그럼에도 모든 API에서 오류율을 0%로 안정적으로 전 요청 정상 처리를 하는 것을 확인
4.3. Load Test (실제 운영 부하 테스트)
API Endpoint | Avg. Resp (ms) | Max Resp (ms) | Error Rate | Throughput (req/s) |
---|---|---|---|---|
Notice → Detail → Comments | 6,334 | 9,358 | 0.000% | 0.85244 |
News → Detail → Comments | 6,377 | 10,481 | 0.000% | 0.85800 |
Notice → Detail → Comments → Write Comment → Like | 10,566 | 14,611 | 0.000% | 0.85284 |
News → Detail → Comments → Write Comment → Like | 10,647 | 15,344 | 0.000% | 0.85404 |
GET /notices | 115 | 1,420 | 0.000% | 1.70932 |
GET /notices/124 | 119 | 1,833 | 0.000% | 1.70928 |
GET /notices/124/comments | 105 | 1,686 | 0.000% | 2.54514 |
POST /notices/124/comments | 118 | 1,695 | 0.171% | 0.86395 |
PUT /notices/124/likes | 107 | 1,237 | 0.000% | 0.86366 |
GET /news | 100 | 1,708 | 0.000% | 1.70545 |
GET /news/6 | 116 | 1,840 | 0.000% | 1.70836 |
PUT /news/6/likes | 127 | 1,532 | 0.000% | 0.86012 |
POST /chatbots | 9,897 | 15,409 | 1.751% | 0.86102 |
Chatbot (대화 흐름 전체) | 9,880 | 15,409 | 1.748% | 0.85933 |
TOTAL (전체 평균) | 2,503 | 15,409 | 0.163% | 18.45090 |
검증 성공 조건: 응답코드 200, 평균 응답 시간 ≤ 1200ms, 오류율 ≤ 2%
대부분의 요청이 오류율 0%로 성공적으로 처리. 그러나, POST /chatbots
요청에서 오류율 1.75% 수준으로 발생. 이는 추가적으로 로그를 확인해서 결론을 내야 함
단일 시나리오가 아닌 복합 시나리오에서 전체적으로 10초 이상 소요되는 요청이 다수이며, 이는 사용자 이탈을 유발할 수 있는 성능으로 판단
- Throughput 분석:
- 평균 처리량은 0.85 ~ 0.86 req/s 수준으로 부하 자체는 낮지 않음
- 이는 백엔드 응답이 지연되고 있음에도 일정 수준 이상의 요청을 지속적으로 수용 중이라는 것을 의미
- 동시에, 대기 요청 큐가 쌓이고 있다는 신호
개선 필요 영역
우선순위 | 항목 | 조치 |
---|---|---|
🔴 1 | 복합 시나리오 응답 시간 과다 (10~15초) | 각 요청 단위로 분해하여 지연 API 식별, APM으로 트레이싱 |
🟠 2 | Chatbot 관련 오류 발생 | API 내부 로직/에러 응답 확인, 재현 시나리오 확보 |
🟡 3 | 전반적인 요청 처리량 낮음 | DB 커넥션 풀, 스레드 수 조정 등 서버 설정 점검 |
🟢 4 | 단일 API 응답은 안정적 (별도 분석 필요) | 단건 요청은 거의 문제 없음. 복합 시나리오 중심으로 집중 분석 필요 |
4.4. Spike Test (과부하 테스트)
API Endpoint | Avg. Resp (ms) | Max Resp (ms) | Error Rate | Throughput (req/s) |
---|---|---|---|---|
Notice → Detail → Comments | 12,433 | 20,966 | 0.116% | 13.66228 |
News → Detail → Comments | 10,821 | 19,239 | 13.526% | 11.85451 |
Notice → Detail → Comments → Write Comment → Like | 19,820 | 30,742 | 0.0% | 8.96101 |
News → Detail → Comments → Write Comment → Like | 17,076 | 29,933 | 4.568% | 10.9529 |
GET /notices | 3,848 | 65,977 | 9.973% | 17.24361 |
GET /notices/124 | 4,001 | 9,428 | 0.06% | 20.20798 |
GET /notices/124/comments | 3,948 | 10,563 | 1.326% | 26.00428 |
POST /notices/124/comments | 4,122 | 8,176 | 0.0% | 9.8031 |
PUT /notices/124/likes | 3,765 | 8,411 | 0.0% | 9.74349 |
GET /news | 3,473 | 8,952 | 0.238% | 17.83628 |
GET /news/6 | 3,292 | 9,641 | 4.657% | 17.75173 |
PUT /news/6/likes | 3,335 | 9,728 | 3.57% | 11.44132 |
POST /chatbots | — | — | — | — |
Chatbot (대화 흐름 전체) | — | — | — | — |
TOTAL (전체 평균) | 4,929 | 6,5977 | 4.085% | 188.54823 |
검증 성공 조건: 응답 코드 200, 오류율 ≤5%, 평균 응답 시간 ≤ 2000ms
Chatbot에 대한 API 호출은 SLA이 미충족될 것임은 자명하기에 테스트에서 제외. 평균 응답 시간 SLA는 전 항목이 미통과되었음. 오류율 SLA를 통과한 API가 존재하긴 하나, 대부분의 API 호출에서 오류율 SLA를 미통과. 결국 모든 시나리오가 SLA 미달 상태라고 판단할 수 있음.
이는 현재 시스템이 1000명 수준의 급격한 사용자 유입을 감당하기에는 성능상 한계가 있음을 의미함
세부 개선 필요 사항
GET /notices
- 병목 지점:
/notices
응답에서 가장 높은 오류율(9.97%)과 응답 지연(3.8s 이상) 발생 - 해결책:
- Redis 캐싱: 동일 요청이 반복되는 경우, DB 접근 없이 응답 제공
- 서버 사이드 pagination 캐싱 적용 등
- 병목 지점:
- Spring Boot 성능 최적화
- 전체 응답 시간이 기본적으로 수 초 이상 → 비정상적인 지연
- 해결책:
- 비동기 처리
- 쿼리 최적화
- 서버 내부 로깅 레벨 조정
- ThreadPool 설정 조정
- DB 병목 최소화
- 댓글/좋아요 관련 API도 점차 느려짐 → 누적 쿼리 부하 가능성
- 해결책:
- RDS 리소스로의 전환
- Read Replica 구성 → 읽기 트래픽 분산
- Index 최적화 및 slow query log 분석
- 부하 분산: Auto Scaling 적용
- API Gateway 또는 Rate Limiting 도입
- 스파이크 상황에서 특정 사용자나 봇 트래픽이 시스템을 마비시킬 수 있음
- 해결책:
- API Gateway + Throttling 정책 설정
- Nginx rate limiting 또는 Spring Interceptor 도입
- 정상 사용자 우선순위 처리 정책 마련
- 모니터링 + Alert 시스템 구축
- 실시간으로 병목이나 응답 실패 탐지 불가
- 해결책:
- GloudWath, Grafana Prometheus, Scouter, SigNoz 등 모니터링 툴 도입
- 스파이크 상황 시 SLA 기준 초과 여부 자동 감지 및 알림
4.4. 댓글 Integrity Test (댓글 무결성 테스트)
댓글 검증 Sampler 코드
String postedId = vars.get("postedNoticeCommentId");
String deletedId = vars.get("deletedNoticeCommentId");
if (!postedId.equals(deletedId)) {
throw new Exception("댓글 ID 정합성 실패: 작성한 ID(" + postedId + ") ≠ 삭제한 ID(" + deletedId + ")");
} else {
log.info("댓글 ID 정합성 통과: " + postedId);
}
String postedId = vars.get("postedNewsCommentId");
String deletedId = vars.get("deletedNewsCommentId");
if (!postedId.equals(deletedId)) {
thrownew Exception("댓글 ID 정합성 실패: 작성한 ID(" + postedId + ") ≠ 삭제한 ID(" + deletedId + ")");
}else {
log.info("댓글 ID 정합성 통과: " + postedId);
}
검증 성공 조건:
postedId == deletedId
먼저 POST
요청을 할 때, postedNoticeCommentId
및 postedNewsCommentId
를 저장해놓고, 그 다음 DELETE
요청을 할 때, deletedNoticeCommentId
및 deletedNewsCommentId
를 저장해, 그 둘의 값을 비교하는 검증 방식을 택함.
테스트를 진행한 결과, 모든 API 요청에서 두 변수의 값이 같은 것을 확인할 수 있었음.
4.5. 공지/뉴스 좋아요 Integrity Test (좋아요 무결성 테스트)
V2로 테스트 이전
4.6. 공지/뉴스 조회수 Integrity Test (조회수 무결성 테스트)
조회 수 검증 Sampler 코드
int initialNotice = Integer.parseInt(props.get("initialNoticeViewCount"));
int finalNotice = Integer.parseInt(props.get("finalNoticeViewCount"));
int expectedIncrease = Integer.parseInt(props.get("expectedIncrease")) + 1;
// 조회 무결성 검증
if (finalNotice - initialNotice != expectedIncrease) {
log.error("Assertion failed: expectedIncrease " + expectedIncrease + ", but got " + (finalNotice - initialNotice));
throw new Exception("Notice View count mismatch: expected increase " + expectedIncrease + ", actual " + (finalNotice - initialNotice));
}
int initialNews = Integer.parseInt(props.get("initialNewsViewCount"));
int finalNews = Integer.parseInt(props.get("finalNewsViewCount"));
int expectedIncrease = Integer.parseInt(props.get("expectedIncrease")) + 1;
if (finalNews - initialNews != expectedIncrease) {
log.error("Assertion failed: expectedIncrease " + expectedIncrease + ", but got " + (finalNews - initialNews));
thrownew Exception("News View count mismatch: expected increase " + expectedIncrease + ", actual " + (finalNews - initialNews));
}
검증 성공 조건:
finalNotice - initialNotice = 51
,finalNews - initialNews = 51
한번에 요청을 보내는 유저의 수는 50명이므로, expectedIncrease = 50
또한, finalViewCount
를 가져올 때, GET
요청을 한 번 더 보내므로 성공 기준에 대한 수를 51으로 설정.
테스트를 진행한 결과, 모든 API 요청에서 정확히 조회 수가 51 증가한 것을 확인할 수 있었음.
5. 지표 분석
부하가 걸리는 지점을 파악하기 위해서 Smoke Test와 Baseline Test는 생략했다.
5.1. Load Test (실제 운영 테스트)
5.1.1. TPS 분석
- TPS(Transactions Per Second)가 대부분 18
19 수준에서 유지되다가, **중간 구간(약 13:4313:44)에 일시적으로 17 아래로 하락**한 뒤 다시 회복됨. - 이 하락은 순간적 병목 또는 응답 지연으로 인해 TPS가 떨어진 것으로 보이며, 서버 전체 처리량이 제한된 순간으로 판단됨.
✅ 결론: 대부분 안정적이지만 짧은 병목 구간 존재 → 그 시간대의 Heap/GC를 함께 봐야 함.
5.1.2. Perm Used (PermGen 영역)
- Perm 영역(120MB)이 테스트 전반에 걸쳐 완전히 평탄함
- 이는 클래스로딩, static 객체 증가 등의 이상 현상이 전혀 없음을 의미
✅ 결론: 안정적. 클래스 누수, 메타 정보 증가 없음
5.1.3. CPU 사용률
- 전체적으로 20~35% 수준에서 안정적으로 유지
- 중간에 피크가 있어도 45%를 넘지 않음
- 따라서 CPU 병목은 없으며, CPU는 여유가 있는 상태
✅ 결론: CPU 리소스는 충분하고, 병목 원인은 CPU가 아님
5.1.4. Heap Used (GC 사이클)
- Heap 사용량이 주기적으로 70 → 170MB로 증가했다가 GC로 감소하는 패턴 반복
- 이는 정상적인 Minor GC 사이클로 해석됨
- 단, GC 사이클이 상당히 자주 일어남 (10초 미만 주기)
⚠️ 결론: Memory Pressure는 없지만, GC가 너무 자주 발생하면 Throughput 저하 가능성 있음. 또한, GC tuning (Heap size 조정, G1GC 고려) 필요 가능성 있음
5.1.5. GC Count 그래프
- GC 횟수는 0~3회 사이에서 계속 반복
- 이는 Heap 그래프와 연동되며, 거의 10초 이하 주기로 GC 발생
- Full GC는 발생하지 않음
⚠️ 결론: Minor GC만 반복되지만, 빈도는 다소 많음. TPS 하락 구간(13:43~44)에 GC 스파이크가 연관돼 있을 가능성 있음
5.1.6. XLog 분석 (응답시간이 늦은 개별 트랜잭션 상세)
- 대상 요청:
GET /news/${newsId}/comments
- 특징:
- SQL이 10회 이상 연속 실행됨
SELECT u0_.id, ... FROM user ...
패턴이 반복 → N+1 쿼리 강력 의심- 특정 쿼리 하나가
127ms
→ 나머지는 대부분 수 ms
PREPARE
,RESULT
,FETCH
흐름이 길게 유지됨
❗ 결론: 명확한 N+1 쿼리 현상 존재. 댓글을 불러오면서 관련된 유저 정보를 매 댓글마다 개별 쿼리로 불러오는 구조 → 해당 API는 쿼리 튜닝, fetch join, batch join 최적화 필요
5.2. Spike Test (과부하 테스트)
5.2.1. TPS 분석
- TPS가 급격히 증가하여 최고 약 120 TPS까지 상승
- 이후 14:17:15~14:17:30 사이에 급락 → 테스트 종료
- 평균적으로 100 TPS 이상 처리 지속은 서버 처리 성능 기준치로 볼 수 있음
🔍 결론: 서버가 300스레드 부하를 걸었을 때 약 100~120 TPS 수준으로 응답을 유지했다는 점은 매우 좋은 결과. 급락은 부하 종료 시점으로 판단되며 병목이 아님.
5.2.2. Perm Used (PermGen 영역)
Load Test와 동일
5.2.3. CPU 사용률
- CPU 사용률이 100% 근접하여 약 2분 이상 고정됨
- 부하 종료 후 즉시 0~5%로 하락 → CPU 자체는 정상 회복 가능함
⚠️ 해석: CPU는 부하 처리 중 완전히 포화 상태였음 → 추가 요청이 있었다면 병목 가능성 높음. 또한, 고사양 작업(GC, SQL, 다중 연산) 등 CPU 연산비용이 높은 작업이 집중됐을 가능성 큼
5.2.4. Heap Used (GC 사이클)
Load Test와 동일
5.2.5. GC Count 그래프
- 평균 2~4회 수준으로 매우 빈번한 Minor GC 발생
- GC도 CPU와 마찬가지로 부하 종료 후 0으로 안정화
⚠️ 해석: GC 횟수가 과도함 → 처리량에는 큰 영향 없지만 지속되면 Throughput 저하 및 Stop-the-world delay 위험 → Heap 크기 재조정 또는 G1GC와 같은 GC 전략 고려 필요
5.2.6. Active Service (실시간 처리 중 요청 수)
- 최고 약 200개의 동시 요청 처리 상태
- 일정 시간 동안 완전히 flat하게 유지됨 → 서버가 200개까지 동시 처리 가능했음을 의미
- 부하 종료와 함께 정상 감소
✅ 해석: 서버가 동시에 200개 요청을 버티는 성능 확보됨. 이는 Thread Pool 또는 Servlet container의 처리 능력 기준선 수립에 도움 됨
5.2.7. Memory (System 전체 메모리 사용률)
- 50% 내외로 완전히 평탄한 메모리 사용
- Spike Test에도 메모리 부족이나 누수 없음
✅ 해석: 시스템 메모리는 여유 있음. 병목 없음. 오히려 GC가 너무 자주 발생하는 쪽이 이슈.
5.2.6. XLog 분석 (응답시간이 늦은 개별 트랜잭션 상세)
XLog 1: GET /api/v1/notices
- 총 응답시간: 8.7초
- 동일 SELECT 반복:
user_id from user_notice_read ...
형태 20회 이상 - N+1 쿼리 명백함
- 유저별 읽음 여부를 개별 쿼리로 조회
✅ 개선: JOIN FETCH 또는 EXISTS, IN 절로 변경
XLog 2: GET /api/v1/news
- 총 응답시간: 7.4초
- 단일 SELECT, FETCH만 1회 실행
- 하지만 응답 자체는 지연됨
- 가능성:
- DB 커넥션 풀 대기
- GC or Thread Queue 대기 (병렬 상황에서 지연)
✅ 개선: 이 트랜잭션 단독 이슈는 없으나, 전체 시스템 자원 점검 필요
XLog 3: GET /api/v1/notices/{id}
- 총 응답시간: 6.8초
- DB write 쿼리 1회 (
UPDATE view_count
) - 이어지는
SELECT
,INSERT
,FETCH
반복 → 여러 테이블 조인 및 서브 쿼리
✅ 개선: view_count는 Redis와 같은 In-Memory로 처리 후 비동기 업데이트 고려
XLog 4: GET /api/v1/news/{id}
- 총 응답시간: 7.0초
- update + 4개 SELECT → 그 중 한 SELECT는 6,357ms
- 해당 쿼리:
select * from news_like where user_id = ? and news_id = ?
추정: news_like에 index 누락 또는 서브쿼리 복잡도 높음
✅ 개선: news_like에 (user_id, news_id) 복합 인덱스 확인 필요
XLog 5: GET /api/v1/notices/{id}/comments
- 총 응답시간: 6.8초
- 20회 이상 반복 SELECT (유저 정보 추정)
- N+1 + 각각 개별 180ms 정도 → 총합 지연 유발
✅ 개선: 댓글-유저 JOIN 최적화 + DTO projection
XLog 6: GET /api/v1/news/{id}/comments
- 총 응답시간: 6.4초
- 20회 반복 SELECT (user 정보 또는 like 여부 추정)
- 단일 쿼리는 1~3ms → 로직 반복 구조 문제
✅ 개선: JOIN FETCH, MapStruct DTO, @BatchSize 등 적용
6. 결론 및 개선 방향
본 테스트를 통해 Smoke / Baseline / Load / Spike / Integrity Test 전 영역에 대해 체계적인 검증을 수행하였고, 아래와 같은 핵심 인사이트를 도출할 수 있었습니다.
6.1. 주요 진단 결과 요약
구분 | 주요 관찰 사항 |
---|---|
전반적인 시스템 안정성 | 단건 API는 대부분 응답시간 100ms 이내로 빠르고, 오류율도 0%로 매우 안정적 |
복합 시나리오 성능 | 평균 응답 시간 6~10초 이상으로 사용자 체감 성능 저하 발생 |
부하 대응 한계 | Spike Test에서 TPS 100 이상까지 처리 가능했으나, CPU 100% 포화 상태 도달 |
GC 사이클 | 지나치게 자주 발생하며, Throughput 저하 가능성 존재 |
데이터 정합성 | 댓글/좋아요/조회수 Integrity Test 모두 무결성 통과 → 동시성 처리 문제 없음 |
병목 구간 | News/Notice의 댓글 조회, 좋아요 여부 확인 등에서 N+1 쿼리 및 반복 SELECT 구조 확인됨 |
오류 발생 지점 | POST /chatbots 등 일부 요청에서 SLA 초과 및 오류율 1.7% 이상 발생 |
6.2. 개선 방향 (기술별 제안)
6.2.1. 쿼리 최적화 및 N+1 해소
- 문제: 대부분의 성능 병목은 단일 SELECT가 아닌 반복 SELECT → N+1 쿼리
- 조치 방안:
JOIN FETCH
,@BatchSize
,EntityGraph
, DTO Projection 사용- QueryDSL 또는 Native Query로 비즈니스 쿼리 단순화
IN (...)
서브쿼리 활용하여 한 번의 쿼리로 처리
6.2.2. 캐싱 전략 적용
- 대상 API:
GET /notices
,GET /news
,GET /comments
→ 조회 반복 많고 실시간성 필요 낮음
- 조치 방안:
- Spring Cache, Redis, 또는 CDN 레벨에서 캐시 적용
@Cacheable
,@CacheEvict
, TTL 설계- 공지/뉴스 ID 기준 캐시 키 구성
6.2.3. 조회수, 좋아요 수 처리 방식 개선
- 현재: 실시간 DB
UPDATE
→ 잠재적인 DB Lock 및 지연 유발 - 개선안:
- Redis
INCR
,SET
로 일시 저장 후 주기적 배치 저장 - Kafka/Queue 기반 비동기 처리
- 조회수/좋아요는 Eventually Consistent 구조로 충분함
- Redis
6.2.4. GC / 메모리 튜닝
- 현상: Heap GC가 10초 이하 주기로 지속 발생 → Minor GC 과다
- 조치 방안:
- JVM GC 설정 변경 (G1GC 또는 ZGC)
- Heap Size 조정:
Xms
,Xmx
크기 조율 - 객체 할당 패턴 점검: 불필요한 객체 생성 제거
6.2.5. 서버 설정 및 ThreadPool 튜닝
- Tomcat 스레드 수, Connection Pool 설정 점검
maxThreads
,acceptCount
,maxConnections
확대- HikariCP 설정 점검:
maximumPoolSize
,minimumIdle
,connectionTimeout
등
6.2.6. 시스템 보호: Rate Limiting, Circuit Breaker
- Spring Cloud Gateway or Nginx Rate Limit 적용
- 특정 API 요청 과도시 임계치 제어
- Netflix Hystrix / Resilience4j로 실패 격리 전략 도입