대규모 마케팅 선착순 이벤트 오픈 전날 밤, 텅 빈 서버에 혼자 조용히 접속해 결제 버튼을 누르며 "오, 응답 속도가 0.1초네! 서버 개발 진짜 완벽하게 잘했다!"라고 자화자찬하는 주니어 백엔드 개발자의 안도감은, 다음 날 오전 10시 정각 이벤트 오픈 5분 만에 지옥의 비명으로 바뀝니다. 선착순 쿠폰을 받기 위해 1만 명의 유저가 동시에 접속하여 광클을 시작하는 순간, 스프링 부트 내장 톰캣(Tomcat)의 쓰레드 풀(Thread Pool)이 단 1초 만에 꽉 차버리고, DB 커넥션 풀(HikariCP)이 고갈되며, 결국 화면에는 "502 Bad Gateway" 혹은 "504 Gateway Timeout"이라는 처참한 메시지와 함께 서버가 통째로 기절해 버리기 때문입니다.
실무 백엔드 엔지니어의 진짜 실력은 깔끔한 코드를 짜는 능력이 아니라, "내 서버가 초당 정확히 몇 명의 동시 결제(TPS)를 뻗지 않고 버텨낼 수 있는가?"를 객관적인 수치(Metric)로 정확히 증명해 내고 보장하는 능력입니다. 서비스 오픈 전에 가상의 유저 1만 명, 10만 명을 강제로 생성하여 서버를 향해 집중 포화를 퍼붓고, 어느 구간(WAS, DB, 네트워크, 외부 API)에서 먼저 병목(Bottleneck)이 터지는지 사전에 색출해 내는 극한의 모의고사!
이번 54단계 포스팅에서는 글로벌 백엔드 성능 테스트의 양대 산맥인 JMeter와 네이버(Naver)가 개발한 nGrinder를 활용하여 서버의 영혼까지 끌어다 쓰는 극한의 부하 테스트(Load Test) 방법론과 APM을 활용한 병목 추적술에 대해 상세히 알아보겠습니다.

1. 성능 테스트의 3가지 종류: Load, Stress, Spike
부하를 주는 방식에 따라 테스트의 목적이 완전히 달라집니다. "테스트 한번 돌려봐"라는 지시를 받았을 때 이 세 가지를 구분하지 못하면 엉뚱한 결론을 도출하게 됩니다.

