Spring Cloud Gateway 2.x vs 4.x vs WebFlux 완전 비교 — 무엇이 달라졌나
Spring Cloud Gateway 2.x vs 4.x vs Spring WebFlux Gateway 차이를 YAML 설정, 필터 구현, 성능, 선택 기준까지 실전 코드로 완전 비교합니다.
버전 체계 이해
Spring Cloud Gateway 버전은 Spring Cloud 릴리즈 트레인에 묶여 있습니다.
| Spring Boot | Spring Cloud | Gateway 버전 | Java |
|---|---|---|---|
| 2.6.x | 2021.0.x (Jubilee) | 3.1.x | 8+ |
| 2.7.x | 2021.0.x (Jubilee) | 3.1.x | 8+ |
| 3.0.x | 2022.0.x (Kilburn) | 4.0.x | 17+ |
| 3.1.x | 2022.0.x (Kilburn) | 4.0.x | 17+ |
| 3.2.x | 2023.0.x (Leyton) | 4.1.x | 21+ |
| 3.3.x | 2023.0.x (Leyton) | 4.1.x | 21+ |
관행적으로 “2.x”는 Boot 2.x + Gateway 3.x를, “최신”은 Boot 3.x + Gateway 4.x를 가리킵니다.
이 포스트에서도 2.x = Gateway 3.1 (Boot 2.7), 4.x = Gateway 4.1 (Boot 3.3) 기준으로 비교합니다.
1. 의존성 및 BOM 변경
2.x (Boot 2.7 + Gateway 3.1)
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
</parent>
<properties>
<spring-cloud.version>2021.0.9</spring-cloud.version>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<!-- 3.1.x 자동 선택 -->
</dependency>
<!-- Hystrix 서킷 브레이커 (deprecated) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
</dependencies>
4.x (Boot 3.3 + Gateway 4.1)
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.5</version>
</parent>
<properties>
<spring-cloud.version>2023.0.3</spring-cloud.version>
<java.version>21</java.version> <!-- Java 21 LTS 권장 -->
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<!-- 4.1.x 자동 선택 -->
</dependency>
<!-- Hystrix 완전 제거 → Resilience4j 사용 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>
</dependencies>
핵심 변경:
- Java 최소 버전: 11 → 17 (권장 21)
- Hystrix → Resilience4j (Hystrix 완전 제거)
javax.*→jakarta.*패키지 전환
2. YAML 설정 비교
기본 라우팅
# ─────────── 2.x (Gateway 3.1) ───────────
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/api/users/**
filters:
- StripPrefix=1
# ─────────── 4.x (Gateway 4.1) ───────────
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/api/users/**
filters:
- StripPrefix=1
# 기본 라우팅 문법은 동일 — 하위 호환 유지
서킷 브레이커 필터
# ─────────── 2.x — Hystrix ───────────
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/api/users/**
filters:
- name: Hystrix
args:
name: userServiceFallback
fallbackUri: forward:/fallback/user
# Hystrix 설정
hystrix:
command:
userServiceFallback:
execution:
isolation:
thread:
timeoutInMilliseconds: 3000
# ─────────── 4.x — Resilience4j ───────────
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/api/users/**
filters:
- name: CircuitBreaker
args:
name: userServiceCB
fallbackUri: forward:/fallback/user
statusCodes: # 4.x 신규: 특정 상태코드도 실패로 간주
- 500
- 503
# Resilience4j 설정 (4.x)
resilience4j:
circuitbreaker:
instances:
userServiceCB:
slidingWindowSize: 10
failureRateThreshold: 50
waitDurationInOpenState: 10s
permittedNumberOfCallsInHalfOpenState: 3
timelimiter:
instances:
userServiceCB:
timeoutDuration: 3s
Rate Limiting
# ─────────── 2.x ───────────
spring:
cloud:
gateway:
routes:
- id: api
uri: lb://API-SERVICE
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
# 2.x에는 requestedTokens 없음
key-resolver: "#{@ipKeyResolver}"
# ─────────── 4.x ───────────
spring:
cloud:
gateway:
routes:
- id: api
uri: lb://API-SERVICE
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
redis-rate-limiter.requestedTokens: 1 # 4.x 신규: 요청당 소비 토큰 수
key-resolver: "#{@userKeyResolver}"
Retry 필터
# ─────────── 2.x ───────────
filters:
- name: Retry
args:
retries: 3
series: SERVER_ERROR
methods: GET
# 2.x에는 exceptions, backoff 옵션 제한적
# ─────────── 4.x ───────────
filters:
- name: Retry
args:
retries: 3
series: SERVER_ERROR
methods: GET,POST
exceptions: # 4.x 신규: 예외 클래스 지정
- java.io.IOException
- java.util.concurrent.TimeoutException
backoff: # 4.x 신규: 지수 백오프 설정
firstBackoff: 10ms
maxBackoff: 500ms
factor: 2
basedOnPreviousValue: false
헤더 조작 필터
# ─────────── 2.x ───────────
filters:
- AddRequestHeader=X-Source, gateway
- AddResponseHeader=X-Response-Time, "#{T(System).currentTimeMillis()}"
- RemoveRequestHeader=X-Internal-Token
- RewritePath=/api/(?<segment>.*), /$\{segment}
# ─────────── 4.x (위 문법 모두 유지 + 신규 추가) ───────────
filters:
- AddRequestHeader=X-Source, gateway
- AddRequestHeadersIfNotPresent=X-Request-ID:{randomUuid} # 4.x 신규
- RemoveRequestHeader=X-Internal-Token
- RewritePath=/api/(?<segment>.*), /$\{segment}
- name: MapRequestHeader # 4.x 신규: 헤더 이름 매핑
args:
fromHeader: X-Old-Header
toHeader: X-New-Header
- TokenRelay= # OAuth2 토큰 자동 전달 (4.x 개선)
CORS 설정
# ─────────── 2.x ───────────
spring:
cloud:
gateway:
globalcors:
corsConfigurations: # 2.x: corsConfigurations (camelCase)
'[/**]':
allowedOrigins:
- "https://example.com"
allowedMethods:
- GET
- POST
# ─────────── 4.x ───────────
spring:
cloud:
gateway:
globalcors:
cors-configurations: # 4.x: cors-configurations (kebab-case)
'[/**]':
allowedOriginPatterns: # 4.x 권장: 패턴 매칭 지원
- "https://*.testforge.kr"
allowedMethods:
- GET
- POST
- PUT
- DELETE
- OPTIONS
allowCredentials: true
maxAge: 3600
3. Java 코드 변경점
패키지 변경 (javax → jakarta)
// ─────────── 2.x ───────────
import javax.annotation.PostConstruct;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
// ─────────── 4.x ───────────
import jakarta.annotation.PostConstruct;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
GlobalFilter 구현
// ─────────── 2.x ───────────
@Component
public class AuthFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// HttpStatus: org.springframework.http.HttpStatus
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
}
// ─────────── 4.x — API 동일, 내부 개선 ───────────
@Component
public class AuthFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 4.x: ProblemDetail 활용 가능 (RFC 7807)
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.UNAUTHORIZED, "인증 토큰이 필요합니다"
);
problem.setType(URI.create("https://testforge.kr/errors/unauthorized"));
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
exchange.getResponse().getHeaders()
.setContentType(MediaType.APPLICATION_PROBLEM_JSON); // 4.x RFC 7807
byte[] bytes = objectMapper.writeValueAsBytes(problem);
return exchange.getResponse()
.writeWith(Mono.just(exchange.getResponse()
.bufferFactory().wrap(bytes)));
}
}
RouteLocator
// ─────────── 2.x ───────────
@Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("user", r -> r
.path("/api/users/**")
.filters(f -> f
.hystrix(c -> c // 2.x: Hystrix
.setName("fallback")
.setFallbackUri("forward:/fallback")
)
)
.uri("lb://USER-SERVICE")
)
.build();
}
// ─────────── 4.x ───────────
@Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("user", r -> r
.path("/api/users/**")
.filters(f -> f
.circuitBreaker(c -> c // 4.x: Resilience4j
.setName("userServiceCB")
.setFallbackUri("forward:/fallback")
.addStatusCode("500") // 4.x 신규
)
.retry(config -> config // 4.x 개선된 Retry
.setRetries(3)
.setMethods(HttpMethod.GET)
.setBackoff(
Duration.ofMillis(10),
Duration.ofMillis(500),
2, false
)
)
)
.uri("lb://USER-SERVICE")
)
.build();
}
4. 4.x 신규 기능 상세
4-1. HttpClient SSL/TLS 개선
# 4.x: 세밀한 SSL 설정
spring:
cloud:
gateway:
httpclient:
ssl:
use-insecure-trust-manager: false # 프로덕션에서 false 필수
trusted-x509-certificates:
- cert/root-ca.crt
handshake-timeout: 10000 # ms
close-notify-flush-timeout: 3000
close-notify-read-timeout: 0
connect-timeout: 5000 # ms
response-timeout: 30s
max-header-size: 16KB
pool:
type: ELASTIC # ELASTIC | FIXED | DISABLED
max-connections: 500
acquire-timeout: 45000
max-idle-time: 15s
max-life-time: 60s
4-2. 가중치 기반 라우팅 (Weighted Routing)
카나리 배포에 유용합니다.
# 4.x 신규: 동일 그룹 내 가중치로 트래픽 분배
spring:
cloud:
gateway:
routes:
# 안정 버전 — 트래픽 90%
- id: user-service-v1
uri: lb://USER-SERVICE-V1
predicates:
- Path=/api/users/**
- Weight=userGroup, 90 # 그룹명, 가중치
# 신규 버전 — 트래픽 10% (카나리)
- id: user-service-v2
uri: lb://USER-SERVICE-V2
predicates:
- Path=/api/users/**
- Weight=userGroup, 10
배포 안정화 후 가중치를 점진적으로 조정합니다:
# Actuator API로 동적 라우트 변경 (재배포 없이)
POST /actuator/gateway/refresh
4-3. spring.cloud.gateway.mvc — MVC 지원 (4.1 신규)
4.1부터 WebFlux 없이 Spring MVC 위에서도 Gateway 사용 가능합니다.
<!-- 4.1 신규: MVC 기반 Gateway -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway-mvc</artifactId>
<!-- spring-boot-starter-web 기반 (WebFlux 불필요) -->
</dependency>
# MVC Gateway 설정 (4.1+)
spring:
cloud:
gateway:
mvc:
routes:
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/api/users/**
언제 MVC Gateway를 선택하나?
- 기존 MVC 프로젝트에 Gateway 기능만 추가할 때
- 블로킹 I/O가 많은 레거시 환경
- WebFlux 학습 비용이 부담될 때
단점: 논블로킹 처리량이 WebFlux Gateway보다 낮음.
4-4. AddRequestHeadersIfNotPresent 필터 (4.0 신규)
# 4.x 신규: 헤더가 없을 때만 추가
filters:
- AddRequestHeadersIfNotPresent=X-Request-ID:{randomUuid},X-Source:gateway
# 클라이언트가 이미 X-Request-ID를 보냈다면 덮어쓰지 않음
# 2.x의 AddRequestHeader는 항상 덮어씀
4-5. CacheRequestBody 필터 개선
# 4.x: 요청 body를 캐시해서 필터 체인에서 여러 번 읽기 가능
filters:
- name: CacheRequestBody
args:
bodyClass: com.example.dto.OrderRequest
- name: ModifyRequestBody
args:
inClass: com.example.dto.OrderRequest
outClass: com.example.dto.EnrichedOrderRequest
rewriteFunction: com.example.filter.OrderEnricher
4-6. TokenRelay OAuth2 개선
# 2.x: TokenRelay 설정 복잡
# 4.x: 자동 토큰 전달 간소화
spring:
cloud:
gateway:
routes:
- id: resource-service
uri: lb://RESOURCE-SERVICE
predicates:
- Path=/api/resource/**
filters:
- TokenRelay= # 업스트림으로 OAuth2 AccessToken 자동 전달
security:
oauth2:
client:
registration:
gateway-client:
client-id: gateway
client-secret: ${OAUTH2_SECRET}
authorization-grant-type: client_credentials
scope: read,write
provider:
auth-server:
token-uri: https://auth.testforge.kr/oauth/token
4-7. Observability 통합 (4.x)
# 4.x: Micrometer + OpenTelemetry 네이티브 통합
management:
tracing:
sampling:
probability: 1.0 # 100% 샘플링 (프로덕션에서 0.1 권장)
otlp:
tracing:
endpoint: http://jaeger:4318/v1/traces
metrics:
tags:
application: api-gateway
endpoints:
web:
exposure:
include: health,metrics,gateway,prometheus
<!-- 4.x Observability 의존성 -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>
2.x에서는 Zipkin/Sleuth를 별도로 구성해야 했지만, 4.x는 자동 설정으로 Jaeger/Tempo/Zipkin 연동됩니다.
4-8. ProxyExchange 개선 (MVC Gateway 전용)
// 4.1 MVC Gateway: ProxyExchange로 유연한 프록시 처리
@RestController
public class ProxyController {
@GetMapping("/api/users/**")
public ResponseEntity<?> proxyToUser(ProxyExchange<byte[]> proxy) {
return proxy
.uri("http://user-service" + proxy.path("/api/users/"))
.header("X-Gateway-Source", "mvc-gateway")
.get();
}
}
5. Predicate 변경 및 추가
# ─── 2.x에서도 지원되는 Predicate ───
predicates:
- Path=/api/**
- Method=GET,POST
- Header=X-Custom-Header, \d+
- Query=param, value
- Cookie=session, .*
- Host=**.testforge.kr
- After=2026-01-01T00:00:00+09:00[Asia/Seoul]
- Before=2026-12-31T23:59:59+09:00[Asia/Seoul]
- Between=2026-01-01T00:00:00+09:00,2026-12-31T23:59:59+09:00
- RemoteAddr=192.168.1.0/24
# ─── 4.x 신규 Predicate ───
predicates:
- Weight=groupName, 80 # 가중치 기반 라우팅
- XForwardedRemoteAddr=192.168.0.0/16 # 4.x: X-Forwarded-For 기반 IP 제한
# 2.x RemoteAddr는 프록시 뒤에서 부정확
6. 보안 설정 변경
Spring Security 통합
// ─────────── 2.x ───────────
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
return http
.csrf().disable()
.authorizeExchange()
.pathMatchers("/actuator/health").permitAll()
.anyExchange().authenticated()
.and()
.build();
}
}
// ─────────── 4.x — 람다 DSL 방식 ───────────
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
return http
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.authorizeExchange(auth -> auth
.pathMatchers("/actuator/health", "/fallback/**").permitAll()
.pathMatchers("/actuator/**").hasRole("ADMIN")
.anyExchange().authenticated()
)
.build();
}
}
7. 마이그레이션 체크리스트 (2.x → 4.x)
필수 변경
✅ Spring Boot 2.7.x → 3.3.x
✅ Java 11 → 17 이상 (권장 21)
✅ javax.* → jakarta.* 전체 교체
✅ spring-cloud.version 2021.0.x → 2023.0.x
✅ Hystrix 제거 → Resilience4j로 교체
✅ corsConfigurations → cors-configurations (kebab-case)
✅ Security DSL: .csrf().disable() → .csrf(spec::disable)
✅ allowedOrigins → allowedOriginPatterns (패턴 지원)
YAML 마이그레이션 자동화
# Spring Boot 3.x 마이그레이션 도우미
# OpenRewrite 플러그인 사용
mvn rewrite:run \
-Drewrite.activeRecipes=org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_3
기능별 대응표
| 2.x 기능 | 4.x 대응 |
|---|---|
| Hystrix CircuitBreaker | Resilience4j CircuitBreaker |
spring-cloud-starter-netflix-hystrix | spring-cloud-starter-circuitbreaker-reactor-resilience4j |
hystrix.command.* yml | resilience4j.circuitbreaker.* yml |
corsConfigurations | cors-configurations |
allowedOrigins | allowedOriginPatterns |
| Sleuth + Zipkin | Micrometer Tracing + OTLP |
AddRequestHeader (항상 덮어씀) | AddRequestHeadersIfNotPresent (조건부) |
RemoteAddr Predicate | XForwardedRemoteAddr (프록시 환경) |
8. 성능 비교
| 항목 | 2.x (Boot 2.7) | 4.x (Boot 3.3) |
|---|---|---|
| 기동 시간 | ~3s | ~1.5s (GraalVM Native 지원) |
| 메모리 사용 | ~350MB | ~280MB |
| 처리량 (RPS) | 기준 | +15~20% |
| GraalVM Native | 제한적 | 완전 지원 |
| Virtual Thread | 미지원 | 지원 (MVC Gateway) |
GraalVM Native Image (4.x)
<!-- 4.x: Native Image 지원 -->
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
</plugin>
mvn -Pnative native:compile
# 결과: 기동 시간 ~100ms, 메모리 ~80MB
9. 전체 YAML 비교 (같은 구성)
# ═══════════════════════════════════════
# 2.x 전체 설정 예시 (Boot 2.7, Gateway 3.1)
# ═══════════════════════════════════════
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
globalcors:
corsConfigurations: # camelCase
'[/**]':
allowedOrigins:
- "https://testforge.kr"
allowedMethods: [GET, POST, PUT, DELETE, OPTIONS]
allowedHeaders: ["*"]
allowCredentials: true
routes:
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/api/v1/users/**
filters:
- StripPrefix=2
- name: Hystrix # Hystrix
args:
name: userFallback
fallbackUri: forward:/fallback/user
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
key-resolver: "#{@ipKeyResolver}"
- name: Retry
args:
retries: 3
series: SERVER_ERROR
methods: GET
hystrix:
command:
userFallback:
execution:
isolation:
thread:
timeoutInMilliseconds: 3000
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
management:
endpoints:
web:
exposure:
include: health,gateway
---
# ═══════════════════════════════════════
# 4.x 전체 설정 예시 (Boot 3.3, Gateway 4.1)
# ═══════════════════════════════════════
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
globalcors:
cors-configurations: # kebab-case
'[/**]':
allowedOriginPatterns: # 패턴 매칭
- "https://*.testforge.kr"
allowedMethods: [GET, POST, PUT, DELETE, OPTIONS]
allowedHeaders: ["*"]
allowCredentials: true
maxAge: 3600
httpclient: # 4.x: HttpClient 세밀 설정
connect-timeout: 5000
response-timeout: 30s
pool:
type: ELASTIC
max-connections: 500
routes:
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/api/v1/users/**
- Weight=userGroup, 90 # 4.x: 가중치
filters:
- StripPrefix=2
- AddRequestHeadersIfNotPresent=X-Request-ID:{randomUuid} # 4.x 신규
- name: CircuitBreaker # Resilience4j
args:
name: userServiceCB
fallbackUri: forward:/fallback/user
statusCodes: [500, 503] # 4.x 신규
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
redis-rate-limiter.requestedTokens: 1 # 4.x 신규
key-resolver: "#{@userKeyResolver}"
- name: Retry
args:
retries: 3
series: SERVER_ERROR
methods: GET
backoff: # 4.x 신규: 지수 백오프
firstBackoff: 10ms
maxBackoff: 500ms
factor: 2
resilience4j: # Hystrix 대신 Resilience4j
circuitbreaker:
instances:
userServiceCB:
slidingWindowSize: 10
failureRateThreshold: 50
waitDurationInOpenState: 10s
timelimiter:
instances:
userServiceCB:
timeoutDuration: 3s
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
management:
tracing:
sampling:
probability: 0.1 # 4.x: Micrometer Tracing
endpoints:
web:
exposure:
include: health,gateway,metrics,prometheus
10. Spring Cloud Gateway vs 순수 WebFlux — 무엇이 다른가
Spring Cloud Gateway는 WebFlux 위에 구축된 Gateway 특화 레이어입니다.
같은 Reactor Netty 엔진을 쓰지만, 역할과 구성 방식이 완전히 다릅니다.
구조 비교
순수 WebFlux:
Client → Reactor Netty → DispatcherHandler → @RestController / RouterFunction → 비즈니스 로직
Spring Cloud Gateway:
Client → Reactor Netty → Gateway Handler Mapping
→ Predicate 매칭
→ Pre Filter 체인
→ Proxied 백엔드 서비스 (HTTP 프록시)
→ Post Filter 체인
→ Client Response
Gateway는 자체 비즈니스 로직을 처리하지 않고 다른 서비스로 요청을 전달하는 것이 목적입니다.
WebFlux는 직접 비즈니스 로직을 구현하는 애플리케이션 프레임워크입니다.
의존성 비교
<!-- 순수 Spring WebFlux 앱 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<!-- Reactor Core + Reactor Netty + Spring WebFlux -->
</dependency>
<!-- Spring Cloud Gateway (WebFlux 기반) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<!-- 내부적으로 spring-boot-starter-webflux 포함 -->
<!-- + 라우팅 엔진, 필터 체인, Predicate 등 추가 -->
</dependency>
<!-- Spring Cloud Gateway MVC (4.1 신규, WebFlux 불필요) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway-mvc</artifactId>
<!-- spring-boot-starter-web 기반 -->
</dependency>
YAML 설정 방식 비교
# ══════════════════════════════════════════════
# 순수 WebFlux — YAML에 라우팅 설정 없음
# 라우팅은 @RestController 또는 RouterFunction으로 코드 작성
# ══════════════════════════════════════════════
server:
port: 8080
spring:
application:
name: user-service
r2dbc:
url: r2dbc:postgresql://localhost:5432/mydb
username: user
password: pass
# ══════════════════════════════════════════════
# Spring Cloud Gateway 4.x — YAML에서 라우팅 선언
# 비즈니스 로직 없이 프록시 규칙만 정의
# ══════════════════════════════════════════════
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: user-service
uri: lb://USER-SERVICE # 백엔드 서비스로 프록시
predicates:
- Path=/api/v1/users/**
filters:
- StripPrefix=2
- name: CircuitBreaker
args:
name: userServiceCB
fallbackUri: forward:/fallback/user
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 100
redis-rate-limiter.burstCapacity: 200
key-resolver: "#{@userKeyResolver}"
컨트롤러 / 라우팅 코드 비교
// ══════════════════════════════════════════
// 순수 WebFlux — 직접 비즈니스 로직 처리
// ══════════════════════════════════════════
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserRepository userRepository;
@GetMapping("/{id}")
public Mono<ResponseEntity<UserDto>> getUser(@PathVariable Long id) {
return userRepository.findById(id) // R2DBC 논블로킹 DB 조회
.map(user -> ResponseEntity.ok(UserDto.from(user)))
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Mono<UserDto> createUser(@RequestBody @Valid Mono<CreateUserRequest> req) {
return req.flatMap(userRepository::save).map(UserDto::from);
}
}
// ══════════════════════════════════════════
// Spring Cloud Gateway — 프록시 라우팅만 정의
// 비즈니스 로직은 백엔드 USER-SERVICE가 처리
// ══════════════════════════════════════════
@Configuration
public class GatewayRouteConfig {
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("user-service", r -> r
.path("/api/v1/users/**")
.filters(f -> f
.stripPrefix(2)
.circuitBreaker(c -> c
.setName("userServiceCB")
.setFallbackUri("forward:/fallback/user")
)
)
.uri("lb://USER-SERVICE") // 실제 처리는 USER-SERVICE가
)
.build();
}
}
필터 구현 방식 비교
// ══════════════════════════════════════════
// 순수 WebFlux — WebFilter로 횡단 관심사 처리
// ══════════════════════════════════════════
@Component
@Order(-1)
public class RequestLoggingFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
log.info("→ {} {}", exchange.getRequest().getMethod(),
exchange.getRequest().getPath());
return chain.filter(exchange)
.doFinally(signal ->
log.info("← {}", exchange.getResponse().getStatusCode())
);
}
}
// ══════════════════════════════════════════
// Spring Cloud Gateway — GlobalFilter로 처리
// API는 같은 WebFilter 기반이지만 Gateway 전용 API 추가
// ══════════════════════════════════════════
@Component
@Order(-1)
public class RequestLoggingFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// GatewayFilterChain: Gateway 전용 (WebFilterChain과 다름)
// exchange.getAttributes()로 Gateway 메타데이터 접근 가능
Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
log.info("→ {} {} (route: {})",
exchange.getRequest().getMethod(),
exchange.getRequest().getPath(),
route != null ? route.getId() : "unknown"
);
return chain.filter(exchange);
}
}
WebClient 사용 비교
// ══════════════════════════════════════════
// 순수 WebFlux 앱에서 외부 서비스 호출
// WebClient로 직접 HTTP 요청
// ══════════════════════════════════════════
@Service
@RequiredArgsConstructor
public class OrderService {
private final WebClient.Builder webClientBuilder;
public Mono<List<OrderDto>> getOrders(Long userId) {
return webClientBuilder
.baseUrl("http://order-service")
.build()
.get()
.uri("/orders?userId={id}", userId)
.retrieve()
.bodyToFlux(OrderDto.class)
.collectList();
}
}
// ══════════════════════════════════════════
// Spring Cloud Gateway에서는 WebClient 직접 호출 X
// 라우팅 설정으로 요청을 자동 프록시
// ══════════════════════════════════════════
# application.yml
spring:
cloud:
gateway:
routes:
- id: order-service
uri: lb://ORDER-SERVICE # Gateway가 자동으로 프록시
predicates:
- Path=/api/v1/orders/**
에러 처리 비교
// ══════════════════════════════════════════
// 순수 WebFlux — @ExceptionHandler 또는 WebExceptionHandler
// ══════════════════════════════════════════
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(EntityNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Mono<ErrorResponse> handleNotFound(EntityNotFoundException e) {
return Mono.just(new ErrorResponse("NOT_FOUND", e.getMessage()));
}
@ExceptionHandler(WebExchangeBindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Mono<ErrorResponse> handleValidation(WebExchangeBindException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
.collect(Collectors.joining(", "));
return Mono.just(new ErrorResponse("VALIDATION_FAILED", message));
}
}
// ══════════════════════════════════════════
// Spring Cloud Gateway — DefaultErrorWebExceptionHandler 커스터마이징
// 백엔드 서비스 장애 시 Fallback 처리
// ══════════════════════════════════════════
@RestController
@RequestMapping("/fallback")
public class FallbackController {
// 서킷 브레이커가 열렸을 때 호출
@GetMapping("/user")
public Mono<ResponseEntity<Map<String, String>>> userFallback(
ServerWebExchange exchange) {
// Gateway 메타데이터에서 원인 확인 가능
Throwable cause = exchange.getAttribute(
ServerWebExchangeUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR
);
return Mono.just(ResponseEntity
.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(Map.of(
"error", "USER_SERVICE_UNAVAILABLE",
"cause", cause != null ? cause.getMessage() : "unknown",
"message", "잠시 후 다시 시도해주세요"
))
);
}
}
테스트 방식 비교
// ══════════════════════════════════════════
// 순수 WebFlux 테스트
// ══════════════════════════════════════════
@SpringBootTest
@AutoConfigureWebTestClient
class UserControllerTest {
@Autowired
private WebTestClient webTestClient;
@Test
void getUser_returnsUser() {
webTestClient.get()
.uri("/api/v1/users/1")
.exchange()
.expectStatus().isOk()
.expectBody(UserDto.class)
.value(dto -> assertThat(dto.name()).isEqualTo("홍길동"));
}
}
// StepVerifier로 리액티브 스트림 테스트
class UserServiceTest {
@Test
void findById_returnsUser() {
StepVerifier.create(userService.findById(1L))
.expectNextMatches(u -> u.id().equals(1L))
.verifyComplete();
}
}
// ══════════════════════════════════════════
// Spring Cloud Gateway 테스트
// ══════════════════════════════════════════
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class GatewayRoutingTest {
@Autowired
private WebTestClient webTestClient;
@MockBean
private RouteLocator routeLocator; // 실제 백엔드 없이 라우트 테스트
// WireMock으로 백엔드 서비스 목킹
@RegisterExtension
static WireMockExtension wireMock = WireMockExtension.newInstance()
.options(wireMockConfig().dynamicPort())
.build();
@Test
void route_toUserService_proxiesCorrectly() {
wireMock.stubFor(get("/users/1")
.willReturn(okJson("""
{"id": 1, "name": "홍길동"}
""")));
webTestClient.get()
.uri("/api/v1/users/1")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$.name").isEqualTo("홍길동");
}
@Test
void circuitBreaker_fallback_returns503() {
wireMock.stubFor(get("/users/1")
.willReturn(serverError())); // 백엔드 오류 시뮬레이션
webTestClient.get()
.uri("/api/v1/users/1")
.exchange()
.expectStatus().isEqualTo(HttpStatus.SERVICE_UNAVAILABLE);
}
}
언제 무엇을 선택해야 하나
질문 1: 요청을 직접 처리하는가, 아니면 다른 서비스로 전달하는가?
→ 직접 처리 (DB 조회, 비즈니스 로직) : 순수 WebFlux
→ 다른 서비스로 프록시 : Spring Cloud Gateway
질문 2: 팀 규모와 서비스 수?
→ 서비스 1~3개, 소규모 팀 : WebFlux 직접 구성
→ 서비스 5개+, 마이크로서비스 : Gateway + WebFlux 서비스들
질문 3: 공통 관심사 (인증, Rate Limit) 처리 위치?
→ 각 서비스 개별 처리 : 코드 중복 발생
→ Gateway 집중 처리 : Spring Cloud Gateway 적합
최종 요약 — 3자 비교표
| 항목 | Gateway 2.x | Gateway 4.x | 순수 WebFlux |
|---|---|---|---|
| Java 최소 | 11 | 17 (권장 21) | 17 (권장 21) |
| 역할 | 프록시/Gateway | 프록시/Gateway | 앱 프레임워크 |
| 서킷 브레이커 | Hystrix | Resilience4j | Resilience4j (직접 구성) |
| 라우팅 설정 | YAML / Java DSL | YAML / Java DSL | 코드 (@RestController) |
| Rate Limiting | Redis (기본) | Redis + requestedTokens | 직접 구현 |
| CORS 설정 | corsConfigurations | cors-configurations | @CrossOrigin / Security |
| Observability | Sleuth + Zipkin | Micrometer + OTLP | Micrometer + OTLP |
| 필터 | GlobalFilter | GlobalFilter (개선) | WebFilter |
| MVC 지원 | 없음 | Gateway MVC (4.1+) | WebFlux 전용 |
| Native Image | 제한 | 완전 지원 | 완전 지원 |
| 패키지 | javax.* | jakarta.* | jakarta.* |
| 테스트 | WebTestClient + WireMock | WebTestClient + WireMock | WebTestClient + StepVerifier |
| 주 사용처 | 마이크로서비스 진입점 | 마이크로서비스 진입점 | 개별 백엔드 서비스 |
전형적인 조합
[Client]
↓
[Spring Cloud Gateway 4.x] ← JWT 인증, Rate Limit, 라우팅
↓ ↓ ↓
[WebFlux 서비스] [WebFlux 서비스] [WebFlux 서비스]
User Service Order Service Payment Service
Gateway가 공통 횡단 관심사를 처리하고, 각 백엔드 서비스는 순수 WebFlux로 비즈니스 로직에 집중합니다.