Flux 기반 대용량 조회 최적화

Flux 기반 대용량 조회 최적화

1. 왜 Flux였는가?

기존의 비즈니스 로직은 순차적인 루프 기반 구조로 동작하고 있었습니다. 데이터 목록을 하나씩 순회하면서 외부 API 또는 DB를 호출하고, 각 결과를 수집한 뒤 다음 로직을 수행하는 매우 전형적인 구조였습니다.

초기에는 데이터 양이 많지 않았기 때문에 큰 문제가 발생하지 않았습니다. 하지만 서비스 규모가 점점 커지고 처리해야 하는 옵션 및 조회 대상이 증가하면서 성능 저하가 본격적으로 발생하기 시작하였습니다.

특히 외부 서비스 또는 DB에 대한 의존성이 높은 구조에서는 네트워크 I/O 대기 시간이 누적되면서 전체 응답 시간이 급격하게 증가하였습니다.

예를 들어 평균 응답 시간이 200ms인 API를 100번 순차 호출하면 단순 계산만으로도 20초 이상의 시간이 소요될 수 있었습니다. 문제는 CPU 자원은 충분히 남아 있음에도 불구하고 대부분의 시간이 I/O 대기 상태로 소비되고 있었다는 점입니다.

즉, 시스템 자원을 제대로 활용하지 못하는 비효율적인 구조였습니다.

기존 방식은 아래와 같은 특징을 가지고 있었습니다.

첫째, 이전 요청이 완료되어야만 다음 요청이 시작될 수 있었습니다.

둘째, 네트워크 응답 대기 시간이 그대로 전체 응답 시간에 누적되었습니다.

셋째, 데이터 개수가 증가할수록 응답 시간이 선형적으로 증가하였습니다.

넷째, 외부 API 지연이 발생하면 전체 서비스 응답 시간도 함께 느려졌습니다.

특히 대량 옵션 데이터를 조회하는 환경에서는 이러한 문제가 더욱 심각하게 드러났습니다. 실제 운영 환경에서는 특정 시간대에 수백 개 이상의 조회 요청이 동시에 발생하는 경우도 있었으며, 이 과정에서 응답 지연과 타임아웃 문제가 반복적으로 발생하였습니다.

이를 해결하기 위해 가장 먼저 고려한 것은 병렬 처리였습니다.

독립적인 조회 작업이라면 굳이 순차적으로 처리할 필요가 없었기 때문입니다.

특히 대부분의 작업이 CPU 연산보다 네트워크 I/O 대기 중심이었기 때문에, 병렬 처리만 적절히 적용해도 큰 성능 개선 효과를 기대할 수 있었습니다. 이 과정에서 선택한 기술이 Reactor 기반의 Flux였습니다. Flux는 단순한 컬렉션 반복 도구가 아니라, 데이터를 스트림 형태로 처리하면서 병렬 처리와 비동기 실행을 자연스럽게 결합할 수 있는 Reactive Stream 기반 라이브러리입니다.

특히 다음과 같은 이유 때문에 Flux를 선택하게 되었습니다.

첫째, 대량 데이터를 스트림 형태로 안정적으로 처리할 수 있었습니다.

둘째, 병렬 레일(parallel rail)을 이용해 손쉽게 병렬 처리를 구성할 수 있었습니다.

셋째, Scheduler 기반으로 스레드 자원을 세밀하게 제어할 수 있었습니다.

넷째, Backpressure 기반 구조를 통해 무분별한 스레드 증가를 방지할 수 있었습니다.

다섯째, 기존 Java 및 Spring 기반 시스템과 자연스럽게 통합할 수 있었습니다.무엇보다 중요한 점은 “전체 처리 시간을 가장 오래 걸리는 단일 요청 수준까지 단축할 수 있다”는 가능성이었습니다.

기존 순차 처리 방식에서는 n개의 요청이 있으면 전체 수행 시간이 아래와 같은 구조였습니다.

기존 방식 :

n × 평균 응답 시간

반면 병렬 처리 구조에서는 아래와 같은 형태로 개선될 수 있었습니다.

개선 방식 :

MAX(개별 응답 시간) + α(스케줄링 오버헤드)

즉, 병렬 처리 수만 충분히 확보된다면 전체 응답 시간은 가장 오래 걸리는 단일 작업 시간 수준으로 수렴하게 됩니다.

