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 응답 스트리밍 예제까지 다루었습니다. 리액티브 프로그래밍의 장점과 단점을 이해하고, 필요에 맞게 적용해보세요!