비동기 프로그래밍(CompletableFuture)을 활용한 서버 응답 속도 극대화

지난 포스팅에서는 여러 스레드가 공유 자원에 접근할 때 발생하는 병목을 해결하는 ConcurrentHashMap에 대해 알아보았습니다. 동시성 제어를 통해 스레드 간의 충돌을 막았다면, 이제는 ‘스레드가 아무 일도 하지 않고 버려지는 시간’을 최소화하여 서버의 전체 응답 속도를 극대화할 차례입니다.

엔터프라이즈 Java 애플리케이션 성능 저하의 가장 큰 주범은 외부 시스템(데이터베이스, 외부 API 등)과의 통신 시 발생하는 대기 시간입니다. 이번 글에서는 전통적인 동기(Synchronous) 처리의 한계를 짚어보고, Java 8부터 도입된 강력한 비동기 프로그래밍 도구인 CompletableFuture를 활용해 이 한계를 어떻게 돌파하는지 분석해 보겠습니다.

1. 동기(Synchronous) 처리의 한계와 블로킹(Blocking)

우리가 작성하는 일반적인 Java 코드는 위에서 아래로 순차적으로 실행되는 동기식(Synchronous)입니다.

  • 블로킹(Blocking) 현상: 웹 서버가 클라이언트의 요청을 받아 데이터베이스에 쿼리를 전송했다고 가정해 봅시다. 동기식 환경에서는 DB가 결과를 응답할 때까지 해당 요청을 담당하는 스레드는 아무 작업도 하지 못한 채 대기(Blocked) 상태로 멈춰 있어야 합니다.
  • 리소스 낭비: 만약 DB 응답에 3초가 걸린다면, 그 귀중한 3초 동안 스레드는 CPU를 전혀 활용하지 못하고 메모리만 점유하게 됩니다. 트래픽이 몰려 모든 스레드가 이처럼 멈춰 있게 되면, 결국 스레드 풀이 고갈되어 새로운 사용자의 요청을 거절(Timeout)하게 되는 참사가 발생합니다.

2. CompletableFuture의 등장과 논블로킹(Non-Blocking) 패러다임

이러한 스레드 낭비를 막기 위해 도입된 것이 비동기(Asynchronous) 프로그래밍입니다. Java 8에서 등장한 CompletableFuture는 스레드가 특정 작업이 끝날 때까지 기다리지 않고 다른 일을 할 수 있도록 만들어주는(Non-Blocking) 핵심 클래스입니다.

  • 콜백(Callback) 지옥 탈출: 과거 Future 인터페이스만으로는 비동기 작업이 끝났는지 계속 확인(Polling)해야 했고, 여러 비동기 작업을 연결하려면 코드가 매우 복잡해졌습니다. CompletableFuture는 작업이 완료되었을 때 실행할 다음 동작을 체인 형태로 우아하게 선언할 수 있어 가독성과 유지보수성을 크게 높였습니다.
  • 다중 작업 병렬 처리: A API 호출과 B DB 조회를 동시에 시작하고, 두 작업이 모두 완료되었을 때 그 결과를 조합하는 복잡한 분기 처리를 단 몇 줄의 메서드 체이닝(thenCombine 등)으로 구현할 수 있습니다.

3. 실무 최적화 사례: 데이터베이스 쿼리와 JSON 변환의 파이프라인

실제 백엔드 실무에서 응답 속도를 개선하는 시나리오를 살펴보겠습니다. 데이터베이스에서 복잡한 쿼리를 통해 대규모 결과 리스트를 조회한 뒤, 이를 외부 시스템 규격에 맞춰 JSON 파일 형식으로 변환하여 전송하는 무거운 비즈니스 로직이 있다고 가정해 보겠습니다.

동기식 설계라면 ‘쿼리 조회 완료 ➡️ JSON 변환 시작 ➡️ 전송’의 과정 동안 메인 스레드가 완전히 묶여버립니다.

하지만 CompletableFuture를 도입하면 이야기가 달라집니다.

  1. supplyAsync(): 데이터베이스 쿼리 리스트 조회 작업을 백그라운드 스레드(ForkJoinPool)에 위임합니다. 메인 스레드는 즉시 해방되어 다른 클라이언트의 요청을 처리하러 떠납니다.
  2. thenApply(): 백그라운드 스레드에서 DB 조회가 완료되는 즉시, 리스트 데이터를 JSON 형식으로 매핑하고 변환하는 파이프라인을 실행합니다.
  3. thenAccept(): JSON 변환까지 완료되면 최종적으로 외부 시스템에 데이터를 전송하거나 파일로 쓰는 작업을 마무리합니다.

이처럼 무거운 I/O 작업과 데이터 변환(Parsing) 작업을 별도의 비동기 파이프라인으로 구성하면, 메인 스레드의 점유 시간을 획기적으로 줄여 적은 수의 스레드 풀로도 수만 건의 동시 트래픽을 거뜬히 방어할 수 있습니다.

결론: 현대 아키텍처의 필수 무기

마이크로서비스 아키텍처(MSA)가 대세가 되면서 애플리케이션 내부 로직보다 외부 API 호출 구간이 훨씬 많아졌습니다. 이러한 환경에서 CompletableFuture를 활용한 비동기 프로그래밍은 선택이 아닌 생존을 위한 필수 기술입니다. 동기식 코드의 늪에서 벗어나, 시스템 자원을 100% 활용하는 진정한 고성능 서버를 구축해 보시기 바랍니다.

지금까지 스레드와 동시성에 대한 깊이 있는 분석을 마쳤습니다. 다음 포스팅부터는 이러한 시스템 내부를 시각적으로 감시하고 문제를 찾아내는 ‘성능 모니터링 및 분석 도구’ 파트로 넘어가, ‘JMX(Java Management Extensions)를 활용한 JVM 실시간 모니터링 구축’에 대해 본격적으로 알아보겠습니다.

댓글 남기기