Home API만 따로 떼서 테스트해봤습니다\
4개 API를 동시에 돌리면 CPU가 버티질 못하니, 일단 가장 무겁다고 판단한 /home API만 단독으로 테스트해보기로 했습니다. 부하는 100명에서 시작해서 최대 600명까지 단계적으로 올렸습니다.
결과가 나왔는데, 뭔가 이상했습니다.

MED: 21ms
AVG: 712ms
P95: 2428ms
MED와 AVG 사이의 격차가 너무 컸습니다. 절반의 요청은 21ms 안에 처리되는데, 평균은 712ms, P95는 2428ms입니다. 이건 대부분의 요청은 빠른데 일부 요청만 2~3초씩 걸리고 있다는 뜻입니다.
원인은 둘 중 하나라고 판단했습니다. 캐시 TTL이 만료되는 순간 Thundering Herd가 터지거나, 배치가 실행되면서 HikariCP 커넥션을 가져가는 순간이거나. 그리고 한 가지 의심이 더 들었습니다. 혹시 로컬 환경이라 CPU 자원을 k6와 Spring이 같이 쓰면서 결과가 왜곡되는 건 아닐까?
그래서 AWS에 서버를 직접 올리기로 했습니다. Spring, DB, Redis, 모니터링 서버를 각각 분리하고, 로컬에서 k6만 돌려 부하를 주는 구조로 바꿨습니다. 각 서버는 2코어 CPU로 구성하였습니다.
EC2에 올렸더니 에러율이 오히려 높아졌습니다
배포 후 동일한 테스트를 돌렸습니다.

| 로컬 | EC2 | |
| MED | 21ms | 11ms |
| P95 | 2428ms | 744ms |
| 에러율 | 3% | 11% |
P95는 2428ms에서 744ms로 크게 줄었습니다. 서버를 분리한 효과가 분명히 있었습니다. 그런데 에러율이 3%에서 11%로 오히려 나빠졌습니다.
네트워크 레이턴시가 추가됐으니 성능이 조금 나빠지는 건 자연스럽습니다. 그런데 에러율이 이렇게 튀는 건 납득이 안 됐습니다. 뭔가 다른 문제가 있다는 뜻이었습니다.

로그를 뒤지고 설정을 하나씩 확인하다가 결국 원인을 찾았습니다.
Redis 커넥션 풀이 실제로 동작하지 않고 있었습니다.
spring-boot-starter-data-redis는 기본으로 Lettuce를 사용하는데, Lettuce가 커넥션 풀링을 위해 commons-pool2 의존성을 필요로 합니다. 그런데 Spring Boot가 이걸 자동으로 포함하지 않기 때문에 직접 추가해야 합니다. yml에 Redis Pool 설정을 아무리 열심히 작성해도, 이 의존성이 없으면 설정 자체가 무시되고 풀 관리가 전혀 되지 않습니다.
implementation 'org.apache.commons:commons-pool2'
단 한 줄이 빠져있었던 겁니다. 의존성을 추가하고, 다시 테스트를 돌렸습니다.
P95 2.04ms — 목표를 훨씬 넘어섰습니다

목표는 P95 300ms 이하였는데, 결과는 2.04ms였습니다. 에러율은 0%였습니다.
솔직히 이 결과를 보고 잠깐 멍했습니다. 그동안 수십 번 테스트를 돌리면서 계속 벽에 부딪혔는데, 결국 문제는 의존성 하나와 어노테이션 하나였습니다. 거창한 아키텍처 변경이 아니라, 설정이 제대로 적용되고 있는지 확인하는 것이 중요하다는 걸 다시 한번 느꼈습니다.
너무 신이 나서 바로 나머지 API도 테스트를 돌렸습니다.
이때 너무 신나서 다른 api 도 부하테스트를 시도했습니다
나머지 API 결과
Favorite API

Announce API

Checklist API

인덱스 중복을 발견했습니다
성능 개선을 마무리하면서 EXPLAIN으로 각 쿼리가 인덱스를 제대로 타는지 확인하던 중, 한 가지 실수를 발견하였습니다
document 테이블의 announce_id 인덱스가 두 개였습니다.
idx_document_announce_id ← 이미 있던 것
idx_doc_announce ← 제가 추가한 것 (중복)
이미 존재하는 인덱스를 확인하지 않고 새로 추가한 것이었습니다. 인덱스는 조회 성능을 높이지만 INSERT, UPDATE, DELETE 시에는 인덱스도 함께 갱신해야 하므로 중복 인덱스는 오히려 쓰기 성능에 부담이 됩니다. 중복된 인덱스는 즉시 제거하였습니다.


쿼리가 인덱스를 정상적으로 타고 있는 것과 벌크 INSERT도 의도대로 동작하는 것까지 확인하였습니다.
explain으로 혹시나 벌크 연산이 잘 수행 되고 있는지도 보니깐
2026-04-21T21:24:56.922+09:00 DEBUG 15764 --- [nio-8080-exec-4] org.hibernate.SQL : delete mdc1_0 from member_document_check mdc1_0 join document d1_0 on d1_0.document_id=mdc1_0.document_id where mdc1_0.member_id=? and d1_0.announce_id=?
2026-04-21T21:24:56.939+09:00 DEBUG 15764 --- [nio-8080-exec-4] org.hibernate.SQL : INSERT INTO member_document_check (member_id, document_id, checked) SELECT ?, d.document_id, true FROM document d WHERE d.document_id IN (?,?,?,?,?)
잘 되고 있었습니다!!!!
4개 API 동시 최종 테스트
모든 개선이 끝난 뒤 다시 4개 API를 동시에 부하테스트를 돌렸습니다.

처음 테스트에서 서버가 터지고 오류율 99.96%를 기록했던 것과 비교하면 완전히 다른 서버가 되었습니다. 오류율 99.96%에서 시작해서, 수많은 테스트와 삽질을 거쳐 목표를 달성하기까지의 여정이었습니다.
중간에 포기하고 싶었던 순간도 있었지만, 결국 한 줄의 의존성과 어노테이션 하나가 마지막 열쇠였다는 게 아직도 조금 허탈하면서도 재밌습니다. 그래도 그 과정에서 HikariCP와 Tomcat Thread의 관계, Redis 커넥션 풀 동작 방식, 락 범위와 동시성의 관계까지 직접 부딪히며 배울 수 있었습니다.
| 지표 | 최초 | 최종 | 개선율 |
| home P95 | 2865 ms | 3.35 ms | 99.9% 개선 |
| checklist P95 | 2773 ms | 26.49 ms | 99.0% 개선 |
| announce P95 | 2877 ms | 10.08 ms | 99.6% 개선 |
| favorite P95 | 2748 ms | 7.07 ms | 99.7% 개선 |
| 실패 건수 | 5754건 | 19건 | 99.7% 감소 |

'🖥️ 컴퓨터 공부 > 부하테스트 & 성능최적화' 카테고리의 다른 글
| 해커톤 성능 최적화 — 전체 정리 (0) | 2026.04.27 |
|---|---|
| 해커톤 성능 최적화 -3 (0) | 2026.04.24 |
| 해커톤 성능 최적화 -2 (0) | 2026.04.24 |
| 해커톤 성능 최적화 - 1 (0) | 2026.04.24 |
| k6 와 그라파나로 성능최적화 도전 (0) | 2026.04.24 |