[K6] 대용량 엑셀 다운로드 API 부하 테스트 - XSSF vs SXSSF 성능 비교 - 2편 (feat. AWS 배포 환경 재검증)
목차
1. 테스트 배경
2. 테스트 환경
3. 테스트 시나리오
4. 결과(로컬XSSF, 로컬 SXSSF, AWS XSSF, AWS SXSSF)
5. Grafana 시각화(API 부하테스트)
6. JVM 힙 제한 후 재테스트(AWS XSSF, AWS SXSSF)
1. 테스트 배경
이전 포스팅에서 로컬 환경 기준으로 XSSF vs SXSSF 부하 테스트를 진행했으나, 로컬 메모리(16GB+)가 충분하여 XSSF OOM을 재현하지 못했음. 또한 예상과 달리 SXSSF가 더 느린 결과가 나왔음.
이번 포스팅에서는 실제 운영 환경에 가까운 AWS EC2(t2.small, 2GB RAM)에 배포하여 동일 시나리오로 재검증함. 추가로 K6 결과를 InfluxDB에 저장하고 Grafana로 시각화하는 모니터링 환경도 구축함.
참고:
[K6] 대용량 엑셀 다운로드 API 부하 테스트 - XSSF vs SXSSF 성능 비교 - 1편
[K6] 대용량 엑셀 다운로드 API 부하 테스트 - XSSF vs SXSSF 성능 비교 - 1편목차1. 테스트 배경2. 테스트 환경3. 테스트 시나리오4. 결과 — XSSF (레거시)5. 결과 — SXSSF (개선)6. 비교 분석7. 한계 및 다음
lhk9311.tistory.com
2. 테스트 환경
| 항목 | 로컬 환경 | AWS 환경 |
| 부하 도구 | K6 v1.7.1 | K6 v1.7.1 (로컬에서 EC2로 쏨) |
| 서버 | Spring Boot, 로컬 (port 9081) | AWS EC2 t2.small (2GB RAM) |
| DB | Oracle XE, 예약 데이터 10,112건 | Oracle XE, 예약 데이터 10,112건 |
| 모니터링 | 없음 | InfluxDB 1.8 + Grafana (Docker) |
| 테스트 대상 API | GET /admin/reservation/excel | GET /final_hotel/admin/reservation/excel |

3. 테스트 시나리오
export const options = {
stages: [
{ duration: '1m', target: 3 }, // 3명까지 ramp-up
{ duration: '3m', target: 3 }, // 3명 유지
{ duration: '1m', target: 10 }, // 10명까지 올리기
{ duration: '2m', target: 10 }, // 10명 유지
{ duration: '1m', target: 0 }, // 종료
],
thresholds: {
http_req_duration: ['p(95)<10000'], // p95 10초 이내
http_req_failed: ['rate<0.05'], // 실패율 5% 미만
},
};
VU(가상 사용자)를 3명 → 10명으로 단계적으로 올려서 동시 접속 상황을 재현함.
※ VU(Virtual User) = 동시에 요청을 날리는 가상 사용자 수 -------> 동일한 상황 가정
4. 결과(로컬 XSSF, 로컬 SXSSF, AWS XSSF, AWS SXSSF)
■ AWS XSSF (응답시간 단축됨)

※ 로컬(236ms)보다 AWS(89ms)가 더 빠른 이유 — 로컬은 IDE, Docker 등 다른 프로세스가 함께 돌아가서 리소스를 나눠 쓰기 때문으로 추정됨. AWS는 Spring Boot만 전담으로 실행 중. (정확한 원인은 cpu 사용량 측정 등 추가 모니터링이 필요함.)
■ AWS SXSSF(응답시간 마찬가지로 단축됨)

