Spring WebFlux와 FeignClient 충돌 문제 해결: WebClient로 전환

임채령·2024년 10월 30일

트러블 슈팅

Spring Boot를 사용한 분산 시스템에서 마이크로서비스 간의 상호작용은 매우 중요하다. 초기에는 Gateway와 인증 서버 간의 통신을 용이하게 하기 위해 FeignClient를 사용했다. 그러나 리액티브 스택인 Webflux를 사용해 구현해보고 싶었고, 그것이 내 사이드프로젝트에서의 인증서버, 게이트웨이가 구현 대상이 되었다 ! 하지만 처음보는 에러를 마주쳤다. 그래서 나는 FeignClient 대신 WebClient를 사용하게 되었는데… 이번 포스팅에서는 WebFlux와 MVC의 차이점, 각각의 클라이언트(FeignClient vs WebClient), 그리고 어댑터 패턴에서의 변경 사항에 대해 작성해보려한다 !!

Webflux와 같은 리액티브 환경에서도 FeignClient를 사용 하는 방법에 대한 문서

이 문서를 참고했던게 구현하는데 도움이 많이됐다. 링크 첨부 합니다 ! ! !

https://github.com/OpenFeign/feign/tree/master/reactive

Spring MVC 개념

Spring MVC (Model-View-Controller)는 동기적, 블로킹 프레임워크다. 기존 라이브러리와의 통합이 쉬운 사용 사례나 간단한 데이터 처리를 필요로 하는 애플리케이션에 적합하다. MVC는 서블릿 기반 모델에 의존하며, 요청 생명주기 전체 동안 스레드를 유지하기 때문에 적당한 로드를 처리하는 데 적합하다.

WebFlux 개념

Spring WebFlux는 리액티브, 논블로킹 프로그래밍을 위해 설계되었다. 많은 수의 동시 요청을 적은 스레드로 처리해야 하는 현대 클라우드 네이티브 애플리케이션에 적합하다. WebFlux는 Reactor가 제공하는 리액티브 모델을 사용하여 IO가 많은 작업에서 Spring MVC보다 훨씬 더 확장성이 뛰어나다 !

잠시만 여기서 동기, 비동기, 블로킹, 논블로킹이란 ?!

  • 동기적, 블로킹: 한 가지 일을 할 때 다른 일을 멈추고 기다리는 방식이다. 예를 들어, 전자레인지에 음식을 넣고 완료될 때까지 그 앞에서 기다리는 상황이다 ! 이 동안 다른 일을 할 수 없어서 '블로킹'이라고도 한다. 동기와 블로킹은 같은 의미이고, 대표적으로 서블릿 기반이라고 한다 !
  • 비동기적, 논블로킹: 한 가지 일을 시킨 후 그 결과를 기다리지 않고 다른 일을 계속하는 방식이다. 예를 들어, 세탁기를 돌려놓고 그 결과를 기다리지 않고 청소를 하는 것과 같다. 이렇게 다른 일을 계속할 수 있기 때문에 '논블로킹'이라고 부른다. 반대로 비동기와, 논블로킹은 같은 의미이고, 대표적으로 리액티브 기반 이라고 한다 !

HTTP 통신에 대한 두 접근 방식 (FeignClient, Webclient) 비교

  • FeignClient는 Netflix에서 개발한 선언적 HTTP 클라이언트로, Spring과 잘 통합되어 있다. @RequestLine 같은 어노테이션을 사용하여 간결한 인터페이스로 HTTP 요청을 작성할 수 있다. FeignClient는 동기적으로 작동하며, Spring MVC 모델과 함께 사용하기에 적합하다.
  • WebClient는 리액티브, 논블로킹 클라이언트로 WebFlux 생태계에 적합하다. 비동기 요청을 만들고 복잡한 데이터 변환을 처리하는 데 더 다양한 API를 제공한다. WebClient는 리액티브 데이터 흐름을 다룰 때 더 강력하지만, FeignClient에 비해 더 많은 보일러플레이트 코드가 필요해진다 !

Spring MVC와 Spring WebFlux 비교

Spring MVC
  • 명령형 논리: 코드를 절차적으로 작성하고 디버그하기 쉽다.
  • 쉬운 디버깅 및 작성: 익숙한 동기 방식으로 인해 작성하고 디버깅하기가 상대적으로 간단하다.
  • 블로킹 종속성: JDBC, JPA와 같은 블로킹 기반 라이브러리를 사용한다.
  • 서버 지원: Tomcat, Jetty, Undertow 같은 서블릿 기반 서버에서 실행된다.
Spring WebFlux
  • 함수형 엔드포인트: 요청을 처리하기 위해 함수를 기반으로 하는 엔드포인트를 사용한다.
  • 이벤트 루프 동시성 모델: 논블로킹 방식으로 요청을 처리하고 이벤트 루프를 통해 많은 동시 요청을 관리한다.
  • 서버 지원: Netty 같은 리액티브 기반 서버에서 실행된다.
공통점
  • @Controller 어노테이션: Spring MVC와 Spring WebFlux 모두 @Controller 어노테이션을 사용할 수 있다.
  • 리액티브 클라이언트: 두 기술 모두 리액티브 클라이언트를 사용할 수 있다.

WebFlux에서 FeignClient 사용의 문제점

