Spring Cloud Gateway 심화

Spring Cloud Gateway란?

Spring Cloud Gateway는 Spring WebFlux 기반의 API 게이트웨이입니다. 마이크로서비스 앞단에서 라우팅, 인증, 속도 제한, 로드밸런싱, 요청/응답 변환을 처리합니다. Netflix Zuul의 후속으로, 논블로킹 아키텍처 덕분에 높은 동시성을 처리할 수 있습니다.

API 게이트웨이는 클라이언트가 수십 개의 마이크로서비스 엔드포인트를 직접 알 필요 없이, 단일 진입점을 통해 접근하게 합니다.

핵심 개념 3가지

개념 역할 예시
Route 요청을 어디로 보낼지 결정 /api/users/** → user-service
Predicate 요청이 라우트에 매칭되는 조건 Path, Header, Method, Cookie 등
Filter 요청/응답을 가공 헤더 추가, 경로 재작성, 인증 검증

요청 흐름: Client → Predicate 매칭 → Pre Filter → 백엔드 서비스 → Post Filter → Client

라우팅 설정: YAML vs Java DSL

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/1 → /users/1
            - AddRequestHeader=X-Gateway, true

        - id: order-service
          uri: lb://ORDER-SERVICE
          predicates:
            - Path=/api/orders/**
            - Header=X-Api-Version, v2    # 특정 헤더가 있을 때만
          filters:
            - StripPrefix=1
            - name: CircuitBreaker        # 서킷 브레이커
              args:
                name: orderCB
                fallbackUri: forward:/fallback/orders

        - id: websocket-route
          uri: lb:ws://NOTIFICATION-SERVICE
          predicates:
            - Path=/ws/**

Java DSL (프로그래밍 방식)

@Configuration
public class GatewayConfig {

    @Bean
    public RouteLocator customRoutes(RouteLocatorBuilder builder) {
        return builder.routes()
            .route("user-service", r -> r
                .path("/api/users/**")
                .and().method(HttpMethod.GET, HttpMethod.POST)
                .filters(f -> f
                    .stripPrefix(1)
                    .addRequestHeader("X-Gateway", "true")
                    .retry(config -> config
                        .setRetries(3)
                        .setStatuses(HttpStatus.SERVICE_UNAVAILABLE)
                        .setBackoff(Duration.ofMillis(100), Duration.ofSeconds(1), 2, true))
                )
                .uri("lb://USER-SERVICE"))
            .route("rate-limited", r -> r
                .path("/api/public/**")
                .filters(f -> f
                    .requestRateLimiter(config -> config
                        .setRateLimiter(redisRateLimiter())
                        .setKeyResolver(ipKeyResolver())))
                .uri("lb://PUBLIC-SERVICE"))
            .build();
    }
}

커스텀 GatewayFilter: JWT 인증

가장 흔한 게이트웨이 필터는 JWT 인증입니다. 각 마이크로서비스에서 인증 로직을 반복하지 않고, 게이트웨이에서 한 번에 처리합니다.

@Component
public class JwtAuthFilter implements GatewayFilterFactory<JwtAuthFilter.Config> {

    private final JwtUtil jwtUtil;

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            String path = request.getPath().value();

            // 공개 경로 스킵
            if (config.getExcludePaths().stream().anyMatch(path::startsWith)) {
                return chain.filter(exchange);
            }

            String token = extractToken(request);
            if (token == null) {
                return unauthorized(exchange, "Missing token");
            }

            return jwtUtil.validateToken(token)
                .flatMap(claims -> {
                    // 인증 정보를 헤더로 전달 (백엔드 서비스가 읽음)
                    ServerHttpRequest mutated = request.mutate()
                        .header("X-User-Id", claims.getSubject())
                        .header("X-User-Roles", String.join(",", claims.getRoles()))
                        .build();
                    return chain.filter(exchange.mutate().request(mutated).build());
                })
                .onErrorResume(e -> unauthorized(exchange, e.getMessage()));
        };
    }

    private String extractToken(ServerHttpRequest request) {
        String auth = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
        if (auth != null && auth.startsWith("Bearer ")) {
            return auth.substring(7);
        }
        return null;
    }

    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)));
    }

    @Data
    public static class Config {
        private List<String> excludePaths = List.of("/api/auth/login", "/api/auth/register");
    }

    @Override
    public Class<Config> getConfigClass() { return Config.class; }
}
# YAML에서 사용
filters:
  - name: JwtAuth
    args:
      excludePaths: /api/auth/login,/api/auth/register,/actuator/health

GlobalFilter: 모든 라우트에 적용

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

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

        // 요청 ID 헤더 추가 (분산 추적)
        ServerHttpRequest request = exchange.getRequest().mutate()
            .header("X-Request-Id", requestId)
            .build();

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

@Component
@Order(0)
public class ResponseHeaderFilter implements GlobalFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        return chain.filter(exchange).then(Mono.fromRunnable(() -> {
            exchange.getResponse().getHeaders().add("X-Response-Time",
                String.valueOf(System.currentTimeMillis()));
            // 백엔드 서버 정보 숨기기
            exchange.getResponse().getHeaders().remove("Server");
        }));
    }
}

Rate Limiting: Redis 기반

// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-data-redis-reactive'

// KeyResolver — 제한 기준 정의
@Configuration
public class RateLimiterConfig {

    // IP 기반 제한
    @Bean
    public KeyResolver ipKeyResolver() {
        return exchange -> Mono.just(
            Objects.requireNonNull(exchange.getRequest().getRemoteAddress())
                .getAddress().getHostAddress()
        );
    }

    // 사용자 기반 제한 (JWT 인증 후)
    @Bean
    public KeyResolver userKeyResolver() {
        return exchange -> Mono.just(
            exchange.getRequest().getHeaders()
                .getFirst("X-User-Id") != null
                ? exchange.getRequest().getHeaders().getFirst("X-User-Id")
                : "anonymous"
        );
    }

    @Bean
    public RedisRateLimiter redisRateLimiter() {
        // replenishRate: 초당 허용 요청 수
        // burstCapacity: 최대 버스트 허용량
        // requestedTokens: 요청당 소비 토큰
        return new RedisRateLimiter(10, 20, 1);
    }
}
# YAML 설정
spring:
  cloud:
    gateway:
      routes:
        - id: rate-limited-api
          uri: lb://API-SERVICE
          predicates:
            - Path=/api/**
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10
                redis-rate-limiter.burstCapacity: 20
                key-resolver: "#{@ipKeyResolver}"

서킷 브레이커 통합

백엔드 서비스가 응답하지 않을 때 빠르게 실패하고, fallback 응답을 반환합니다.

// build.gradle
implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-reactor-resilience4j'

// Resilience4j 설정
resilience4j:
  circuitbreaker:
    instances:
      orderCB:
        slidingWindowSize: 10
        failureRateThreshold: 50          # 50% 실패 시 OPEN
        waitDurationInOpenState: 10000     # 10초 후 HALF_OPEN
        permittedNumberOfCallsInHalfOpenState: 3
  timelimiter:
    instances:
      orderCB:
        timeoutDuration: 3s               # 3초 타임아웃
// Fallback 컨트롤러
@RestController
@RequestMapping("/fallback")
public class FallbackController {

    @GetMapping("/orders")
    public Mono<Map<String, String>> orderFallback() {
        return Mono.just(Map.of(
            "status", "SERVICE_UNAVAILABLE",
            "message", "주문 서비스가 일시적으로 불가합니다. 잠시 후 다시 시도해주세요."
        ));
    }
}

서비스 디스커버리 연동

# Eureka 연동
spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true                    # 서비스 이름으로 자동 라우팅
          lower-case-service-id: true      # /USER-SERVICE/** → /user-service/**
          
# 수동 라우트 + 디스커버리
      routes:
        - id: user-service
          uri: lb://USER-SERVICE           # lb:// → 로드밸런싱 + 디스커버리
          predicates:
            - Path=/api/users/**

lb:// 접두사는 Spring Cloud LoadBalancer를 통해 서비스 인스턴스를 자동 선택합니다. Eureka, Consul, Kubernetes 서비스 디스커버리 모두 지원합니다.

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-Request-Id
            allowCredentials: true
            maxAge: 3600

CORS는 게이트웨이에서 한 번만 설정하면 됩니다. 백엔드 서비스에서 중복 설정하면 Access-Control-Allow-Origin 헤더가 두 번 나가서 브라우저가 차단합니다. Spring Security Filter Chain과 함께 사용할 때는 필터 순서에 주의하세요.

Gateway vs Nginx vs Kong

기준 Spring Cloud Gateway Nginx Kong
언어 Java (WebFlux) C Lua + Nginx
커스터마이징 Java 코드로 자유롭게 Lua 스크립트 플러그인 기반
Spring 생태계 완벽 통합 별도 연동 필요 별도 연동 필요
적합한 경우 Spring 마이크로서비스 정적 리버스 프록시 언어 무관 API 관리

Spring 마이크로서비스 아키텍처라면 Spring Cloud Gateway가 자연스러운 선택입니다. Spring WebFlux를 기반으로 하므로, 리액티브 프로그래밍 경험이 있다면 빠르게 적용할 수 있습니다.

정리

Spring Cloud Gateway의 핵심은 “마이크로서비스의 단일 진입점”입니다. Route-Predicate-Filter 3가지 개념으로 라우팅과 필터링을 선언적으로 구성하고, 커스텀 필터로 JWT 인증, GlobalFilter로 요청 추적, Redis Rate Limiter로 속도 제한, Resilience4j로 서킷 브레이커까지 — 게이트웨이 하나로 횡단 관심사를 모두 처리할 수 있습니다.

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