Spring Webflux와 WebClient

구본식·2023년 1월 30일
4
post-thumbnail

사이드 프로젝트를 진행하면서 소셜 로그인을 구현하기 위해 카카오인증서버와 API통신을 하면서 WebClient의 Http client를 사용한적이 있다.
프로젝트가 끝난후 자세히 공부를 할 필요가 있어 공부한 내용을 정리해보겠다.
무진장 어려운 내용이였네..ㅎㅎ🤣

Spring Webflux를 학습하기에 앞서 Spring MVCThread Poll에 대해서 학습할 필요가 있다.

1. Spring MVC의 Thread Pool

Spring MVC 프레임워크Multi-ThreadBlocking방식을 사용한다.

참고로 Blocking방식이란?
: 간단히 설명하자면 요청한 작업이 끝날때까지 다른 작업을 하지않고 끝날때까지 기다리는것이다.
즉 하나의 스레드가 요청을 처리하기 위해 처리함수를 실행하면 제어권도 함께 넘겨 해당 함수가 끝이날때까지 다른 함수를 호출할수 없는것이다.

Spring MVC 프레임워크는 클라이언트가 접속을 하게 되면 서버에 쓰레드를 모아논 쓰레드 풀에서 하나씩 분배해주게 되고 접속이 끝이 나면 다음 클라이언트에게 해당 쓰레드를 주게된다.

만약 클라이언트가 많아져 쓰레드 풀의 남은 쓰레드가 없으면 Queue공간에 클라이언트가 쌓이게 된다.

그래서 클라이언트의 접속수가 증가하면(동시성이 증가)
CPU,메모리가 충분하더라도 쓰레드가 부족하여 성능이 떨어질수 있게 된다.
또한 쓰레드를 늘리게 되면 메모리,CPU성능이 떨어지기 때문에 무조권적으로 쓰레드를 늘린다고 해결될수 없다.

대부분 Spring MVC에서는 동시성을 스레드를 늘려 처리한다고 한다.


2. Spring Webflux란?

Spring Webflux란 Client,Server에서 reactive스타일의 어플리케이션의 개발을 도와주는 스프링 모듈이다. 스프링의 리엑티브 스택의 웹파트를 담당하고 있는 프레임워크이다.

Spring WebfluxSingle-ThreadNon-Blocking방식을 사용한다.

참고로 Non-Blocking방식이란?
: 요청한 작업이 수행되는 공안 다른 작업을 할수 있는 방식이다. 즉 호출자가 함수를 호출할때 제어권을 호출자가 가지고 있어 다른 작업을 수행할수 있다.

그래서 Spring Webflux는 적은 수의 스레드를 사용하여 여러 클라이언트의 요청을 처리할수 있다.

그럼Spring WebfluxSpring MVC보다 좋을수 있다고 생각할수 있다. 결론은 아니다.!

자원측면에서는 Spring Webflux적은 스레드로 자원(메모리,CPU)를 효율적으로 사용할수 있다.
하지만 속도측면에서는 리엑티브, 논블로킹 방식인 Spring Webflux은 어플리케이션의 속도를 향샹시키지는 못한다.! (하지만 어떤 경우에는 속도가 향상 될수도 있음)

즉, 적은 리소스(스레드,메모리,CPU)로 많은 트래픽을 처리할수 있다.

리엑티브 프로그래밍이란?

데이터 스프림과 변경사항 전파를 중심으로하는 비동기 프로그래밍 패러다임이다.

데이터 스트림을 이용하여 전달하고, 데이터 변경시점을 이벤트로하여 수신자와 송신자 사이에 데이터를 전달시키는 비동기 프로그램이라고 한다.

내용이 어렵다.ㅠㅠ

Reactor란 ??

JVM 위에서 동작하는 논블러킹 어플리케이션을 만들기 위한 리엑티브 라이브러리라고 한다.
(Reactive Stream (인터페이스)의 구현체중에 하나라고한다. 추가로 RxJava라는 것도 있다고 한다.)

또한 Spring Webflux가 선택한 리엑티브 라이브러리가 Reactor라고 한다.

Mono와 Flux란

Reactor는 리액티브 스트림을 구현한 라이브러리이며 Mono와 Flux 2가지 데이터 타입으로 데이터 스트림을 정의한다고 한다.

