Spring Cloud Gateway 실전

Spring Cloud Gateway란?

Spring Cloud Gateway는 마이크로서비스 아키텍처의 API Gateway를 구현하는 Spring 공식 프로젝트다. WebFlux 기반 논블로킹으로 동작하며, 라우팅, 필터링, 로드밸런싱, 인증, Rate Limiting을 하나의 진입점에서 처리한다. Netflix Zuul의 후속으로, 리액티브 스택 위에 구축되어 높은 동시성을 처리할 수 있다.

이 글에서는 Route/Predicate/Filter 3요소, Java DSL과 YAML 설정, 커스텀 필터, 서킷브레이커 통합, Rate Limiter, 그리고 Kubernetes 환경 배포까지 실무 패턴을 다룬다.

핵심 구조: Route · Predicate · Filter

구성 요소 역할 예시
Route 라우팅 규칙 정의 id, uri, predicates, filters
Predicate 요청 매칭 조건 Path, Header, Method, Host
Filter 요청/응답 변환 AddHeader, StripPrefix, Retry

요청 흐름: 클라이언트 → Predicate 매칭Pre Filter 체인 → 다운스트림 서비스 → Post Filter 체인 → 클라이언트

YAML 기반 라우팅 설정

# application.yml
spring:
  cloud:
    gateway:
      routes:
        # 사용자 서비스
        - id: user-service
          uri: lb://user-service        # 서비스 디스커버리 로드밸런싱
          predicates:
            - Path=/api/users/**
            - Method=GET,POST,PUT,DELETE
          filters:
            - StripPrefix=1              # /api/users → /users
            - AddRequestHeader=X-Gateway, true
            - name: CircuitBreaker
              args:
                name: userCB
                fallbackUri: forward:/fallback/users

        # 주문 서비스
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/orders/**
            - Header=Authorization, Bearer.*   # JWT 필수
          filters:
            - StripPrefix=1
            - name: Retry
              args:
                retries: 3
                statuses: SERVICE_UNAVAILABLE
                backoff:
                  firstBackoff: 100ms
                  maxBackoff: 500ms

        # 정적 리소스 (S3/CDN)
        - id: static-assets
          uri: https://cdn.example.com
          predicates:
            - Path=/assets/**
          filters:
            - SetResponseHeader=Cache-Control, public, max-age=86400

      # 글로벌 필터
      default-filters:
        - DedupeResponseHeader=Access-Control-Allow-Origin
        - AddResponseHeader=X-Response-Time, ${spring.cloud.gateway.response-time}

Java DSL 라우팅

복잡한 조건 로직이 필요할 때는 YAML 대신 Java DSL을 사용한다.

@Configuration
public class GatewayConfig {

    @Bean
    public RouteLocator customRoutes(RouteLocatorBuilder builder) {
        return builder.routes()
            // API 버전별 라우팅
            .route("user-v1", r -> r
                .path("/api/v1/users/**")
                .and().header("Accept", "application/vnd.myapp.v1\+json")
                .filters(f -> f
                    .stripPrefix(2)
                    .addRequestHeader("X-Api-Version", "1")
                    .circuitBreaker(cb -> cb
                        .setName("userV1CB")
                        .setFallbackUri("forward:/fallback/users"))
                )
                .uri("lb://user-service-v1")
            )
            // 카나리 배포: 10% 트래픽을 v2로
            .route("user-v2-canary", r -> r
                .path("/api/v1/users/**")
                .and().weight("user-group", 10)
                .filters(f -> f
                    .stripPrefix(2)
                    .addRequestHeader("X-Api-Version", "2"))
                .uri("lb://user-service-v2")
            )
            .route("user-v1-stable", r -> r
                .path("/api/v1/users/**")
                .and().weight("user-group", 90)
                .filters(f -> f.stripPrefix(2))
                .uri("lb://user-service-v1")
            )
            .build();
    }
}

커스텀 Global Filter: 인증·로깅·추적

실무 게이트웨이의 핵심은 커스텀 필터다. 인증 토큰 검증, 요청 로깅, 분산 추적 ID 주입을 구현한다.

@Component
@Order(-1)  // 가장 먼저 실행
public class AuthGlobalFilter implements GlobalFilter {

    private final JwtDecoder jwtDecoder;
    private final Set<String> publicPaths = Set.of(
        "/api/public", "/api/auth/login", "/actuator/health"
    );

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String path = exchange.getRequest().getURI().getPath();

        // 공개 경로는 인증 스킵
        if (publicPaths.stream().anyMatch(path::startsWith)) {
            return chain.filter(exchange);
        }

        String authHeader = exchange.getRequest().getHeaders()
            .getFirst(HttpHeaders.AUTHORIZATION);

        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            return unauthorized(exchange, "Missing token");
        }

        try {
            Jwt jwt = jwtDecoder.decode(authHeader.substring(7));
            // 다운스트림으로 사용자 정보 전달
            ServerHttpRequest mutated = exchange.getRequest().mutate()
                .header("X-User-Id", jwt.getSubject())
                .header("X-User-Roles", 
                    String.join(",", jwt.getClaimAsStringList("roles")))
                .build();

            return chain.filter(exchange.mutate().request(mutated).build());
        } catch (JwtException e) {
            return unauthorized(exchange, "Invalid token");
        }
    }

    private Mono<Void> unauthorized(ServerWebExchange exchange, String message) {
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
        exchange.getResponse().getHeaders()
            .setContentType(MediaType.APPLICATION_JSON);
        byte[] bytes = ("{"error":"" + message + ""}").getBytes();
        return exchange.getResponse()
            .writeWith(Mono.just(exchange.getResponse()
                .bufferFactory().wrap(bytes)));
    }
}

// 요청/응답 로깅 + 응답 시간 측정
@Component
@Order(0)
public class LoggingGlobalFilter implements GlobalFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        long start = System.currentTimeMillis();
        String traceId = UUID.randomUUID().toString().substring(0, 8);

        ServerHttpRequest request = exchange.getRequest().mutate()
            .header("X-Trace-Id", traceId)
            .build();

        log.info("[{}] {} {} from {}", traceId,
            request.getMethod(), request.getURI().getPath(),
            request.getRemoteAddress());

        return chain.filter(exchange.mutate().request(request).build())
            .then(Mono.fromRunnable(() -> {
                long duration = System.currentTimeMillis() - start;
                HttpStatusCode status = exchange.getResponse().getStatusCode();
                log.info("[{}] {} {}ms", traceId, status, duration);
            }));
    }
}

Redis Rate Limiter

Spring Cloud Gateway는 Redis 기반 분산 Rate Limiter를 내장하고 있다. Token Bucket 알고리즘으로 동작한다.

# application.yml
spring:
  cloud:
    gateway:
      routes:
        - id: api-rate-limited
          uri: lb://api-service
          predicates:
            - Path=/api/**
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10    # 초당 10개 토큰 충전
                redis-rate-limiter.burstCapacity: 20     # 최대 20개 burst
                redis-rate-limiter.requestedTokens: 1    # 요청당 1 토큰
                key-resolver: "#{@userKeyResolver}"
// KeyResolver: 누구를 기준으로 제한할 것인가
@Configuration
public class RateLimiterConfig {

    // 사용자 ID 기반
    @Bean
    public KeyResolver userKeyResolver() {
        return exchange -> {
            String userId = exchange.getRequest().getHeaders()
                .getFirst("X-User-Id");
            return Mono.justOrEmpty(userId)
                .defaultIfEmpty(
                    exchange.getRequest().getRemoteAddress()
                        .getAddress().getHostAddress());
        };
    }

    // API 키 기반
    @Bean
    public KeyResolver apiKeyResolver() {
        return exchange -> Mono.justOrEmpty(
            exchange.getRequest().getHeaders().getFirst("X-API-Key")
        );
    }

    // 경로별 제한
    @Bean
    public KeyResolver pathKeyResolver() {
        return exchange -> Mono.just(
            exchange.getRequest().getURI().getPath());
    }
}

서킷브레이커 통합

# Resilience4j 서킷브레이커 설정
resilience4j:
  circuitbreaker:
    instances:
      userCB:
        slidingWindowSize: 10
        failureRateThreshold: 50
        waitDurationInOpenState: 30s
        permittedNumberOfCallsInHalfOpenState: 3
  timelimiter:
    instances:
      userCB:
        timeoutDuration: 3s
// 폴백 컨트롤러
@RestController
@RequestMapping("/fallback")
public class FallbackController {

    @GetMapping("/users")
    public ResponseEntity<Map<String, String>> userFallback() {
        return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
            .body(Map.of(
                "message", "User service is temporarily unavailable",
                "fallback", "true"
            ));
    }

    @GetMapping("/orders")
    public ResponseEntity<Map<String, String>> orderFallback() {
        return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
            .body(Map.of(
                "message", "Order service is temporarily unavailable",
                "fallback", "true"
            ));
    }
}

CORS 설정

spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]':
            allowedOrigins:
              - "https://app.example.com"
              - "https://admin.example.com"
            allowedMethods:
              - GET
              - POST
              - PUT
              - DELETE
              - OPTIONS
            allowedHeaders: "*"
            exposedHeaders:
              - X-RateLimit-Remaining
              - X-Trace-Id
            allowCredentials: true
            maxAge: 3600

Spring Cloud Gateway vs K8s Ingress

기준 Spring Cloud Gateway K8s Ingress (Nginx)
레이어 애플리케이션 레벨 (L7) 인프라 레벨 (L7)
커스텀 로직 Java/Kotlin으로 자유로움 annotation/lua 스크립트 제한적
서비스 디스커버리 Eureka/Consul/K8s K8s Service 자동
서킷브레이커 Resilience4j 네이티브 통합 별도 구현 필요
적합 사례 복잡한 비즈니스 라우팅, API 조합 단순 라우팅, TLS 종단

실무에서는 둘을 함께 사용하는 것이 일반적이다. K8s Ingress로 TLS 종단과 기본 라우팅을 처리하고, Spring Cloud Gateway에서 인증·서킷브레이커·Rate Limiting 등 비즈니스 로직을 담당한다.

운영 체크리스트

  • 타임아웃: 글로벌 connect-timeoutresponse-timeout을 설정하여 느린 다운스트림이 게이트웨이를 블로킹하지 않도록 한다
  • 메모리: WebFlux 기반이므로 Netty 이벤트 루프 스레드에서 블로킹 코드를 실행하지 않는다. DB 조회가 필요한 필터는 Schedulers.boundedElastic()으로 격리한다
  • 헬스체크: /actuator/gateway/routes로 현재 라우트 목록을 확인하고, /actuator/health로 게이트웨이 상태를 모니터링한다
  • Rate Limiter 응답 헤더: X-RateLimit-Remaining, X-RateLimit-Burst-Capacity 헤더가 자동 포함된다. 클라이언트에게 남은 한도를 알려준다
  • 모니터링: Micrometer로 라우트별 응답 시간, 에러율, 서킷브레이커 상태를 대시보드에 노출한다
  • 보안: 게이트웨이가 유일한 진입점이 되도록 다운스트림 서비스는 내부 네트워크에서만 접근 가능하게 설정한다

Spring Cloud Gateway는 마이크로서비스의 단일 진입점으로서, 라우팅·인증·Rate Limiting·서킷브레이커를 통합 관리한다. WebFlux 기반이므로 높은 동시성을 처리할 수 있고, Java DSL로 복잡한 비즈니스 라우팅도 구현할 수 있다. 핵심은 커스텀 Global Filter로 횡단 관심사를 중앙화하고, Redis Rate Limiter와 서킷브레이커로 다운스트림을 보호하는 것이다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux