WebFlux 전환 부하 테스트를 다시 쓴 이야기
2026년 8월 21일
원문에서 보기 ↗글. 김민승(Miles) / 유저혜택개발팀

안녕하세요. 여기어때 유저혜택개발팀 마일스입니다.
"리액티브로 바꿨더니 4배 빨라졌습니다."
부하 테스트 보고서 초안에 호기롭게 적었던 문장입니다. 하지만 이 보고서는 이후 몇 번이고 고쳐 쓰여야 했습니다.
결국 남은 최종 결론은 처음과 완전히 달랐습니다. '4배'라는 숫자의 대부분은 리액티브 아키텍처의 우위가 아니라, 우리가 만든 설정 결함과 측정 환경이 만든 착시였습니다. 심지어 API 특성에 따라서는 기존 블로킹 방식이 더 빠르기도 했습니다.
저희 팀은 이벤트 쿠폰 API를 기존 event-api에서 reactive-api로 이관하는 작업을 진행했습니다. 앞은 Tomcat과 JPA 위에서 요청 하나가 스레드 하나를 붙잡는 전통적인 구조고, 뒤는 Netty와 R2DBC 위에서 스레드를 붙잡지 않고 이벤트 루프가 돌립니다. 선착순 트래픽이 몰리는 핵심 도메인인 만큼, 전환의 당위성을 객관적인 숫자로 검증해야 했습니다.
이 글은 단순히 프레임워크를 교체한 후기가 아닙니다. "얼마나 빨라지나"를 증명하려다 스스로의 측정을 의심하게 된 기록이자, 결국 "얼마나 빨라지나"라는 질문 자체가 잘못되었다는 결론에 도달한 과정입니다.
1. 처음 잰 숫자 : 4~4.7배
첫 측정 대상은 쿠폰 발급 가능 여부를 확인하는 조회 API였습니다. 정책 10개를 Redis에서 읽어 판정하는 비교적 가벼운 경로입니다. 부하 도구는 nGrinder를 로컬 노트북에서 실행해 측정했습니다.

"논블로킹이 블로킹보다 훨씬 빠르다"는 가설에 정확히 부합하는 결과였습니다. 망설임 없이 이 수치를 보고서에 담았습니다.
하지만 지금 돌이켜보면 이 결과에는 분명 이상한 점이 있었습니다. reactive TPS를 보면 Vuser를 3배 넘게 늘려도 248에서 225로 오히려 살짝 줄어들 뿐, 전혀 늘지 않았습니다.
부하를 올리는데 처리량이 그대로라면, 서버가 이미 포화 상태이거나 실제 부하가 더 들어가지 않고 있거나 둘 중 하나입니다.
2. 교란요인 탐색
2--1. 부하 도구와 서버가 같은 CPU를 두고 싸우고 있었다
원인은 후자였습니다. 문제는 서버가 아니라 제 실험 환경에 있었습니다.
당시 저는 노트북 한 대에 모든 것을 띄워두고 있었습니다. 부하 생성기, 측정 대상인 두 애플리케이션 서버, Redis와 MySQL까지 전부 같은 CPU 코어를 나눠 쓰는 상태였습니다. Vuser를 올리면 서버만 바빠지는 게 아니라 부하 생성기도 같이 바빠집니다. 둘이 같은 코어를 두고 경쟁하고 있었던 겁니다.
문제는 하나 더 있었습니다. ngrinder/agent Docker 이미지는 amd64만 제공합니다. arm64 장비에서는 에뮬레이션으로 돌 수밖에 없었고, 같은 CPU 연산을 arm64와 비교해보니 약 1.8배 느렸습니다.
자원이 빠듯한 노트북에서 이 1.8배는 단순히 부하 도구가 느리다는 뜻이 아닙니다. 요청 하나를 만들어 보내는 데 CPU를 1.8배 더 쓴다는 뜻이고, 그만큼 서버 몫을 가져갔다는 뜻입니다. Vuser를 올릴수록 생성기가 코어를 점유하고 서버가 쓸 몫은 줄어듭니다.
TPS가 늘지 않고 평탄하던 그래프의 정체가 이것이었습니다. 서버가 한계에 도달한 게 아니라, 서버와 부하 생성기가 같은 CPU를 나눠 먹느라 전체 처리량이 갇혀 있었던 겁니다.
부하 테스트 도구 벤더들이 항상 경고하는 함정이기도 합니다. k6 공식 문서도 부하 생성기의 CPU 사용률을 80% 이내로 유지하라고 명시합니다. 기본을 놓친 채 서버 탓만 하고 있었던 셈입니다.
그래서 부하 도구를 k6로 교체했습니다. k6는 arm64를 공식 배포하므로 에뮬레이션 계층이 사라집니다. 여기에 더해, nGrinder가 Vuser마다 스레드를 할당하는 JVM 기반인 것과 달리 k6는 Go 런타임에서 VU를 goroutine으로 다룹니다. 같은 동시성을 만드는 데 드는 자원이 훨씬 적다는 뜻입니다. 에뮬레이션 비용과 실행 모델 양쪽에서 생성기가 가벼워졌고, 그만큼 서버가 쓸 CPU를 돌려받았습니다.