Spring WebFlux를 사용하여 비동기적인 데이터 스트림처리를 리액터 라이브러리가 제공하는 데이터 타입인 Mono와 Flux로 처리해야 한다고 한다.

즉 Reactor가 제공하는 데이터 스트림 타입이라고 생각하면 될거같다.

Mono는 0개 또는 하나의 결과만 처리하기 위한 Reactor 객체이고,
Flux는 0개 또는 여러개의 결과를 처리하는 객체이다.

보통 여러 데이터 스트림을 하나의 결과를 모아줄 때 Mono를 쓰고, 각각의 Mono를 합쳐서 여러 개의 값을 여러 개의 값을 처리하는 Flux로 처리한다고 한다.

그 외

그외에도 SpringMVC에서 사용하던 어노테이션 기반 컨트롤러(@Controller,@GetMapping등)를 사용할수 있고 추가로 @RequestBody에 Mono,Flux객체를 사용할수 있다고한다.


3. Spring WebClient란?

WebClientSpring WebFlux에서 HTTP Client로 사용되는 비동기적으로 작동하는 모듈이다.

내부적으로 WebClientHTTP Client 라이브러리에 위임하는데 디폴트로 NettyHttp Client를 사용한다고 한다.

Spring MVC에서도 마찬가지로 사용할수 있고, 추후 연습도 Spring MVC 프레임워크 환경에서 웹 클라이언트를 사용해볼것이다.

📌참고

Servlet Stack에 포함되는 Spring MVC가 있듯이 Spring WebFluxReactive Stack에 별개로 포함된 프레임워크이다.
Spring Security와 더불어 JPA,Spring Data JPA도 Spring WebFlux전용으로 지원한다고 한다.

Spring WebFlux기반의 서비스가 JPA를 사용하면 JPA는 동기로 작동하기 때문에 WebFlux를 사용한 장점이 없어지기 때문에 비동기인 WebFlux를 사용한 장점이 없어지기 때문에 맞는 기능을 사용해야된다.


4. Spring Webclient 간단한 연습

4.1 gradle

	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-webflux'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'io.projectreactor:reactor-test'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'

4.2 WebClientConfig

필요시 직접 WebClient를 생성해서 사용하는 방법도 있지만 시간이 걸리므로 미리 생성하여 사용할것이다.

@Configuration@Bean를 사용하여 Spring Bean으로 등록하였다.

@Slf4j
@Configuration
public class WebClientConfig {
    @Bean
    public WebClient webClient() {

        /**
         * 통신시 timeout 세팅
         * - connect, read, write 를 모두 5000ms
         */
        HttpClient httpClient = HttpClient.create()
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
                .responseTimeout(Duration.ofMillis(5000))
                .doOnConnected(conn ->
                        conn.addHandlerLast(new ReadTimeoutHandler(5000, TimeUnit.MILLISECONDS))
                                .addHandlerLast(new WriteTimeoutHandler(5000, TimeUnit.MILLISECONDS)));

        WebClient webClient = WebClient.builder()
                .baseUrl("http://43.200.219.233:8888")
                .clientConnector(new ReactorClientHttpConnector(httpClient)) //생성한 HttpClient 연결
                //Request Header 로깅 필터
                .filter(
                        ExchangeFilterFunction.ofRequestProcessor(
                                clientRequest -> {
                                    log.info(">>>>>>>>> REQUEST <<<<<<<<<<");
                                    log.info("Request: {} {}", clientRequest.method(), clientRequest.url());
                                    clientRequest.headers().forEach(
                                            (name, values) -> values.forEach(value -> log.info("{} : {}", name, value))
                                    );
                                    return Mono.just(clientRequest);
                                }
                        )
                )
                //Response Header 로깅 필터
                .filter(
                        ExchangeFilterFunction.ofResponseProcessor(
                                clientResponse -> {
                                    log.info(">>>>>>>>>> RESPONSE <<<<<<<<<<");
                                    clientResponse.headers().asHttpHeaders().forEach(
                                            (name, values) -> values.forEach(value -> log.info("{} {}", name, value))
                                    );
                                    return Mono.just(clientResponse);
                                }
                        )
                )
                .defaultHeader("Content-type", "application/x-www-form-urlencoded;charset=utf-8") //기본 헤더설정
                .build();

        return webClient;
    }
}

Timeout