- Load Testing (부하 테스트): 시스템이 '평상시'에 예상되는 정상적인 최대 트래픽(예: 동시 접속자 1,000명)을 던졌을 때, 서버가 목표 응답 시간(예: 200ms) 내에 안정적으로 데이터를 반환하는지를 검증합니다.
- Stress Testing (스트레스 테스트): 서버가 감당할 수 있는 수준을 한참 넘어서는 미친 트래픽(예: 동시 접속자 50,000명)을 쏟아부어 서버를 고의로 완전히 다운시켜 버립니다. 이를 통해 우리 서버의 물리적 최대 한계치(임계점)가 어디인지 파악하고, 서버가 죽기 직전에 어떤 기괴한 동작을 보이는지(OOM, Deadlock) 관찰합니다.
- Spike Testing (스파이크 테스트): 선착순 티켓팅, 푸시 알람 발송 직후 등 아주 짧은 1~2초의 순간에 평소 트래픽의 100배가 몰려드는 상황을 시뮬레이션합니다. 오토 스케일링(Auto Scaling)이 트래픽 급증을 따라갈 수 있는지 검증합니다.
2. 병목(Bottleneck) 추적의 원리: 꺾이는 지점을 찾아라
부하 테스트의 핵심은 단순히 "만 명 들어오면 어떻게 되나 보자!" 하고 한방에 트래픽을 때려 붓는 것이 아닙니다. Ramp-up(점진적 증가)이라는 기법을 통해 유저를 10명, 50명, 100명, 500명으로 계단식으로 서서히 올려가며 서버의 비명을 관찰해야 합니다.
VUser를 늘릴수록 초반에는 내 서버의 TPS(초당 처리량)도 아주 예쁘게 비례해서 쭉쭉 올라갑니다. 하지만 어느 순간 VUser는 1,000명으로 늘었는데, TPS는 300에서 더 이상 오르지 않고 천장에 부딪히며(Saturation), 사용자의 응답 시간(Latency)만 0.1초에서 10초, 20초로 수직 상승하는 끔찍한 구간이 발생합니다. 이곳이 바로 당신 서버의 '임계점(한계 용량)'입니다. 이 지점을 뚫어내기 위해서는 단순히 서버 RAM을 돈으로 늘리는 것(Scale-up)이 아니라, 내부 애플리케이션의 설정과 쿼리를 뜯어고쳐야 합니다.
3. 무엇이 TPS의 발목을 잡는가? (3대 병목 원인과 튜닝)
임계점에 도달했을 때, 성능 테스트 툴 하나만 보고 있으면 원인을 절대 찾을 수 없습니다. 반드시 APM(Application Performance Management, 예: Pinpoint, Scouter, Datadog)을 스프링 부트에 연동해 놓고, 병목이 터진 그 순간 어떤 메서드가 시간을 잡아먹고 있는지 X-Ray를 찍듯 들여다보아야 합니다.
① 톰캣 쓰레드 풀(Tomcat Thread Pool) 고갈
스프링 부트의 기본 내장 톰캣 쓰레드(일꾼) 수는 200개입니다. 만약 당신이 짠 결제 로직이 외부 PG사 API를 RestTemplate으로 호출하느라 일꾼 한 명이 3초 동안 멍때리며 응답을 기다리고 있다면, 금세 200개의 일꾼이 모두 잠겨버리고 201번째 유저부터는 접속이 거절(Connection Refused)됩니다. 이때는 application.yml에서 server.tomcat.threads.max를 500개로 튜닝하거나, 83단계에서 배울 WebFlux(비동기 논블로킹) 또는 50단계의 Java 21 가상 쓰레드(Virtual Threads)를 도입하여 쓰레드 점유 문제를 원천적으로 부숴야 합니다.
② HikariCP DB 커넥션 풀 고갈과 튜닝 공식
WAS(톰캣)가 일꾼을 500명으로 늘렸는데, DB로 가는 연결 통로(커넥션 풀)가 기본값인 10개뿐이라면? 490명의 일꾼은 DB 앞에서 문을 두드리며 커넥션이 반납될 때까지 무한정 락(Lock) 대기 상태에 빠집니다(Connection Timeout 예외 발생). 트래픽에 맞춰 spring.datasource.hikari.maximum-pool-size를 늘려줘야 합니다. 하지만 무작정 100개, 200개로 늘리면 DB 인스턴스의 메모리가 터져버립니다. PostgreSQL 공식 위키에서 제안하는 최적의 Connection Pool 개수 공식은 (Core 수 * 2) + 유효 디스크 수 입니다. 즉, 무턱대고 늘리기보다 쿼리 하나의 실행 시간을 극단적으로 줄이는 것이 정답입니다.
③ Slow Query (가장 흔한 범인: 인덱스 부재와 N+1)
톰캣 쓰레드도 넉넉하고 커넥션 풀도 넉넉한데 TPS가 안 나옵니다. 이때 APM이나 DB 모니터링 툴(CloudWatch)을 보면 DB의 CPU 점유율이 100%를 찍고 불타고 있습니다. 십중팔구 당신이 짠 조회 쿼리에 인덱스(Index)가 안 걸려 있어서 DB 엔진이 1,000만 건의 데이터를 매번 무식하게 풀스캔(Table Full Scan)하고 있거나, 지난 시간에 배운 JPA N+1 문제가 터져서 루프 한 번에 쿼리가 수백 개씩 난사되고 있는 것입니다. EXPLAIN 명령어로 실행 계획을 분석하고 쿼리를 갈아엎어야 합니다.
정리하며
이번 54단계 포스팅에서는 오픈 전 개발자가 반드시 거쳐야 할 필수 모의고사, JMeter와 nGrinder를 이용해 가상 유저 1만 명을 쏟아붓고 TPS와 Latency의 임계점을 찾아내어 톰캣 쓰레드 풀과 DB 커넥션 풀의 병목을 부숴버리는 성능 테스트 튜닝의 예술을 뼈 때리게 파헤쳐 보았습니다. 백엔드 개발자의 진짜 가치는 입으로 떠드는 아키텍처가 아니라, 모니터링 대시보드에 찍히는 압도적인 TPS 수치로 증명됩니다. 자, 이렇게 성능 테스트까지 완벽하게 해서 시스템을 튼튼하게 만들어 두었습니다. 그런데 만약 운영 중에 누군가 실수로 IDC 센터 랜선을 뽑아버리거나, 의존하고 있던 외부 결제 서버가 5분 동안 다운되면 우리 서버는 어떻게 될까요? 완벽한 시스템은 장애가 '안 나는' 시스템이 아니라, 장애가 났을 때 연쇄 붕괴하지 않고 스스로 회복(Resilience)하는 시스템입니다. 멀쩡한 서버를 고의로 고장 내는 극악무도한 군사 훈련! 다음 55단계(Phase 11의 마지막) 포스팅에서는 "카오스 엔지니어링 (Chaos Engineering for Spring Boot): 프로덕션 환경에 원숭이를 풀어 고의로 장애를 유발하고 서킷 브레이커 회복력 검증하기"에 대해 아주 치밀하게 딥다이브 해보겠습니다!