2. 적용 과정 및 구현 전략

실제 적용 과정에서는 단순히 비동기 호출만 적용한다고 해결되는 문제가 아니었습니다. 운영 환경에서는 성능뿐 아니라 안정성, 데이터 정합성, 스레드 자원 관리까지 함께 고려해야 했습니다.

특히 가장 중요했던 부분은 “병렬 처리 후 결과를 다시 동기적으로 회수해야 한다”는 제약 조건이었습니다. 실제 비즈니스 로직에서는 모든 조회 결과가 수집되어야만 다음 단계의 계산 및 후속 처리 로직이 수행될 수 있었습니다.

즉, 조회 자체는 병렬 처리하되 최종 결과는 반드시 동기적으로 합쳐야 하는 구조였습니다.

(1) Flux 기반 스트림 구성

먼저 조회 대상 데이터를 Flux.fromIterable()을 통해 스트림 형태로 변환하였습니다. 이후 .parallel(size)을 사용하여 병렬 레일을 구성하였습니다 .

여기서 size 값은 단순히 CPU 코어 개수만 기준으로 결정하지 않았습니다. 외부 API 응답 시간, DB 부하, 네트워크 지연, 서버 메모리 상황 등을 함께 고려하여 병렬 처리 수를 조정하였습니다

.병렬 수를 지나치게 높이면 오히려 외부 서비스에 과도한 부하를 유발할 수 있었기 때문입니다.따라서 운영 환경에서는 다음과 같은 기준을 함께 고려하였습니다.

• 외부 API Rate Limit

• DB Connection Pool 크기

• 평균 응답 시간

• 서버 메모리 사용량

• CPU 사용률

• 네트워크 지연 상황

(2) Scheduler 최적화

병렬 처리에서 또 하나 중요한 요소는 스레드 관리였습니다.

초기에는 단순 parallel()만 사용하였지만, 실제 운영 환경에서는 스레드 관리 전략이 매우 중요하다는 점을 확인할 수 있었습니다.

특히 네트워크 I/O 중심 작업에서는 블로킹(Blocking) 가능성이 존재하기 때문에 일반적인 CPU 연산 전용 스레드 풀만 사용하는 것은 적절하지 않았습니다. 이를 해결하기 위해 Schedulers.boundedElastic()을 사용하였습니다. boundedElastic은 Reactor에서 제공하는 탄력적 스레드 풀입니다. 필요한 경우 스레드를 확장하되, 무제한 증가를 방지하도록 제한이 걸려 있습니다.

즉, 시스템 안정성을 유지하면서도 I/O 대기 중심 작업에 적합한 구조를 제공하였습니다.

특히 다음과 같은 장점이 있었습니다.

첫째, 필요 시 스레드를 유연하게 확장할 수 있었습니다.

둘째, 유휴 스레드는 자동으로 정리되었습니다.

셋째, 무제한 스레드 생성으로 인한 OOM 위험을 줄일 수 있었습니다.

넷째, Blocking I/O 환경에서도 안정적으로 동작하였습니다.

실제 운영 환경에서는 단순 성능보다 “안정적인 자원 사용”이 훨씬 중요하였습니다. boundedElastic은 이러한 운영 안정성 측면에서 매우 적절한 선택이었습니다.

(3) CountDownLatch 기반 동기화

가장 핵심적인 고민은 비동기와 동기를 어떻게 조합할 것인가였습니다. 비동기 처리를 통해 조회 속도는 크게 개선할 수 있었지만, 최종적으로는 모든 조회 결과가 모여야만 다음 비즈니스 로직을 수행할 수 있었습니다.

즉, 조회 과정은 비동기지만 최종 흐름은 동기적으로 제어해야 하는 구조였습니다. 이를 해결하기 위해 CountDownLatch를 사용하였습니다. 각 스레드는 병렬로 API를 호출하고 결과를 responseBodyList에 저장하였습니다.

그리고 작업이 완료될 때마다 countDown()을 호출하였습니다. 메인 흐름에서는 await()를 통해 모든 작업이 종료될 때까지 대기하도록 구성하였습니다.

구조를 정리하면 다음과 같습니다.

• 비동기 : 각 스레드에서 API를 병렬 호출하고 결과를 responseBodyList에 저장

• 동기 : CountDownLatch.await()를 통해 모든 스레드 종료까지 메인 흐름 대기

