본문 바로가기
spring/spring webFlux

[Spring] WebMVC에서 WebFlux 사용 (with WebClient)

by moonsiri 2023. 7. 13.
728x90
반응형

Spring에는 두 가지 웹 프레임워크가 있습니다.

  • WebMVC : 전통적인 멀티 스레드 기반의 웹 프레임워크
  • WebFlux : 리액티브 스택 기반의 웹 프레임워크

 

Spring WebMVC와 WebFlux는 공존할 수 없다고 생각했는데, Spring framework에서 제공하는 Http Client API로 RestTemplate 대신 WebClient를 사용하라고 권고하고 있어서 의문이 생겼습니다.

두 모듈의 공존이 가능한 것인가?

 

spring doc

 

Spring 문서를 확인해보면 "애플리케이션은 하나 또는 다른 모듈을 사용하거나 경우에 따라 두 모듈을 모두 사용할 수 있습니다(예: 반응형 WebClient가 포함된 Spring MVC 컨트롤러)."라고 써져 있습니다.

실제로 spring-boot-starter-web, spring-boot-starter-webflux 의존성을 동시에 추가할 수 있습니다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

 

SpringBoot는 WebMVC와 WebFlux를 모두 사용할 수 있는 경우 SpringMVC를 자동으로 구성합니다.

  • Spring MVC는 Netty에서 Run 할 수 없음
  • 두 인프라가 동일한 작업(예: 정적 리소스 제공, 매핑 등)을 위해 경쟁하게 됨
  • 동일한 컨테이너 내에서 두 런타임 모델을 혼합하는 것은 좋은 생각이 아니며 성능이 나쁘거나 전혀 작동하지 않을 가능성이 높음

org.springframework.boot.WebApplicationType.java

그래서 일반적으로 두 가지 의존성이 존재하면 WebApplicationType이 SERVELT이 되므로 WebFluxAutoConfiguration은 활성화되지 않습니다. 만약 @EnableWebFlux 어노테이션으로 강제로 WebFlux를 활성화하더라도 WebFluxConfigurationSupport에서 애플리케이션 실행 시점에 에러를 던져 방지하고 있습니다.

org.springframework.web.reactive.config.WebFluxConfigurationSupport.java

 

WebClient를 사용하여 Multiple HTTP 호출에 최적화하고 Reactor 연산자를 사용하려는 경우 MVC의 어노테이션 @Controller를 사용하고 return type을 Mono나 Flux로 사용할 수 있습니다. (SpringBoot talk 참조)

더보기

WebClient를 최대한 활용하고 싶다면 코어에 논블로킹(내부적으로 netty를 사용하여 논블로킹 방식으로 요청을 관리함)이 가능한 WebFlux를 사용해야 합니다.

다만, 비동기 워크플로우를 관리하는 두 가지 방법이 다르기 때문에 퍼블리셔 기반 코드(Mono, Flux)와 MVC 비동기 스타일 (@Async, DeferredResult) 중 하나만 사용해야 합니다.

 

더보기
	@Bean
	public WebClient webClient() {
		//참고: https://lasel.kr/archives/740
		ConnectionProvider connectionProvider = ConnectionProvider.builder("web-client-pool")
			.maxConnections(500) //유지할 Connection Pool의 수, max 값 많큼 미리 생성해 놓지 않고 필요할때마다 생성(최대 생성 가능한 수)
			.pendingAcquireTimeout(Duration.ofSeconds(5)) //Connection Pool에서 사용할 수 있는 Connection 이 없을때 (모두 사용 중일때) Connection을 얻기 위해 대기하는 시간
			.pendingAcquireMaxCount(-1) //커넥션 풀에서 커넥션을 가져오는 시도 횟수
//			.evictInBackground(Duration.ofSeconds(5)) //백그라운드에서 만료된 connection을 제거하는 주기
			.maxIdleTime(Duration.ofSeconds(55L)) //커넥션 풀에서 idle 상태의 커넥션을 유지하는 시간(주의: AWS ALB 타임아웃이 60초라서 60초보다 작은게 좋음)
			.maxLifeTime(Duration.ofSeconds(55L)) //커넥션 풀에서 살아있을 수 있는 커넥션의 최대 수명시간
			.build(); //keepAliveTimeout 보다 maxIdleTime 이 작은게 좋음(일반적으로 AWS나 nginx keeplive timeout이 60초인 경우 많음)

		final int timeoutSeconds = 5;
		//참고: https://yangbongsoo.tistory.com/30
		HttpClient httpClient = HttpClient.create(connectionProvider)
			.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
			.doOnConnected(connection ->
				connection.addHandlerLast(new ReadTimeoutHandler(timeoutSeconds))
					.addHandlerLast(new WriteTimeoutHandler(timeoutSeconds))
			).responseTimeout(Duration.ofSeconds(timeoutSeconds + 1)); //idle 커넥션을 닫거나 맺는 시간을 고려하지 않은 순수 http 요청/응답 시간을 제한(connection timeout + 커넥션풀에서 얻는 시간 보다 무조건 커야함)

		return WebClient.builder()
			.clientConnector(new ReactorClientHttpConnector(httpClient))
			.build();
	}

 

[Reference]

https://docs.spring.io/spring-boot/docs/current/reference/html/features.html

728x90
반응형

댓글