2--2. 측정 대상이 요청마다 로그를 찍고 있었다
다음은 작지만 실체적인 문제였습니다. 기존 event-api는 요청과 응답 본문 전체를 매 요청마다 로그로 남기고 있었습니다. 요청 하나당 수 KB의 문자열이 메모리에 만들어졌다 바로 버려지고 있었습니다.
로그를 끄고 다시 재보니 TPS(별도 조건에서 측정)가 59 → 76(1.3배)으로 올랐습니다. 다만 이 시점에는 뒤에 나올 더 큰 병목이 아직 남아 있었습니다. 그 병목을 걷어내고 다시 재면 로깅 비용은 이보다 훨씬 크게 드러납니다.
2--3. 커넥션 검증 옵션 하나가 9.2배
그리고 마침내 가장 결정적인 주범을 찾았습니다. event-api가 의존하는 공통 도메인 모듈의 Redis 설정이었습니다.
// domain-module / RedisConfig.java
factory.setValidateConnection(true);
이 옵션을 false로 바꾸자 TPS가 76에서 700으로 뛰었습니다. 옵션 하나에 9.2배의 격차가 있었던 겁니다.
원인은 두 가지가 겹친 것이었습니다.
LettuceConnectionFactory는 기본적으로 네이티브 커넥션 하나를 모든 요청이 공유합니다. 여기에 validateConnection=true가 켜져 있으면 Redis 연산을 수행할 때마다 커넥션이 살아 있는지 PING으로 확인하는데, 이 검증이 동기화 블록 안에서 일어납니다. 모든 스레드가 같은 락 하나를 통과해야 Redis에 접근할 수 있다는 뜻입니다.
문제는 그 락 안에서 하는 일이 계산이 아니라 네트워크 왕복이라는 점입니다. 락을 쥔 스레드가 PING 응답을 기다리는 동안 나머지 스레드는 전부 멈춰 있습니다. 결국 전체 처리량의 상한이 PING 왕복 시간의 역수로 묶입니다. 왕복이 2ms라면 스레드를 1,600개 두든 3,000개 두든 상한은 초당 500건입니다. 스레드를 아무리 늘려도 이 값은 변하지 않습니다.
여기에 왕복 횟수 자체도 늘어납니다. 실제 트레이스를 확인해보니 모든 Redis 연산 앞에 PING이 붙어 있었습니다.