통신을 하다보면 연결이 느려지는 등의 문제가가 발생할때 오랜시간을 대기하여야 된다.
너무 길거나 무제한으로 설정하게 되면 리소스 자원의 낭비가 발생할수 있다.

이러한 문제를 해결하기 위해서 Timeout를 지정할수 있다.

통신시 필요한 설정들을 HTTP Client를 생성하여 WebClient에 지정해주었다.(전역적으로 사용가능)
그리고 기본적으로 WebFlux를 사용하는 경우 HttpClientNetty를 사용하게 된다.

설정 정보를 상세히 살펴보자면,
Connection Timeout은 Client/Server간의 Connection을 맺기 위한 시간이다.
Sever에서 새로운 Connection을 맺을 자원의 여유가 없다면 발생할수 있다.

Read Timeout은 데이터를 읽기 위한 시간이다.
너무 길게 되면, 응답을 받을때까지 대기하게 되고 자원의 고갈되는 현상이 발생할수 있다.
너무 짧다면, 요청을 받은쪽은 처리되었지만 응답을 기다리는쪽에서 Timeout이 발생하게 된다.

Write Timeout은 데이터를 쓰기 위한 시간이다.
Connection이 이루어지고 요청한 쪽에서 데이터를 보내기 시작하였으나 설정한 시간보다 길어지게 되면 중간된다.

response Timeout은 순수 http 요청/응답 시간에 대한 timeout이다.
커넥션을 닫거나 맥거나 하는 시간을 고려하지 않은 순수 http 요청/응답 시간을 제한한다.

filter

그 다음으로 filter를 이용하여 Request,Response를 컨트롤할수 있다.

Request Header에 필요한 데이터를 추가하고, Request Header 로깅, Response Header 로깅등 다양한 기능을 사용할수 있다.

간단하게 요청/응답시 Header의 로깅을 출력해보는 filter를 추가해보았다.
(위 그림은 Request의 부분)

그 외에

그 외에도,
baseUrl(Webclient에 공통으로 쓰일 Host를 지정할수 있다.),defaultHeader,defaultCookie등 다양한 Http통신에 필요한 값들을 디폴트로 설정할수 있다.

4.3 TestController

@RestController
public class TestController {

    @Autowired
    private WebClient webClient;

    //일반 테스트
    @GetMapping("/test1")
    public Mono<String> doTest1() {

        return webClient.get()
                .uri("/codebox/data1/1")
                .retrieve()
                .bodyToMono(String.class);
    }

    //baseUrl 변경 테스트
    @GetMapping("/test2")
    public Mono<String> doTest2() {

        return webClient
                .mutate() //기존의 WebClient를 이용하되 옵션 변경
                .baseUrl("https://kauth.kakao.com")
                .build()
                .get()
                .uri(uriBuilder -> uriBuilder
                        .path("/oauth/authorize")
                        .queryParam("client_id","6d35286d18b06aa91f75f81202a71203")
                        .queryParam("redirect_uri", "http://localhost:8888/login/oauth2/code/kakao")
                        .queryParam("response_type", "code")
                        .build())
                .retrieve()
                .bodyToMono(String.class);
    }
}

테스트를 위해서 doTest1()은 내가 배포해논 서비스에 api를 요청해보았다.
uri에는 앞서 정의한 Host(baseUrl)에 Path를 지정하게 된다.

bodyToMono는 앞서 설명한 0개~1개의 객체(Stirng등)을 처리하는 Reactor가 제공하는 데이터 타입인 Mono에 해당한다.

doTest2()에는 baseUrl를 통해 통신할 Host를 변경할때 사용하는 mutate()를 사용해본 예제이다.
(기능을 테스트 해볼거라서 카카오서버에 인증코드를 받는 api로 호출해보았다.)


이 외에도 WebClient 에러처리,재전송등 다양한 기능들이 존재하는거 같다.
지금은 프로젝트 진행시 정확한 개념을 모르고 사용했던 WebClient의 개념을 공부하는 시간이여서 여기까지만 해봐야겠다.🤣
나중에 리엑티브한 서비스 프로젝트를 진행할 기회가 있다면 다시 공부를 많이 해봐야할거 같다.

참고자료 : https://www.youtube.com/watch?v=4x1QRyMIjGU, https://happycloud-lee.tistory.com/220

profile
백엔드 개발자를 꿈꾸며 기록중💻

0개의 댓글