■ 비교
| 항목 | 로컬 XSSF | 로컬 SXSSF | AWS XSSF | AWS SXSSF |
| 평균 응답시간 | 236ms ✅ | 38.79s ❌ | 89ms ✅ | 16.63s ❌ |
| p95 응답시간 | 501ms ✅ | 46.29s ❌ | 131ms ✅ | 33.31s ❌ |
| 에러율 | 0% ✅ | 0% ✅ | 0% ✅ | 0% ✅ |
| 총 요청 처리수 | 10,615건 | 68건 | 2,317건 | 149건 |
| 메모리 안정성 | oom 위험 ( ⚠️ ) | 안전 | oom 위험 ( ⚠️ ) | 안전 |
| 임계값 통과 | ✅ | ❌ | ✅ | ❌ |
GitHub main 브랜치는 XSSF 방식, 개인 브랜치(hankyung)는 SXSSF 방식으로 관리되어 있음. 각 브랜치를 AWS EC2에 배포한 후 동일한 K6 시나리오로 부하 테스트를 진행했음.
SXSSF로 리팩토링한 이유는 XSSF 방식의 경우 동시 사용자가 몰릴 때 전체 데이터를 힙에 올리는 구조상 OOM이 발생할 수 있기 때문이었음. 즉, 속도보다 서버 안정성을 우선한 선택이었음.
그러나 t2.small + 동시 10명 조건에서는 XSSF도 OOM 없이 정상 동작했음. 이는 테스트 조건이 OOM을 재현하기에 충분하지 않았기 때문으로 판단됨.
이를 검증하기 위해 JVM 힙을 -Xmx64m으로 강제 제한하고 VU를 50명으로 늘린 조건에서 재테스트를 진행했으며, 그 결과는 6번 섹션에서 확인할 수 있음.
+ 로컬이나 AWS환경이나 둘 다 리팩토링 전 인 XSSF 방식이 응답시간이 빠름.
+ 추가적으로 sxssf의 경우 응답시간이 느린 이유를 이전 포스팅에 적어뒀으나 한번 더 정리하자면
■ 왜 SXSSF가 느렸는가?
SXSSF는 메모리 사용량을 줄이기 위해 데이터를 스트리밍 방식으로 처리한다.
이 과정에서:
1. 페이지 단위 DB 조회가 반복 발생
2. 임시 파일 flush I/O 발생
3. 단일 조회 기반 XSSF보다 DB 왕복 횟수가 증가
결과적으로 메모리 안정성은 확보했지만 응답시간은 크게 증가했다.
5. Grafana 시각화 (k6 부하테스트)
K6 실행 시 --out influxdb 옵션으로 결과를 InfluxDB에 저장하고, Grafana Explore에서 시각화함.
k6 run --out influxdb=http://[EC2_IP]:8086/k6 excel_test.js
■ vus 그래프

vu가 3명 -> 10명 단계적으로 증가함.(k6 시나리오 대로 가상사용자가 단계적으로 증가했음을 확인)
■ http_req_duration 그래프

XSSF의 경우 응답시간이 거의 0에 가깝고 SXSSF의 경우 35K(35초)로 나타남.
※ 시각화를 통해 확인한 것
- vu가 3명 -> 10명으로 늘어날수록 sxssf의 응답시간이 급격히 증가함. (db 왕복 11회 누적이 영향인 것으로 보임.)
- xssf는 동시 사용자가 늘어도 응답시간이 거의 일정하게 유지됨. (단일 db 조회라 영향 적음)
- 즉, 동시 사용자가 늘어날수록 sxssf 응답시간은 느려짐 ...
상기 시각화 결과는 HTTP 응답시간 기반 부하 테스트 결과임. 서버 내부 메트릭(CPU, JVM 힙 메모리 사용량 등)은 별도 모니터링 도구(Actuator + Prometheus + Grafana)가 필요하며, 이번 테스트에서는 구축하지 않음. 따라서 XSSF의 실제 메모리 사용량 증가는 이번 테스트에서 수치로 확인하지 못함.
일단 하기부터는 테스트 조건 변경 후 다시 aws 배포한 후에 k6부하테스트 한번 더 진행해보겠음.
6. JVM 힙 제한 후 재테스트(AWS XSSF, AWS SXSSF)
■ 테스트 조건
| 항목 | 값 |
| JVM 힙 제한 | -Xmx64m (docker-compose.yml 환경변수로 설정) |
| 서버 구성 | spring-app-1 단독 (로드밸런싱 제거) |
| VU | 50명 |
| 테스트 시간 | 7분 |
※ JVM(Java Virtual Machine) 힙 제한은 docker-compose.yml의 environment 항목에 JAVA_TOOL_OPTIONS=-Xmx64m을 추가하는 방식으로 설정함. 이 환경변수는 JVM 시작 시 자동으로 읽혀 힙 메모리 상한을 64MB로 제한함.
Spring Boot가 JVM 위에서 실행되어 있고, 힙(Heap)메모리는 JVM이 객체를 저장하는 공간. XSSF의 경우 10,112건 조회시 데이터가 전부 힙에 올라감.
※ -Xmx64m :
Xmx = 힙 최대 크기 설정 64m = 64MB, 즉 힙을 최대 64MB까지만 쓰도록 제한함. 64MB 꽉 차면 OOM 발생.
■ XSSF

■ SXSSF