정책 10개를 판정하는 동안 필요한 왕복이 두 배가 된 셈입니다. 응답 시간의 절반 정도가 검증 PING과 락 대기에 쓰이고 있었습니다.
옵션을 끄자 직렬화가 사라지고 왕복 횟수도 절반이 됐습니다. 커넥션이 끊기면 Lettuce가 자동 재연결하는 것이 기본 동작이라, 이 검증을 꺼도 단절 복구는 라이브러리가 처리합니다. 9.2배는 이 둘이 함께 만든 숫자입니다.
2--4. 그래서 1장의 4배는 무엇이었나
event-api에 validateConnection=false를 적용하자 700 TPS가 나왔습니다. 1장에서 리액티브가 이겼다며 기록했던 248 TPS를 세 배 가까이 뛰어넘는 수치입니다.
도구와 환경이 달라진 비교지만, '4배'라는 판정이 무너진 것만은 분명합니다.
처음에 기록한 '4배의 성능 향상'은 아키텍처의 우위가 아니었습니다. 블로킹 쪽은 모래주머니를 차고 뛰고 있었고, 그 사실을 모른 채 저는 그들의 기록을 재고 있었던 것입니다.
3. 조건을 맞추고 다시 재니, event가 이겼다
측정 조건들을 정비하여 운영 환경에 가깝게 다시 세팅했습니다.
- 두 애플리케이션 모두 Docker --- cpus=2 -m 4g로 격리 (CPU throttle까지 실제 운영 환경 재현)
- 부하 도구(k6)에는 호스트의 남은 코어를 몰아주어 CPU 경쟁 해소
- toxiproxy를 통해 Redis 네트워크 지연 1ms 주입 (원격 Redis 모사)
- Tomcat 스레드 1,600개 설정, 양쪽 모두 validateConnection=false, 로깅 OFF
- 각 조건은 Vuser당 60초씩 두 앱을 순차 측정(동시 구동 배제)하고, 런 중 12초 간격으로 서버 CPU·스레드 수를 샘플링해 기록

Vuser 60에서는 reactive가 655 대 451로 앞섰지만, 200을 넘어서면서 뒤집혔습니다. Vuser 1,000에서 event 941, reactive 698. 이후 Vuser 5,000까지 올려도 다시 교차하지 않았고, 양쪽 에러는 0%였습니다.
이 조회 워크로드의 병목은 스레드나 커넥션 수 부족이 아닌 CPU 2코어의 한계였습니다. 조회 API는 Redis만 가볍게 조회하는 요청이라 요청당 I/O 대기가 있긴 해도, 스레드가 충분해서 그 대기가 처리량의 상한을 정하지 못합니다.
이런 조건에서는 연산자마다 시그널을 전파하고 컨텍스트를 유지해야 하는 Reactor 파이프라인의 오버헤드가 그대로 처리량 손실로 이어지는 것으로 해석됩니다. 반면 event-api는 충분한 스레드와 커넥션 큐 덕분에 Vuser 5,000의 동시 요청도 무리 없이 처리했습니다. 스레드가 고갈되기 전에 CPU가 먼저 포화 상태에 도달했기 때문입니다.
논블로킹은 '절대적인 처리 속도'를 올려주는 기술이 아니라, '적은 스레드로 많은 커넥션을 효율적으로 다루는' 기술입니다. CPU 바운드에 가까운 간단한 요청이고 스레드가 충분하다면, 오버헤드가 적은 블로킹 모델이 처리량 관점에서 더 효율적일 수 있습니다.
3--1. 단, 이 비교는 '로그를 껐을 때' 이야기입니다
결론부터 말하자면 이 절에서 나올 격차는 아키텍처가 아니라 로깅 컨벤션에서 옵니다. 순위가 뒤집히긴 하지만, 그게 리액티브로 갈 이유가 되지는 않습니다.
앞서 2--2에서 다뤘던 로깅 이슈를 다시 가져오겠습니다. 위 실험은 양쪽 모두 로그를 끈 조건이었습니다. 이번에는 로그 레벨만 바꿔가며 다시 측정했습니다.

