WebClient와 리액티브 프로그래밍 사용하기

자바 스프링 부트에서 WebClient와 리액티브 프로그래밍을 사용하여 비동기 논블로킹 방식으로 HTTP 요청을 처리하고, 실시간 스트리밍으로 API 응답을 프론트엔드에 표시하는 방법을 다룹니다.
WebClient와 리액티브 프로그래밍 사용하기
Photo by Emile Perron / Unsplash

On this page

이번 포스팅에서는 자바에서 비동기 논블로킹 방식의 HTTP 요청을 처리할 수 있는 WebClient와 리액티브 프로그래밍에 대해 알아보겠습니다. WebClient는 Spring 5부터 제공되며, 이전의 RestTemplate보다 현대적인 방법으로 웹 서비스를 호출하는 데 유용합니다. 리액티브 프로그래밍의 기본 개념부터 실제 예제까지 함께 살펴보겠습니다.

WebClient와 그 동작 원리

WebClient는 논블로킹 I/O를 사용합니다. HTTP 요청이 발생했을 때, 요청이 완료될 때까지 스레드가 블로킹되지 않고 다른 작업을 계속할 수 있음을 의미합니다. 이는 서버의 리소스 효율성을 높이고 높은 동시성을 처리하는 데 유리합니다.

WebClient 사용 예시

다음은 WebClient를 사용하여 GET 요청을 보내고 응답을 처리하는 간단한 예제입니다.

import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

public class WebClientExample {

    public static void main(String[] args) {
        WebClient webClient = WebClient.create("<https://jsonplaceholder.typicode.com>");

        Mono<String> response = webClient.get()
                .uri("/posts/1")
                .retrieve()
                .bodyToMono(String.class);

        response.subscribe(System.out::println);
    }
}

위 코드는 https://jsonplaceholder.typicode.com/posts/1에 GET 요청을 보내고, 응답을 문자열로 받아서 출력합니다.

오래 걸리는 API 호출 시 리소스 관리

오래 걸리는 API를 호출할 때는 다음과 같은 방법을 사용할 수 있습니다:

  • 타임아웃 설정: 일정 시간이 지나면 요청을 취소하는 타임아웃을 설정할 수 있습니다.
  • 비동기 및 논블로킹: WebClient는 기본적으로 논블로킹 방식이기 때문에, 요청을 보낸 후 다른 작업을 계속할 수 있습니다.
  • 백프레셔: 데이터를 소비하는 속도에 따라 생산하는 속도를 조절하는 메커니즘을 사용할 수 있습니다.

타임아웃 설정 예시

WebClient에서 타임아웃을 설정하는 방법은 다음과 같습니다.

import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.time.Duration;

public class WebClientTimeoutExample {

    public static void main(String[] args) {
        WebClient webClient = WebClient.builder()
                .baseUrl("<https://jsonplaceholder.typicode.com>")
                .build();

        Mono<String> response = webClient.get()
                .uri("/posts/1")
                .retrieve()
                .bodyToMono(String.class)
                .timeout(Duration.ofSeconds(5)) // 5초 타임아웃 설정
                .onErrorResume(throwable -> {
                    System.err.println("Timeout or other error: " + throwable.getMessage());
                    return Mono.empty();
                });

        response.subscribe(System.out::println);
    }
}

이 예제에서는 요청이 5초 이내에 완료되지 않으면 타임아웃 에러를 발생시키고, 에러 발생 시 대체 동작을 정의합니다.

Mono의 개념 및 사용 예시

Mono는 Reactor 라이브러리에서 제공하는 단일 값의 비동기 시퀀스를 나타내는 객체입니다. 이는 CompletableFuture와 비슷하지만, Reactive Streams 사양을 준수하여 더 많은 기능과 유연성을 제공합니다.

Mono의 동작 원리

Mono는 단일 값을 포함하거나 에러를 발생시킬 수 있는 비동기 처리의 결과를 나타냅니다. 이를 통해 비동기 작업의 결과를 처리할 수 있습니다.

Mono 사용 예시

다음은 Mono를 사용하여 비동기 작업을 처리하는 간단한 예제입니다.

import reactor.core.publisher.Mono;

public class MonoExample {

    public static void main(String[] args) {
        // Mono 생성
        Mono<String> mono = Mono.just("Hello, World!");

        // Mono 구독
        mono.subscribe(System.out::println);

        // 비동기 작업 예시
        Mono<String> asyncMono = Mono.fromSupplier(() -> {
            try {
                // 오래 걸리는 작업 시뮬레이션
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "Async Response";
        });

        // 비동기 Mono 구독
        asyncMono.subscribe(response -> System.out.println("Received: " + response));
    }
}

위 예제에서는 Mono를 사용하여 동기 및 비동기 작업을 처리하고 있습니다. Mono.just는 즉시 값을 제공하는 Mono를 생성하고, Mono.fromSupplier는 공급자 함수에서 값을 비동기적으로 생성하는 Mono를 만듭니다.

WebClient와 Mono 사용 예시

다음은 WebClient와 Mono를 함께 사용하여 비동기 HTTP 요청을 처리하는 예제입니다.

import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.time.Duration;

public class WebClientMonoExample {