이 방식의 가장 큰 장점은 기존 비즈니스 로직 구조를 크게 변경하지 않으면서 병렬 처리만 안전하게 도입할 수 있었다는 점입니다.

즉, 기존 동기 기반 비즈니스 구조를 유지하면서 내부 조회 로직만 병렬화할 수 있었습니다.

3. 문제 해결 및 성능 개선 결과

Flux 기반 병렬 처리 구조를 적용한 이후 성능은 매우 크게 개선되었습니다. 기존 순차 처리 방식에서는 데이터 개수가 증가할수록 응답 시간이 선형적으로 증가하였습니다.

예를 들어 평균 응답 시간이 300ms인 작업을 100개 수행하면 전체 응답 시간은 약 30초 수준까지 증가할 수 있었습니다.

하지만 병렬 처리 구조에서는 대부분의 요청이 동시에 수행되었기 때문에 전체 응답 시간은 가장 오래 걸리는 단일 요청 시간 수준으로 수렴하였습니다.실제 운영 환경에서는 다음과 같은 개선 효과를 확인할 수 있었습니다.

• 기존 방식 :

n × 평균 응답 시간

• 개선 방식 :

MAX(개별 응답 시간) + α(스케줄링 및 병합 비용)

특히 네트워크 지연이 발생하는 환경에서 병렬 처리 효과가 매우 크게 나타났습니다. 기존 방식에서는 특정 API 응답이 느려지면 전체 흐름이 모두 지연되었습니다.

반면 병렬 처리 구조에서는 일부 요청이 느리더라도 다른 요청들은 동시에 처리될 수 있었기 때문에 전체 체감 성능이 크게 향상되었습니다.

또한 CPU 사용률 측면에서도 훨씬 효율적인 결과를 얻을 수 있었습니다. 기존에는 대부분의 시간이 I/O 대기로 소비되었지만, 병렬 처리 이후에는 유휴 시간이 크게 감소하였습니다.

운영 관점에서도 긍정적인 변화가 있었습니다.

첫째, 대량 옵션 조회 시 타임아웃의 발생 빈도가 감소하였습니다.

둘째, 피크 시간대 응답 안정성이 개선되었습니다.

셋째, 외부 API 지연 상황에서도 전체 서비스 장애로 이어지는 비율이 감소하였습니다.

넷째, 병렬 처리 수를 환경별로 조정할 수 있어 운영 유연성이 증가하였습니다.

특히 병렬 수 조절만으로 성능과 안정성 균형을 쉽게 조정할 수 있다는 점이 매우 유용하였습니다.

4. 마치며

이번 최적화 경험을 통해 단순히 최신 기술을 사용하는 것보다 중요한 것은 “비즈니스 제약 조건에 맞는 최적의 도구 조합”을 찾는 것이라는 점을 다시 한번 실감할 수 있었습니다. 처음에는 단순히 비동기 처리만 적용하면 모든 문제가 해결될 것이라고 생각하였습니다.

하지만 실제 운영 환경에서는 성능뿐 아니라 안정성, 데이터 정합성, 운영 유지 보수성까지 모두 함께 고려해야 했습니다.

특히 모든 작업을 무조건 완전 비동기 구조로 변경하는 것이 항상 정답은 아니라는 점도 배울 수 있었습니다.

실제 프로젝트에서는 다음 단계 로직 수행을 위해 모든 결과가 반드시 필요하였기 때문에, 비동기 처리 이후 동기적 결과 수집이 반드시 필요하였습니다.

이 과정에서 CountDownLatch와 같은 동기화 도구를 함께 사용하는 혼합 모델이 매우 효과적인 해결책이 될 수 있음을 확인할 수 있었습니다.

또한 Flux를 단순 “최신 기술” 관점이 아니라, 대량 I/O 처리 환경에 적합한 실행 모델이라는 관점에서 접근하게 된 점도 매우 의미 있는 경험이었습니다.

결국 중요한 것은 특정 기술 자체가 아니라, 현재 시스템 구조와 운영 환경에 가장 적합한 방식을 선택하는 것이라고 생각합니다.

이번 경험은 단순 성능 개선 이상의 의미가 있었습니다. 운영 환경에서의 안정성과 유지 보수성까지 함께 고려한 실질적인 최적화 경험이었다고 생각합니다.

Jack

Site footer