event-api는 운영과 같은 INFO 레벨에서 처리량의 42%를 잃었습니다. log.info와 요청/응답을 기록하는 AOP가 매 요청마다 실행되기 때문입니다. reactive-api는 로깅 컨벤션이 DEBUG 위주라 INFO에서는 페널티가 없었습니다.
2--2에서 1.3배로 보였던 로깅 비용이 여기서 42%로 커졌습니다. 로깅이 무거워진 게 아니라, 그때는 커넥션 검증 락이 처리량을 정하고 있어서 로그를 꺼도 아낀 시간 대부분이 락 대기로 흡수되고 일부만 드러났을 뿐입니다. 병목이 CPU로 옮겨간 뒤에야 로깅이 쓰던 비용이 그대로 드러났습니다.
다만 이 42%는 부하 테스트 환경 기준입니다. 측정 당시 프로파일이 local이라 동기 appender를 썼고, 운영은 비동기라 실제 손실은 이보다 작습니다. 그래도 방향은 바뀌지 않습니다. 로그를 켜는 순간 event-api는 처리량을 잃고 reactive-api는 잃지 않습니다.
여기서 짚어야 할 것은 이 차이가 아키텍처에서 오지 않는다는 점입니다. 한쪽은 매 요청 INFO로 기록하고 다른 쪽은 DEBUG로 두었을 뿐, 블로킹이라서 느린 것도 논블로킹이라서 빠른 것도 아닙니다. 로깅 컨벤션을 맞추면 사라지는 격차입니다.
그래서 "순수 조회 처리량은 event-api가 우세하다"는 결론은 로그를 껐을 때만 성립하는 조건부 사실이고, 로깅까지 반영하면 뒤집힐 수 있습니다. 다만 이 역전은 리액티브 전환의 근거가 되지 못합니다. 프레임워크를 바꾸지 않아도 해결되는 문제니까요.
4. 하지만 발급 경로는 결과가 정반대였습니다

동일한 두 서버를 대상으로 위 그림의 쿠폰 발급 경로를 부하 테스트했습니다. 요청 1건당 외부 API 호출 2회 + Redis Lua 스크립트를 통한 재고 차감 + MySQL INSERT + Kafka 이벤트 발행을 모두 처리하는, 조회와는 비교할 수 없이 무거운 I/O 경로입니다.
- 조건: 2코어 / 4GB, 지연 주입(외부 API +10~20ms, Redis +2ms), validateConnection=false, Tomcat 스레드는 1,600(상용 설정)과 200 두 가지로 나눠 측정