WebFlux 환경에서는 FeignClient와 충돌이 발생할 수 있다. 그 이유는 FeignClient가 동기적 방식으로 동작하기 때문이다. WebFlux는 논블로킹 리액티브 프로그래밍 모델을 사용하는데, FeignClient의 동기적 접근 방식은 이러한 리액티브 특성과 맞지 않다.

WebFlux는 리액티브 기반의 WAS(Netty)에서 실행되고, Spring MVC는 서블릿 기반 WAS(Tomcat)에서 주로 실행된다. 이처럼 두 기술은 실행되는 서버 환경 자체가 다르다!!! WebFlux는 많은 수의 동시 요청을 비동기적으로 처리할 수 있도록 설계되었고, FeignClient는 요청에 대해 응답이 올 때까지 기다리기 때문에, WebFlux의 논블로킹 특성을 제대로 활용하지 못하고 전체 애플리케이션의 성능을 저하시킬 수도 있다.

이러한 불일치로 인해 시스템에서는 예기치 않은 동작이나 성능 문제, 심지어는 블로킹 관련 충돌이 발생할 수 있다. 따라서 WebFlux를 사용하는 환경에서는 FeignClient 대신 WebClient와 같은 논블로킹 HTTP 클라이언트를 사용하는 것이 필수적인것이다 ! WebClient는 리액티브 방식으로 요청을 처리하고, WebFlux의 논블로킹 특성을 최대로 활용하여 더 나은 확장성과 성능을 제공한다.

FeignClient 어댑터 예시

FeignClient를 사용했을 때의 어댑터 2가지 예시이다.

보통 첫번째 예시를 많이 쓰는것같다. 나 역시 그랬다 !

import feign.Headers;
import feign.RequestLine;
import org.springframework.cloud.openfeign.FeignClient;
import reactor.core.publisher.Mono;

@FeignClient(name = "auth-server")
public interface AuthAdapter {

    @Postmapping("/auth/refresh-token")
    Mono<TokenResponseDto> refreshToken(RefreshTokenRequestDto refreshTokenRequestDto);
}
import feign.Headers;
import feign.RequestLine;
import org.springframework.cloud.openfeign.FeignClient;
import reactor.core.publisher.Mono;

@FeignClient(name = "auth-server")
public interface AuthAdapter {

    @RequestLine("POST /auth/refresh-token")
    @Headers("Content-Type: application/json")
    Mono<TokenResponseDto> refreshToken(RefreshTokenRequestDto refreshTokenRequestDto);
}

JWT 필터에서 FeignClient 사용 예시

어댑터는 JWT 필터에서 토큰을 갱신하기 위해 다음과 같이 사용되었다.

return authAdapter.refreshToken(refreshTokenRequestDto)
    .flatMap(tokenResponseDto -> {
        if (tokenResponseDto != null) {
            String newAccessToken = tokenResponseDto.accessToken();
            String newRefreshToken = tokenResponseDto.refreshToken();
            // 쿠키 업데이트 로직
            return chain.filter(exchange);
        } else {
            return Mono.error(new UnauthorizedException(ErrorCode.UNAUTHORIZED_ACCESS));
        }
    })
    .onErrorResume(error -> {
        return Mono.error(new UnauthorizedException(ErrorCode.UNAUTHORIZED_ACCESS));
    });

WebClient로의 전환

WebFlux로 전환하면서, FeignClient를 WebClient로 교체했다. WebClient를 사용한 새로운 어댑터이다.

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter implements WebFilter {

    private final WebClient webClient;

    @Value("${auth-server.base-url}")
    private String authServerBaseUrl;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        // ... 기존 토큰 유효성 검사 로직

        return webClient.post()
            .uri(authServerBaseUrl + "/auth/refresh-token")
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue(refreshTokenRequestDto) // 요청 본문 설정
            .retrieve()
            .bodyToMono(TokenResponseDto.class) // 응답을 Mono로 변환
            .flatMap(tokenResponseDto -> {
                if (tokenResponseDto != null) {
                    String newAccessToken = tokenResponseDto.accessToken();
                    String newRefreshToken = tokenResponseDto.refreshToken();
                    // 쿠키 업데이트 로직
                    return chain.filter(exchange);
                } else {
                    return Mono.error(new UnauthorizedException(ErrorCode.UNAUTHORIZED_ACCESS));
                }
            })
            .onErrorResume(error -> {
                return Mono.error(new UnauthorizedException(ErrorCode.UNAUTHORIZED_ACCESS));
            });
    }
}

어댑터 구현의 차이점

  • FeignClient 버전: FeignClient를 사용한 어댑터는 HTTP 요청을 어노테이션으로 정의할 수 있어 더 간결했다. 동기적 상호작용에 잘 맞으며, Spring MVC 애플리케이션에서는 작성하고 관리하기가 훨씬 쉬웠다.
  • WebClient 버전: WebClient 접근 방식은 요청 흐름을 더 잘 제어할 수 있으며 완전히 비동기로 작동하지만, 응답, 오류, 요청 본문을 명시적으로 처리해야 한다. WebClient는 리액티브 환경에서 더 나은 자원 활용을 가능하게 하지만, 코드의 복잡성이 증가하는 단점이 있다.

0개의 댓글