'터닝 페어웰' 서비스의 아키텍처는 완성되었지만, 이것이 실제 트래픽을 감당할 수 있다는 보장은 없었습니다. 약 1,500명의 사용자가 동시에 몰릴 것을 대비해, 시스템의 한계를 명확히 파악하고 병목을 제거하는 과정이 반드시 필요했습니다.
이번 포스트에서는 부하 테스트 도구 k6
와 APM(Application Performance Monitoring) 솔루션 Datadog
을 활용하여, 데이터에 기반해 문제를 진단하고 시스템을 단계적으로 개선해나간 여정을 공유합니다.
개선을 시작하기 전, 추측이 아닌 데이터로 시스템을 바라보기 위해 객관적인 측정 환경을 구축하는 것이 가장 중요하다고 생각했습니다. 이 과정에서 어떤 도구를 선택할지 신중하게 고민했습니다.
부하 테스트 도구: nGrinder
vs k6
처음에는 국내에서 많이 사용되는 nGrinder를 고려했습니다. 강력한 기능과 UI가 장점이지만, 컨트롤러와 에이전트를 별도로 설치하고 관리해야 하는 등 초기 설정에 다소 시간이 소요됩니다. 이번 프로젝트는 빠르게 진행해야 했기에, 별도의 EC2 인스턴스에 간단히 설치하여 스크립트 기반으로 즉시 테스트를 실행할 수 있는 k6를 최종적으로 선택했습니다. EC2를 테스트 전용 머신으로 활용하면 더 빠르고 유연하게 원하는 부하를 생성할 수 있을 것이라 판단했습니다.
모니터링 도구: Prometheus
& Grafana
vs Datadog
오픈소스 조합인 Prometheus와 Grafana는 강력하고 확장성이 뛰어나지만, 직접 서버를 구축하고 메트릭 수집기(Exporter)를 일일이 설정해야 하는 번거로움이 있습니다. 저는 서버의 상태(Metrics), 코드 레벨의 동작(APM Traces), 그리고 로그(Logs)를 한곳에서 통합적으로 보고 싶었고, 이러한 Observability 환경을 가장 빠르고 편리하게 구축해 주는 SaaS 솔루션인 Datadog을 경험해보고 싶은 마음이 컸습니다. Datadog Agent 설치만으로 이 모든 것을 해결할 수 있다는 점이 매력적이었습니다.
k6
를 설치하여 부하를 발생시키고, 운영 서버에는 Datadog
Agent를 설치하여 성능을 모니터링했습니다. 이를 통해 테스트 자체가 서버 성능에 영향을 주는 것을 방지했습니다.모든 준비를 마치고 테스트를 시작했지만, 첫 결과는 처참했습니다. 이제부터 데이터와 로그들 가지고 시스템 속 병목을 찾아 나섰습니다.
t3.micro
의 명백한 한계가장 기본적인 t3.micro
인스턴스에서 진행한 첫 테스트부터 시스템은 비명을 질렀습니다.
k6 결과:
분석: 1,500명의 요청 중 152건이 실패했고, 응답 시간은 목표치를 훨씬 초과했습니다. 시스템이 동시 요청을 전혀 감당하지 못하고 있었습니다. 진짜 문제 해결의 시작이었습니다.
가장 먼저 의심한 것은 서버의 물리적인 한계였습니다. Datadog
대시보드는 CPU 사용량이 100% 에 머무르며 더 이상 처리하지 못하는 상황을 명확히 보여주었습니다.
개선: EC2 인스턴스 사양을 t3.micro
에서 t3.small
로 스케일업(Scale-Up)했습니다.
재테스트 결과:
검증: 요청 실패는 사라졌지만, 응답 시간은 오히려 크게 늘어났습니다. 이 현상은 매우 중요한 단서라고 생각했습니다. 하드웨어라는 급한 불을 끄자, 그 속애 숨어있던 애플리케이션 레벨의 더 큰 병목이 보였습니다. 서버는 이제 죽지 않고 모든 요청을 처리하느라 더 오랜 시간이 걸리게 된 것입니다.
지표 (1500 VUs 기준) | 이전 (t3.micro ) | 개선 후 (t3.small ) | 분석 |
---|---|---|---|
http_req_failed | 84건 실패 (5.6%) | 0건 실패 (0%) | (안정성 확보) 모든 요청을 처리하기 시작. |
http_req_duration (avg) | 2.58초 | 10.42초 | (성능 저하) 애플리케이션 병목으로 인해 처리 시간 증가. |
애플리케이션 성능 저하의 가장 흔한 원인은 데이터베이스입니다.
DB가 아니라면, 남은 병목 지점은 외부 API 호출, 특히 시간이 오래 걸리는 이메일 발송 로직이었습니다. 초기 테스트에서 대량의 이메일을 보내다 보니 Gmail SMTP 서버로부터 454 Throttling failure
에러를 받으며 전송이 차단되는 현상을 겪었습니다.
개선 1: 이메일 서비스 교체 (PR #93):
application-prod.yml
의 SMTP 호스트 정보를 변경하여 간단히 적용할 수 있었습니다.개선 2: 완전한 비동기 처리 도입 (PR #125):
javaMailSender.send()
는 동기적으로 동작하여 외부 SMTP 서버의 응답을 기다리는 동안 Kafka Consumer 스레드를 장시간 붙잡아두는(Blocking) 근본적인 문제가 있었습니다.@Async
를 적용했습니다. 이메일 발송 전용 스레드 풀(ThreadPoolTaskExecutor
)을 설정하고, EmailService
의 발송 메소드에 @Async
를 붙여 느린 I/O 작업을 메인 처리 흐름에서 완전히 분리했습니다.재테스트 결과:
검증: 엄청난 성능 향상이었습니다! 느린 I/O 작업을 비동기로 전환한 것이 핵심이었습니다. 선착순에 성공한 사용자들은 0.26초 만에 응답을 받으며 목표치를 달성했습니다. 하지만 전체 평균 응답 시간은 여전히 길었고, 이는 시스템 내부에 또 다른 병목이 존재함을 의미했습니다.
외부 서비스 연동 문제를 해결하자 응답 시간이 6.2초까지 단축되었지만, 여전히 목표치와는 거리가 멀었습니다. "병목은 이동한다"는 말처럼, 하나의 문제를 해결하자 시스템의 부하는 다음 단계로 옮겨갔습니다. 이제 문제는 애플리케이션의 동시 처리 능력 자체로 좁혀졌습니다. Datadog
의 APM 데이터를 깊게 분석하며 시스템의 파이프라인을 단계적으로 확장하는 작업을 진행했습니다.
개선 1: 웹 서버 스레드 풀 확장 - 더 많은 손님 받기 (PR #116)
Datadog
에서는 특이점이 없었지만, k6
테스트 결과에서 EOF
(End of File) 에러가 간헐적으로 발생했습니다. 이는 "전화가 갑자기 끊겼다"는 의미로, 손님(요청)이 가게(서버)에 들어오기도 전에 문전박대 당하는 상황이었습니다. 내장 웹 서버인 Tomcat이 처리할 수 있는 동시 요청(기본 200개)을 초과한 것이 원인이었습니다.application.yml
에 server.tomcat.threads.max
설정을 추가하여 동시에 처리 가능한 스레드 수를 1600개로 대폭 늘렸습니다. 이는 가게의 입구를 넓혀 1,500명의 손님이 모두 들어올 수 있도록 보장하는 것과 같았습니다.개선 2: Kafka Consumer 증설 - 계산대 늘리기 (PR #119)
spring.kafka.listener.concurrency
를 10으로 설정하여, 10개의 Consumer 스레드가 메시지를 병렬로 처리하도록 변경했습니다. 이론적으로 처리 속도가 10배 빨라지는 효과를 기대했습니다.개선 3: DB 커넥션 풀 확장 - 계산원의 손 늘리기 (PR #122)
maximum-pool-size
)를 늘려 여러 Consumer가 동시에 DB 작업을 원활히 수행할 수 있도록 길을 열어주었습니다.모든 파이프라인 튜닝을 마친 후, 부하 규모를 조절하며 마지막 테스트를 진행했습니다.
최종 결과 (동시 사용자 300명):
결론: 마침내 목표했던 'p95 응답 시간 1초 이내'를 달성했습니다. 수많은 가설과 검증 끝에, 시스템은 이제 동시 사용자 300명까지는 매우 안정적으로 처리할 수 있는 상태가 되었습니다. 약 500명부터는 응답 시간이 1초를 넘어가기 시작하며 현재 시스템의 명확한 한계점 또한 데이터로 확인할 수 있었습니다.
현재 시스템은 1,500명의 목표에는 도달하지 못했지만, 300명의 동시 접속을 1초 내로 처리할 수 있는 안정적인 상태가 되었습니다. 여기서 더 많은 트래픽을 처리하기 위한 선택지는 두 가지입니다.
t3.small
)을 m7i-flex.large
와 같이 더 강력한 인스턴스로 교체하는 것입니다.언뜻 보기에 사용한 만큼만 비용을 내는 스케일아웃이 항상 더 경제적인 선택처럼 보일 수 있습니다. 실제로 장기적으로 운영되는 서비스라면 그것이 정답에 가깝다고 생각합니다.
하지만 '터닝 페어웰'은 단 한 번 진행되는 단기 이벤트입니다. 이 상황에서 "비용"은 단순히 서버 사용료만을 의미하지 않습니다. 로드 밸런서와 오토 스케일링 그룹을 구축하는 데 들어가는 개발자의 시간과 노력 역시 중요한 비용이라고 생각하였습니다.
이 프로젝트의 목적과 기간을 고려했을 때, 복잡한 스케일아웃 아키텍처를 구축하는 데 드는 시간과 노력이 단기간의 서버 비용 절감 효과보다 훨씬 크다고 판단했습니다.
따라서 가장 합리적인 선택은, 현재 최적화된 아키텍처를 유지하되 이벤트가 진행되는 짧은 시간 동안만 일시적으로 스케일업하여 대응하는 것입니다. 이는 총 소유 비용(TCO) 관점에서 가장 효율적인 결정이라고 생각합니다.
이번 성능 개선 여정을 통해 무조건적인 인프라 확장이 정답이 아니라는 것을 배웠습니다. 시스템의 한계를 데이터로 명확히 파악하고, 현재 상황과 비용, 목적에 맞는 최적의 해결책을 찾아나가는 과정이야말로 진정한 엔지니어링의 영역임을 깨달을 수 있었습니다.