상용 설정인 스레드 1,600은 Vuser 1,000에서 에러율이 15%, 3,000에서 21%까지 올라 비교 자체가 안 됐습니다. Vuser 3,000의 257 TPS가 200 조건(226)보다 높아 보이지만 에러를 빼면 203이라 오히려 낮습니다.
그래서 이후 event 수치는 전부 스레드 200 조건입니다. event 쪽에 더 유리한 조건을 준 셈이고, 뒤에 나올 5배도 거기서 나온 값입니다.
넘어가기 전에 하나 확인할 게 있습니다. 스레드를 8배로 늘렸는데 얻은 것이 없었다는 점입니다. Vuser 1,000의 59 TPS는 응답시간(16.7초) 대비 측정 창(60초)이 짧아 표에서 가장 신뢰도가 낮은 값이지만, 이 셀을 빼고 봐도 결론은 같습니다. Vuser 3,000 기준 성공 TPS는 203 대 226으로 이득이 없고, 에러 15~21%와 p99 44초는 1,600 쪽에서만 나왔습니다. CPU는 양쪽 똑같이 포화였고(런 중 12초 간격 docker stats 샘플링 기준, 2코어 상한인 200% 안팎), Hikari 커넥션 타임아웃과 OOM도 0건이었습니다. 같은 CPU로 에러와 꼬리만 나빠졌다는 건, 그 CPU가 일이 아니라 컨텍스트 스위칭·락 경합·GC 압박 같은 오버헤드에 쓰였다는 뜻입니다. 3장에서 조회 경로의 병목이 CPU였던 것과 같은 얘기입니다.
발급은 reactive가 처리량 약 5배였고, 꼬리도 같은 방향이었습니다(Vuser 1,000 p99 기준 event 7.3초 vs reactive 1.7초, 스레드 1,600 조건은 44초). event는 210 TPS 근처에서 천장에 닿았고, reactive는 900~1,060 구간을 유지했습니다.
여기서 하나를 더 확인해봤습니다. 외부 API 지연 때문이라면 지연을 0ms로 낮추면 격차가 줄어야 하지 않을까.
줄지 않았습니다. Vuser 1,000에서 event 225, reactive 1,196으로 5.3배가 그대로였습니다. 대기 시간이 아니라 요청당 쓰는 CPU가 다른 겁니다. JPA 영속성 컨텍스트, .block()으로 감싼 WebClient, 동기 Redis 클라이언트가 먹는 CPU와 R2DBC, 논블로킹 클라이언트, Lua 스크립트가 먹는 CPU의 차이입니다.
다만 이 측정에는 흠이 둘 있습니다. 하나는 검증했습니다. event-api가 공통 모듈에 하드코딩된 hibernate.show_sql 때문에 매 쿼리마다 SQL 로그를 안고 뛰고 있었는데, 측정에서만 끄고 다시 재보니 10~15% 오를 뿐 격차는 4.5~5배로 남았습니다. 다른 하나는 검증하지 못했습니다. 두 앱은 재고 차감 방식 같은 세부 구현이 완전히 동일하지 않아서, 3--1의 로깅처럼 프레임워크를 바꾸지 않아도 좁힐 수 있는 몫이 이 5배 안에 섞여 있을 수 있습니다. 다만 발급 트랜잭션이 외부 API 호출까지 감싸지는 않는 구조입니다.
같은 서버 같은 환경인데 조회는 event가, 발급은 reactive가 5배 앞섰습니다. 리액티브가 무조건 빠른 게 아니라, 요청당 밟는 I/O 경로가 무겁고 동시성이 높을수록 논블로킹의 자원 효율이 드러난다고 하는 게 맞겠습니다.
5. 전환의 진짜 명분은 속도가 아니었다
마지막으로 외부 장애 상황을 모사했습니다. Redis에 50ms의 지연을 의도적으로 주입하고 조회 경로를 다시 측정했습니다.

