MongoDB CQRS 성능 개선기: 예상 밖의 Tomcat NDJSON 병목 해결
MongoDB CQRS로 조회는 **40배** 빨라졌지만, 엑셀 다운로드는 Tomcat **NDJSON** 병목에 막혔다.
네이버페이 정산 시스템은 가맹점 기반 샤딩 DB를 쓰고 있었지만, 상위 대형 가맹점에 데이터가 집중되면서 조회 타임아웃이 잦아졌다. 이를 해결하기 위해 CQRS 패턴과 MongoDB를 도입했고, API 조회 성능은 기존 샤딩 DB 대비 40배 이상 향상됐다.
하지만 엑셀 다운로드는 기대만큼 빨라지지 않았다. MongoDB 튜닝과 엑셀 라이브러리 교체를 해도 효과가 없었고, 원인은 DB나 라이브러리가 아니라 Tomcat(Spring Web Embedded) 이 Flux 응답을 NDJSON으로 스트리밍할 때 발생하는 반복적인 blocking 이었다.
문제 구간을 하나씩 분해해 확인한 결과는 다음과 같았다.
- MongoDB 조회:
readConcern조정과projection적용으로 조회 성능은 약 10% 개선됐지만, 엑셀 생성 전체 시간에는 큰 영향이 없었다. - 엑셀 라이브러리: POI SXSSF, EasyExcel, FastExcel을 비교했고, FastExcel은 약 2배 수준의 생성 성능 개선을 보였다. 그러나 전체 병목은 여전히 남아 있었다.
- 네트워크: 8만 건 규모의 결과 파일 전송은 약 1초 내외로 끝나 네트워크는 문제가 아니었다.
남은 구간인 Spring Web을 테스트하던 중, 일반 JSON 배열 응답과 NDJSON 응답의 처리 속도 차이가 크게 벌어지는 것을 확인했다. curl 기준으로 JSON은 37.7MB/s, NDJSON은 3.7MB/s 수준이었고, 구조적으로는 한 줄을 직렬화한 뒤 write & flush 하고, 개행까지 또 write & flush 하는 방식이라 줄 수만큼 blocking 이 반복됐다.
핵심은 NDJSON 응답에서 JsonEmitterSubscriber가 Flux의 각 item을 건별로 처리하고, Tomcat의 ResponseBodyEmitter가 매번 write & flush 를 수행한다는 점이었다. 반면 일반 JSON은 배열 형태로 합쳐 한 번만 write & flush 하므로 I/O blocking 시간이 훨씬 적었다. 같은 원리로 Netty 환경에서는 Tomcat 대비 약 7배 빠른 19–20MB/s 수준이 나왔지만, 서비스 전체를 바꾸기에는 변경 범위와 검증 비용이 너무 컸다.
해결은 스트리밍 구조를 바꾸는 쪽이었다. Flux<T>를 그대로 반환하지 않고, HttpServletResponse에 직접 write & flush 하도록 확장 함수를 만들고, 1000개 단위 버퍼링을 적용했다.
buffer(1000)으로 묶어서 처리- 각 아이템을 JSON 한 줄로 write
- 배치 단위로 한 번만 flush
- 마지막까지
blockLast()로 완료 보장
이 방식으로 드디어 엑셀 다운로드 성능이 개선됐다. 결국 DB, 라이브러리, 네트워크가 아니라 Servlet 기반 환경에서 Reactive 스타일 NDJSON을 다루는 방식이 병목의 원인이었고, 성능 문제는 예상 밖의 프레임워크 계층에서 발생할 수 있다는 점을 확인했다. 또한 전체를 보지 말고 구간별로 쪼개 측정해야 진짜 병목을 찾을 수 있다는 교훈도 남겼다.
이 요약은 원문 이해를 돕기 위한 큐레이션입니다. 저작권은 원저작자에게 있으며, 정확한 내용과 맥락은 원문을 확인하세요.
요약 오류, 출처 표기 문제, 삭제 요청은 문의 · 건의로 알려주세요.
