Spring Boot를 사용한 분산 시스템에서 마이크로서비스 간의 상호작용은 매우 중요하다. 초기에는 Gateway와 인증 서버 간의 통신을 용이하게 하기 위해 FeignClient를 사용했다. 그러나 리액티브 스택인 Webflux를 사용해 구현해보고 싶었고, 그것이 내 사이드프로젝트에서의 인증서버, 게이트웨이가 구현 대상이 되었다 ! 하지만 처음보는 에러를 마주쳤다. 그래서 나는 FeignClient 대신 WebClient를 사용하게 되었는데… 이번 포스팅에서는 WebFlux와 MVC의 차이점, 각각의 클라이언트(FeignClient vs WebClient), 그리고 어댑터 패턴에서의 변경 사항에 대해 작성해보려한다 !!
이 문서를 참고했던게 구현하는데 도움이 많이됐다. 링크 첨부 합니다 ! ! !
https://github.com/OpenFeign/feign/tree/master/reactive
Spring MVC (Model-View-Controller)는 동기적, 블로킹 프레임워크다. 기존 라이브러리와의 통합이 쉬운 사용 사례나 간단한 데이터 처리를 필요로 하는 애플리케이션에 적합하다. MVC는 서블릿 기반 모델에 의존하며, 요청 생명주기 전체 동안 스레드를 유지하기 때문에 적당한 로드를 처리하는 데 적합하다.
Spring WebFlux는 리액티브, 논블로킹 프로그래밍을 위해 설계되었다. 많은 수의 동시 요청을 적은 스레드로 처리해야 하는 현대 클라우드 네이티브 애플리케이션에 적합하다. WebFlux는 Reactor가 제공하는 리액티브 모델을 사용하여 IO가 많은 작업에서 Spring MVC보다 훨씬 더 확장성이 뛰어나다 !
@RequestLine 같은 어노테이션을 사용하여 간결한 인터페이스로 HTTP 요청을 작성할 수 있다. FeignClient는 동기적으로 작동하며, Spring MVC 모델과 함께 사용하기에 적합하다.
@Controller 어노테이션을 사용할 수 있다.WebFlux 환경에서는 FeignClient와 충돌이 발생할 수 있다. 그 이유는 FeignClient가 동기적 방식으로 동작하기 때문이다. WebFlux는 논블로킹 리액티브 프로그래밍 모델을 사용하는데, FeignClient의 동기적 접근 방식은 이러한 리액티브 특성과 맞지 않다.
WebFlux는 리액티브 기반의 WAS(Netty)에서 실행되고, Spring MVC는 서블릿 기반 WAS(Tomcat)에서 주로 실행된다. 이처럼 두 기술은 실행되는 서버 환경 자체가 다르다!!! WebFlux는 많은 수의 동시 요청을 비동기적으로 처리할 수 있도록 설계되었고, FeignClient는 요청에 대해 응답이 올 때까지 기다리기 때문에, WebFlux의 논블로킹 특성을 제대로 활용하지 못하고 전체 애플리케이션의 성능을 저하시킬 수도 있다.
이러한 불일치로 인해 시스템에서는 예기치 않은 동작이나 성능 문제, 심지어는 블로킹 관련 충돌이 발생할 수 있다. 따라서 WebFlux를 사용하는 환경에서는 FeignClient 대신 WebClient와 같은 논블로킹 HTTP 클라이언트를 사용하는 것이 필수적인것이다 ! WebClient는 리액티브 방식으로 요청을 처리하고, WebFlux의 논블로킹 특성을 최대로 활용하여 더 나은 확장성과 성능을 제공한다.
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 필터에서 토큰을 갱신하기 위해 다음과 같이 사용되었다.
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));
});
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));
});
}
}