부하가 높아질수록 배수는 줄어듭니다. 하지만 마지막 줄에서 봐야 할 건 배수가 아닙니다.
동시 처리 건수 = 처리량 × 응답시간으로 역산해보면, Vuser 2,000에서 event-api는 396 × 4.76초, 약 1,885건을 동시에 물고 있었습니다. Tomcat 워커 스레드는 1,600개뿐인데 말이죠. 최소 285건은 스레드를 못 받고 대기 중이었다는 뜻입니다. Vuser 1,000에서는 342 × 2.75초로 941건이라 아직 여유가 있었으니, 대기가 시작된 건 그 사이 어딘가입니다.
reactive에 같은 계산을 하면 648 × 3.06초, 약 1,983건입니다. 동시에 처리 중인 요청은 오히려 더 많은데, 논블로킹에서는 요청 하나가 스레드 하나를 붙잡지 않으니 스레드 수가 이 값을 따라갈 이유가 없습니다.
측정한 범위에서는 두 쪽 다 죽지 않았습니다. 다만 event는 동시 요청이 스레드 수를 넘어서면서 대기가 쌓이기 시작했고, reactive는 그런 한계선 자체가 없었습니다. 이 대기가 운영에서 타임아웃과 Health Check 실패로 이어질 수 있습니다.
타임아웃과 서킷브레이커로 막을 문제 아니냐고 할 수 있습니다. 절반은 맞습니다. 다만 서킷브레이커의 수단은 차단, 즉 빠른 실패인데, 이 조회 API의 의존처는 우회할 수 없는 핵심 데이터 소스(정책이 담긴 Redis)라 fail-fast가 곧 기능 정지입니다. 타임아웃을 걸어도 기다리는 동안 스레드가 묶이는 것은 그대로고요. 결국 블로킹에 회복 장치를 더하면 "죽는 대신 거절하는" 시스템이 되고, 논블로킹은 "느려지더라도 계속 응답하는" 시스템이었습니다. 저희가 원한 건 후자였습니다. 물론 논블로킹도 한계를 넘으면 쌓인 요청이 메모리를 채우는 식으로 무너집니다.
event-api가 지연에 약했던 건 정책 10개를 순차로 조회했기 때문입니다. 정책 하나를 판정하는 데도 Redis 왕복이 여러 번 필요하고, 그게 10개 순차로 쌓여 요청당 2.7초가 됐습니다.
블로킹에서 병렬화가 불가능한 건 아닙니다. CompletableFuture로 10건을 워커 풀에 던지면 응답 시간은 비슷하게 줄어듭니다. 다만 요청 하나가 스레드 10개를 잡습니다. 아홉 개는 워커로, 마지막 하나는 요청 스레드가 직접 처리하는 식으로요. 한 스레드가 2.7초를 기다리든 열 스레드가 0.27초씩 기다리든, 스레드가 묶여 있는 시간의 총합은 같습니다. 지연은 줄어도 처리량 천장은 그대로고, 고갈 시점만 앞당깁니다.
스레드를 더 쓰지 않고도 병렬화할 수 있다는 것, 이게 논블로킹의 강점입니다. reactive-api는 정책 조회 10건을 flatMapSequential로 fan-out합니다. 그래서 응답 시간이 10개의 합이 아니라 가장 느린 하나로 수렴합니다. 단건 응답이 0.22초, 순차로 처리한 event-api(2.7초)의 12분의 1이었습니다. 물론 Redis 파이프라이닝으로 왕복을 묶으면 블로킹에서도 이 격차의 상당 부분을 좁힐 수 있습니다. 12배 전부가 모델의 몫은 아닙니다.

