TestForge Blog
← 전체 포스트

Spring Cloud Gateway 2.x vs 4.x vs WebFlux 완전 비교 — 무엇이 달라졌나

Spring Cloud Gateway 2.x vs 4.x vs Spring WebFlux Gateway 차이를 YAML 설정, 필터 구현, 성능, 선택 기준까지 실전 코드로 완전 비교합니다.

TestForge Team ·

버전 체계 이해

Spring Cloud Gateway 버전은 Spring Cloud 릴리즈 트레인에 묶여 있습니다.

Spring BootSpring CloudGateway 버전Java
2.6.x2021.0.x (Jubilee)3.1.x8+
2.7.x2021.0.x (Jubilee)3.1.x8+
3.0.x2022.0.x (Kilburn)4.0.x17+
3.1.x2022.0.x (Kilburn)4.0.x17+
3.2.x2023.0.x (Leyton)4.1.x21+
3.3.x2023.0.x (Leyton)4.1.x21+

관행적으로 “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 CircuitBreakerResilience4j CircuitBreaker
spring-cloud-starter-netflix-hystrixspring-cloud-starter-circuitbreaker-reactor-resilience4j
hystrix.command.* ymlresilience4j.circuitbreaker.* yml
corsConfigurationscors-configurations
allowedOriginsallowedOriginPatterns
Sleuth + ZipkinMicrometer Tracing + OTLP
AddRequestHeader (항상 덮어씀)AddRequestHeadersIfNotPresent (조건부)
RemoteAddr PredicateXForwardedRemoteAddr (프록시 환경)

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.xGateway 4.x순수 WebFlux
Java 최소1117 (권장 21)17 (권장 21)
역할프록시/Gateway프록시/Gateway앱 프레임워크
서킷 브레이커HystrixResilience4jResilience4j (직접 구성)
라우팅 설정YAML / Java DSLYAML / Java DSL코드 (@RestController)
Rate LimitingRedis (기본)Redis + requestedTokens직접 구현
CORS 설정corsConfigurationscors-configurations@CrossOrigin / Security
ObservabilitySleuth + ZipkinMicrometer + OTLPMicrometer + OTLP
필터GlobalFilterGlobalFilter (개선)WebFilter
MVC 지원없음Gateway MVC (4.1+)WebFlux 전용
Native Image제한완전 지원완전 지원
패키지javax.*jakarta.*jakarta.*
테스트WebTestClient + WireMockWebTestClient + WireMockWebTestClient + StepVerifier
주 사용처마이크로서비스 진입점마이크로서비스 진입점개별 백엔드 서비스

전형적인 조합

[Client]

[Spring Cloud Gateway 4.x]  ← JWT 인증, Rate Limit, 라우팅
    ↓              ↓               ↓
[WebFlux 서비스] [WebFlux 서비스] [WebFlux 서비스]
  User Service    Order Service   Payment Service

Gateway가 공통 횡단 관심사를 처리하고, 각 백엔드 서비스는 순수 WebFlux로 비즈니스 로직에 집중합니다.