■ 결과 비교
| 지표 | XSSF | SXSSF |
| 평균 응답시간 | 2.22s | 39.05s |
| p95 응답시간 | 14.29s | 59.99s |
| 에러율 | 2.52% | 89.33% |
| 총 요청 수 | 3,365건 | 272건 |
| 200 OK | 97% | 10% |
| OOM 발생 | ✅ 서버 다운 + 재시작 | ✅ 간헐적 발생, 서버 생존 |
※ 200 OK : HTTP 응답코드. 서버가 요청을 정상적으로 처리하면 200 리턴함.
- XSSF 97% = 3,365건 중 97%는 정상 응답
- SXSSF 10% = 272건 중 10%만 정상 응답, 나머지 90%는 응답 못 받음
SXSSF 에러율이 89%인 이유
- 요청 1개 처리하는 데 평균 39초가 걸리는데, k6 스크립트에 timeout: '60s' 설정되어 있어서, 50명 동시에 요청하면
50명 × 39초 = 서버가 동시에 처리해야 할 부하가 몰림
→ 대기 중인 요청들이 60초 넘기 전에 응답 못 받음 → timeout 발생 → 에러로 처리
메모리 부족이 아니라 응답시간이 느려서 에러가 났다고 보면 됨. (89% 요청이 엑셀 다운로드 실패한 것임....) 결국 xssf의 경우 에러율은 2.52%지만 서버가 죽었기 때문에 다운 -> 재시작 (10~15초 동안 서비스 중단) -> 트래픽 계속 오면 -> 다운 -> 재시작 -> 다운 -> 재시작 ....
반면 sxssf는 서버가 살아있기 때문에, 트래픽이 줄어들면 정상적으로 엑셀 다운로드가 되고 다른 api(예약 등)는 영향 없이 정상 동작하게 되는 것임...(서버가 죽지 않는 결과!)
■ XSSF - 서버 다운 확인

동시 요청 50명이 각각 별도 스레드에서 10,112건 전체를 힙에 올리려다 연쇄적으로 OOM 발생. Started FinalProjectApplication 직후 OutOfMemoryError가 터지며 핵심 스레드(Acceptor, Catalina-utility)가 죽음. restart: unless-stopped 설정으로 자동 재시작됐지만 재시작 중 서비스 중단이 발생함.
■ SXSSF — 서버 생존 확인

OOM이 간헐적으로 발생하지만 페이징 처리가 계속 진행됨.

테스트 종료 후에도 spring-app-1 Up 20 minutes — 서버가 살아있음.

※ 결론
SXSSF도 힙 64MB + VU 50명 극한 조건에서는 OOM이 발생함. 그러나 XSSF와 결정적인 차이가 있음.
- XSSF = OOM 발생 시 서버 다운 → 재시작 (서비스 중단)
- SXSSF = OOM 간헐적 발생해도 서버 생존, 페이징 처리 계속
이것으로 SXSSF 리팩토링의 목적 달성을 확인할 수 있었음. 속도는 느리지만 고부하 상황에서 서버가 죽지 않음.
- 추후 개선(보완) 계획
SXSSF로 안정성은 확보했으나, 속도를 개선할 필요가 있음.
1) DB 인덱스 추가
현재 페이징 쿼리는 created_at, reservation_status 컬럼에 인덱스가 없어 매 페이지 조회 시 Full Scan이 발생하는 것으로 추정됨. 두 컬럼에 복합 인덱스를 추가하면 페이징 쿼리 속도 개선이 가능하며, 11번의 db왕복 시간을 단축할 수 있음.
2) HikariCP 커넥션 풀 튜닝
현재 maximum-pool-size 기본값(10)으로 설정되어 있어 동시 요청 50명이 몰릴 경우 DB 커넥션 대기가 발생할 수 있음. maximum-pool-size를 적절히 늘려 DB 연결 대기시간을 단축할 예정임.
3) 비동기 처리 도입
엑셀 생성을 백그라운드에서 처리하고 완료 시 다운로드 링크를 제공하는 방식으로 전환하면, 사용자가 응답을 기다리지 않아도 되므로 체감 응답시간이 대폭 개선될 것으로 예상됨. @Async + 임시 파일 저장 방식으로 구현 예정.
위 세 가지 개선 후 동일 조건(힙 64MB + VU 50명)으로 재테스트하여 SXSSF 응답시간이 얼마나 개선되는지 검증할 예정임.
'4. 모니터링' 카테고리의 다른 글
| [K6] 대용량 엑셀 다운로드 API 부하 테스트 - XSSF vs SXSSF 성능 비교 - 1편 (1) | 2026.04.30 |
|---|