평상시 처리량 경쟁은 서버를 늘려서 어느 정도 덮을 수 있습니다. 하지만 다운스트림이 느려졌을 때 스레드가 말라 서비스 전체가 죽느냐, 적은 스레드로 버티며 느려지기만 하느냐는 다른 문제입니다.
저희가 WebFlux 전환을 최종적으로 정당화할 수 있었던 근거는 속도가 아니라 회복력이었습니다. 정확히 말하면, 장애 시의 성능 저하가 스레드라는 하드 리밋에 갇히지 않는 구조였습니다.
덧붙이면, 이 회복력은 가상 스레드(Virtual Threads)로도 얻을 수 있습니다. 요청이 플랫폼 스레드를 붙잡지 않는다는 점은 같으니까요. 다만 event-api는 Java 11 + Spring Boot 2.7이라 가상 스레드를 쓰려면 어차피 런타임과 프레임워크의 전면 업그레이드가 필요했고, 저희에게는 이미 Java 21로 분리 운영 중인 reactive-api라는 자산이 있었습니다. 백지에서 시작한다면 MVC + 가상 스레드도 유력한 답입니다.
끝으로 이 측정의 적용 범위를 적어둡니다. 가장 확실한 수치는 같은 앱에서 조건 하나만 바꾼 비교입니다. validateConnection의 9.2배, 로깅의 42%, 스레드 200 대 1,600이 여기 속하고, 재실행에서도 수 퍼센트 이내로 재현됐습니다. 반면 event와 reactive 사이의 비교는 스택 세대(Java 11 + Spring Boot 2.7 vs Java 21 + Spring Boot 3.4)와 구현 차이가 섞여 있어, 방향은 믿되 배수는 폭으로 읽는 편이 안전합니다. 그리고 절대값은 이 환경에서만 유효합니다. 노트북 한 대에서의 60초 단회 측정이고, 발급은 재고 소진 전의 성공 경로만 쟀습니다. 이 숫자들은 "이 환경에서의 비교"이지 "상용 예측치"가 아닙니다.
6. 남기고 싶은 이야기
부하 테스트를 하며 찾아낸 교란 요인들이 결국 부하 테스트 체크리스트가 됐습니다. 그리고 대부분은 리액티브로 전환하지 않고도 고칠 수 있는 것들이었습니다.
부하 생성기와 측정 대상을 같은 머신에서 돌리지 마세요. 부하를 올리는데 처리량이 고정된다면, 서버보다 생성기 쪽 자원이 포화됐는지 먼저 봐야 합니다. 한 머신에서 돌리고 있다면 그 숫자는 서버 성능이 아니라 머신 전체 자원의 한계값일 뿐입니다. 분리가 어렵다면 생성기의 CPU 사용률이라도 같이 기록해두세요.
공유 모듈의 인프라 설정을 다시 보세요. 커넥션 검증 옵션 하나가 9.2배의 병목을 만들고 있었습니다. 또한 hibernate.show_sql이 코드에 박혀 있었습니다. SQL 로그 자체는 운영 진단에 필요해서 없앨 대상이 아닙니다. 문제는 방식입니다. show_sql은 로깅 프레임워크를 거치지 않고 표준 출력에 직접 쓰기 때문에, 레벨로 끌 수도, 비동기 Appender로 비용을 낮출 수도, 환경별로 조절할 수도 없습니다. 같은 로그를 org.hibernate.SQL 로거 설정으로 옮기면 셋 다 가능해집니다. 필요한 로그였지만, 비용을 조절할 수 없는 자리에 박혀 있었던 겁니다. 프레임워크를 바꾸기 전에, 지금 스택이 제 성능을 내고 있는지부터 확인해야 합니다.
로깅 정책도 성능 평가 항목입니다. 로그 레벨만이 아니라 Appender 종류와 큐 설정까지가 변수입니다. 같은 INFO라도 동기인지 비동기인지에 따라 비용이 완전히 다르고, 비동기라도 큐가 차면 결국 요청 스레드가 멈춥니다. 그리고 root 레벨을 내려도 설정 파일에 이름이 박힌 로거는 덮이지 않습니다. 저희도 다 껐다고 믿었지만 Hibernate SQL 로그는 그대로 나오고 있었습니다.
스레드 수는 무조건 많을수록 좋은 값이 아닙니다. 상용 설정이던 1,600은 2코어에서 이득 없이 에러와 꼬리 지연만 키웠습니다. 전환과 무관하게, 작은 파드에 불필요하게 큰 스레드 풀이 남아 있지 않은지 재검토가 필요합니다.
질문은 그 다음입니다. 이 경로가 정말 I/O-bound인가. 지금 필요한 것이 처리량인가, 장애 상황에서의 회복력인가.
두 시스템을 비교할 때 대칭을 맞춰야 할 축은 셋입니다. 부하 생성기와 서버의 분리, 로깅 레벨과 Appender, 공유 모듈의 인프라 설정. 저는 셋을 다 놓쳤고, 그게 모여 '4배'라는 환상이 됐습니다.
교과서와 딱 맞아떨어지는 그래프가 나왔다면, 십중팔구 교과서에 맞도록 실험 환경을 짜놓았기 때문입니다.
긴 글 읽어주셔서 감사합니다.
WebFlux 전환 부하 테스트를 다시 쓴 이야기 was originally published in 여기어때 기술블로그 on Medium, where people are continuing the conversation by highlighting and responding to this story.