    public static void main(String[] args) {
        WebClient webClient = WebClient.create("<https://jsonplaceholder.typicode.com>");

        // GET 요청을 비동기로 보내고 응답을 Mono로 받기
        Mono<String> response = webClient.get()
                .uri("/posts/1")
                .retrieve()
                .bodyToMono(String.class)
                .timeout(Duration.ofSeconds(30)) // 타임아웃을 30초로 설정
                .onErrorResume(throwable -> {
                    System.err.println("Error: " + throwable.getMessage());
                    return Mono.empty();
                });

        // 응답 구독 및 처리
        response.subscribe(System.out::println);

        // 메인 스레드가 종료되지 않도록 대기
        try {
            Thread.sleep(35000); // 35초 대기
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

이 예제에서는 WebClient를 사용하여 비동기 GET 요청을 보내고, Mono를 통해 응답을 처리합니다. 타임아웃을 30초로 설정하여 응답 시간이 긴 경우에도 적절히 처리할 수 있도록 했습니다.

리액티브 프로그래밍의 개념

리액티브 프로그래밍은 데이터 스트림과 변경 이벤트의 전파를 중심으로 하는 비동기 프로그래밍 패러다임입니다. 이는 언어와 무관한 개념으로, 다양한 언어와 프레임워크에서 사용할 수 있습니다.

리액티브 프로그래밍의 주요 원칙은 다음과 같습니다:

  • 데이터 스트림: 리액티브 프로그래밍에서 모든 것이 스트림으로 표현됩니다. 예를 들어, 변수, 사용자 입력, HTTP 요청/응답 등이 스트림으로 나타납니다.
  • 비동기성: 비동기적으로 데이터를 처리하며, 이를 통해 높은 성능과 응답성을 제공합니다.
  • 변경 전파: 데이터가 변경되면 이를 구독하고 있는 모든 구독자에게 자동으로 변경 사항이 전달됩니다.
  • 지연 실행(Lazy Evaluation): 데이터가 실제로 필요할 때까지 계산을 지연시킵니다. 이는 리소스를 효율적으로 사용할 수 있게 합니다.

리액티브 프로그래밍의 장점

  • 비동기 및 논블로킹: 높은 성능과 확장성을 제공하여 많은 요청을 효율적으로 처리할 수 있습니다.
  • 구성 가능성: 스트림을 연산자와 결합하여 복잡한 데이터 처리를 간단하게 표현할 수 있습니다.
  • 유지 보수성: 코드의 흐름이 명확하고, 에러 처리가 용이하여 유지 보수성이 높습니다.

리액티브 프로그래밍의 단점

리액티브 프로그래밍이 혁신적이긴 하지만, 몇 가지 단점도 있습니다:

  • 학습 곡선: 리액티브 프로그래밍의 개념과 API는 전통적인 동기 프로그래밍에 비해 복잡하여, 이를 이해하고 숙달하는 데 시간이 걸립니다.
  • 디버깅의 어려움: 비동기 코드의 특성상 디버깅이 어려울 수 있습니다.
  • 호환성 문제: 기존의 동기 코드와 리액티브 코드 간의 상호 운용성이 어려울 수 있습니다.

실제 예제: LLM 응답 스트리밍

GPT, Gemini, Claude API와 같은 LLM의 응답을 스트리밍 방식으로 처리하여 프론트엔드에서 실시간으로 표시하는 예제를 살펴보겠습니다.

백엔드: 스프링 부트

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;

@Controller
@RequestMapping("/api")
public class ApiController {

    @Autowired
    private WebClient.Builder webClientBuilder;

    @GetMapping(value = "/llm-response", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> streamLlmResponse() {
        WebClient webClient = webClientBuilder.baseUrl("<https://api.openai.com/v1/completions>").build();

        return webClient.post()
                .header("Content-Type", "application/json")
                .header("Authorization", "Bearer YOUR_OPENAI_API_KEY")
                .bodyValue("{ \\"model\\": \\"text-davinci-003\\", \\"prompt\\": \\"Hello, how are you?\\", \\"max_tokens\\": 50, \\"stream\\": true }")
                .retrieve()
                .bodyToFlux(String.class);
    }
}

프론트엔드: HTML + 자바스크립트

html코드 복사
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>GPT Streaming</title>
</head>
<body>
    <div id="response-container"></div>

    <script>
        async function fetchGptResponse() {
            const container = document.getElementById('response-container');
            const eventSource = new EventSource('<http://localhost:8080/api/llm-response>');

            eventSource.onmessage = function(event) {
                const newElement = document.createElement('p');
                newElement.textContent = event.data;
                container.appendChild(newElement);
            };

            eventSource.onerror = function() {
                console.error('EventSource failed.');
                eventSource.close();
            };
        }

        fetchGptResponse();
    </script>
</body>
</html>

이 예제에서는 스프링 부트를 사용하여 LLM 응답을 스트리밍 방식으로 처리하고, 프론트엔드에서 실시간으로 이를 표시합니다. 이렇게 하면 긴 응답 시간의 작업도 사용자에게 실시간 피드백을 제공하여 향상된 사용자 경험을 제공할 수 있습니다.

결론

리액티브 프로그래밍과 WebClient를 사용하면 비동기 논블로킹 방식으로 HTTP 요청을 처리하고, 시스템의 성능과 확장성을 높일 수 있습니다. 이번 포스팅에서는 WebClient의 기본 사용법부터 LLM 응답 스트리밍 예제까지 다루었습니다. 리액티브 프로그래밍의 장점과 단점을 이해하고, 필요에 맞게 적용해보세요!

Subscribe to Keun's Story newsletter and stay updated.

Don't miss anything. Get all the latest posts delivered straight to your inbox. It's free!
Great! Check your inbox and click the link to confirm your subscription.
Error! Please enter a valid email address!