여기어때 유저혜택개발팀이 이벤트 쿠폰 API를 Tomcat/JPA 기반 blocking 방식에서 WebFlux/R2DBC 기반 reactive 방식으로 전환하며 수행한 부하 테스트 기록이다. 초기에는 reactive가 4배 빠르다는 결론이 나왔지만, 측정 환경의 교란 요인을 하나씩 제거하자 결론이 완전히 뒤집혔다.
핵심 포인트- 부하 생성기와 서버를 같은 머신에서 돌리면 CPU를 공유해 실제 서버 성능이 아니라 머신 전체 한계를 재는 것이다.
- Redis validateConnection=true 설정 하나가 9.2배의 처리량 차이를 만들었다 — 모든 스레드가 PING 왕복을 기다리며 직렬화됐기 때문이다.
- INFO 레벨 동기 AOP 로깅은 처리량의 42%를 소비했다.
- 교란 요인을 제거하고 나니 순수 조회 경로(Redis만 사용, CPU-bound)에서는 blocking(event-api)이 더 빨랐다.
- I/O-heavy 발급 경로(외부 API 2회+Redis+MySQL+Kafka)에서는 reactive가 약 5배 앞섰다.
- 전환의 최종 근거는 처리량이 아니라 장애 시 회복력이었다 — 다운스트림 지연 시 스레드 고갈 없이 계속 응답하는 구조.
상세 정리- 최초 측정 오류: nGrinder를 arm64 노트북에서 amd64 에뮬레이션으로 실행해 부하 생성기가 1.8배 더 많은 CPU를 썼고, 서버와 생성기가 CPU를 공유해 Vuser를 올려도 TPS가 고정됐다.
- 1차 수정: 부하 도구를 k6(arm64 네이티브, Go goroutine 기반)로 교체해 생성기 자원 소비를 크게 줄였다.
- Redis validateConnection=true 함정: LettuceConnectionFactory의 공유 커넥션에서 매 Redis 연산 전 PING 검증이 동기화 블록 안에서 실행돼, 처리량 상한이 PING 왕복 시간의 역수로 묶였다. 설정을 false로 바꾸자 76 → 700 TPS(9.2배) 향상.
- 로깅 42% 손실: event-api는 매 요청마다 AOP로 요청/응답 본문을 INFO로 기록했다. 동기 Appender라 처리량의 42%를 소비했고, reactive-api는 DEBUG 위주라 영향이 없었다.
- 조회 경로 최종 비교: 조건을 통일(docker cpus=2, toxiproxy 1ms 주입, validateConnection=false, 로그 OFF) 후 Vuser 1,000 이상에서 blocking이 앞섰다. Redis 단순 조회는 CPU-bound이고 Reactor 파이프라인 오버헤드가 처리량 손실로 이어졌다.
- 발급 경로 역전: I/O-heavy 경로에서 Vuser 1,000 기준 event-api 약 200 TPS vs reactive-api 약 1,000 TPS(약 5배). 정책 10개 조회를 flatMapSequential로 fan-out해 응답 시간이 순차 합산이 아닌 최대값으로 수렴됐다.
- 스레드 수 역효과: Tomcat 스레드 1,600은 2코어 환경에서 에러율 15~21%, p99 44초를 만들었고, 200으로 줄이자 에러 0%에 처리량도 동등 이상이었다.
- 외부 장애 시나리오: Redis 50ms 지연 주입 시 event-api는 동시 처리 건이 스레드 수를 초과해 대기가 쌓였고, reactive-api는 스레드 한계선 자체가 없어 느려지되 죽지 않았다.
- 최종 결론: 단순 처리량은 워크로드 특성에 따라 갈리며, WebFlux 전환의 정당화 근거는 장애 시 스레드 고갈 없이 계속 응답하는 구조였다.
왜 읽나WebFlux/리액티브 전환을 검토 중인 백엔드 엔지니어, 또는 부하 테스트 결과를 믿기 전 교란 요인을 점검해야 하는 모든 개발자에게 실전 체크리스트를 제공한다.