클라이언트가 10개의 마이크로서비스를 각각 직접 호출해야 한다면, 서비스 주소가 바뀔 때마다 클라이언트를 수정해야 할까요?

API Gateway는 클라이언트와 마이크로서비스 사이의 단일 진입점입니다. 라우팅, 인증, Rate Limiting, 로깅 같은 공통 관심사를 한곳에서 처리하여 각 서비스가 비즈니스 로직에만 집중할 수 있게 합니다.

Spring Cloud Gateway란

Spring Cloud Gateway는 Spring WebFlux 기반의 API Gateway 프레임워크입니다. 비동기/논블로킹으로 동작하여 높은 성능을 제공합니다.

핵심 개념

  • Route: 요청을 어디로 보낼지 정의 (ID + URI + Predicate + Filter)
  • Predicate: 요청이 이 라우트에 매칭되는지 판단하는 조건
  • Filter: 요청/응답을 가공하는 로직
PLAINTEXT
Client → Gateway → [Predicate 매칭] → [Filter 체인] → Backend Service

기본 설정

의존성

XML
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

YAML로 라우트 설정

YAML
spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: http://localhost:8081
          predicates:
            - Path=/api/orders/**
          filters:
            - StripPrefix=1

        - id: user-service
          uri: http://localhost:8082
          predicates:
            - Path=/api/users/**
          filters:
            - StripPrefix=1

서비스 디스커버리와 연동하면 lb:// 프리픽스로 동적 라우팅이 가능합니다.

YAML
        - id: product-service
          uri: lb://product-service  # 서비스 디스커버리 사용
          predicates:
            - Path=/api/products/**
            - Method=GET,POST
          filters:
            - StripPrefix=1
            - AddRequestHeader=X-Gateway, true

Java 코드로 라우트 설정

JAVA
@Configuration
public class GatewayConfig {

    @Bean
    public RouteLocator customRoutes(RouteLocatorBuilder builder) {
        return builder.routes()
                .route("order-service", r -> r
                        .path("/api/orders/**")
                        .filters(f -> f
                                .stripPrefix(1)
                                .addRequestHeader("X-Gateway", "true"))
                        .uri("lb://order-service"))

여러 Predicate를 and()로 조합하면 경로와 HTTP 메서드를 동시에 매칭할 수 있습니다.

JAVA
                .route("user-service", r -> r
                        .path("/api/users/**")
                        .and()
                        .method(HttpMethod.GET, HttpMethod.POST)
                        .filters(f -> f.stripPrefix(1))
                        .uri("lb://user-service"))
                .build();
    }
}

Predicate — 라우트 매칭 조건

Spring Cloud Gateway는 다양한 내장 Predicate를 제공합니다.

Predicate설명예시
PathURL 경로 패턴Path=/api/orders/**
MethodHTTP 메서드Method=GET,POST
Header요청 헤더Header=X-Request-Id, \\d+
Query쿼리 파라미터Query=category, IT
Host호스트 이름Host=**.example.com
After/Before/Between시간 기반After=2026-03-01T00:00:00+09:00

여러 Predicate를 조합하면 세밀한 라우팅 규칙을 만들 수 있습니다.

Filter — 요청/응답 가공

내장 Filter

YAML
filters:
  - StripPrefix=1              # 경로 앞부분 제거 (/api/orders → /orders)
  - AddRequestHeader=X-Custom, value
  - AddResponseHeader=X-Response, value
  - RewritePath=/api/(?<segment>.*), /$\{segment}
  - SetStatus=200
  - Retry=3                    # 3번 재시도

커스텀 Global Filter

모든 라우트에 적용되는 필터입니다. 로깅, 인증 같은 공통 관심사에 사용합니다.

JAVA
@Component
public class LoggingFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String requestId = UUID.randomUUID().toString();

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

        long startTime = System.currentTimeMillis();

        // 요청 헤더에 Request ID 추가
        ServerHttpRequest mutatedRequest = request.mutate()
                .header("X-Request-Id", requestId)
                .build();

필터 체인 실행 후 then()으로 응답 시간과 상태 코드를 로깅합니다.

JAVA
        return chain.filter(exchange.mutate().request(mutatedRequest).build())
                .then(Mono.fromRunnable(() -> {
                    long duration = System.currentTimeMillis() - startTime;
                    log.info("[{}] 응답 {} — {}ms",
                            requestId,
                            exchange.getResponse().getStatusCode(),
                            duration);
                }));
    }

    @Override
    public int getOrder() {
        return -1;  // 가장 먼저 실행
    }
}

인증 Filter

JAVA
@Component
public class AuthFilter implements GlobalFilter, Ordered {

    private final JwtValidator jwtValidator;

    private static final List<String> PUBLIC_PATHS = List.of(
            "/api/auth/login",
            "/api/auth/register",
            "/health"
    );

공개 경로는 인증을 건너뛰고, 나머지 경로는 토큰을 검증하여 유효하지 않으면 401을 반환합니다.

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

        // 공개 경로는 인증 건너뛰기
        if (PUBLIC_PATHS.stream().anyMatch(path::startsWith)) {
            return chain.filter(exchange);
        }

        String token = extractToken(exchange.getRequest());

        if (token == null || !jwtValidator.isValid(token)) {
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }

토큰 검증에 성공하면 사용자 정보를 내부 헤더로 전달하여 백엔드 서비스에서 활용할 수 있게 합니다.

JAVA
        // 인증된 사용자 정보를 헤더로 전달
        String userId = jwtValidator.getUserId(token);
        ServerHttpRequest mutatedRequest = exchange.getRequest().mutate()
                .header("X-User-Id", userId)
                .build();

        return chain.filter(exchange.mutate().request(mutatedRequest).build());
    }

토큰 추출 헬퍼와 필터 실행 순서를 지정하는 getOrder() 메서드입니다.

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

    @Override
    public int getOrder() {
        return 0;
    }
}

Rate Limiting

Redis 기반의 요청 속도 제한을 설정할 수 있습니다.

XML
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
YAML
spring:
  cloud:
    gateway:
      routes:
        - id: rate-limited-route
          uri: lb://order-service
          predicates:
            - Path=/api/orders/**
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10   # 초당 10개 허용
                redis-rate-limiter.burstCapacity: 20    # 최대 버스트 20개
                key-resolver: "#{@ipKeyResolver}"
JAVA
@Configuration
public class RateLimiterConfig {

    @Bean
    public KeyResolver ipKeyResolver() {
        // IP 주소 기반 Rate Limiting
        return exchange -> Mono.just(
                exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
    }

    @Bean
    public KeyResolver userKeyResolver() {
        // 사용자 ID 기반 Rate Limiting
        return exchange -> Mono.just(
                exchange.getRequest().getHeaders().getFirst("X-User-Id"));
    }
}

Rate Limit을 초과하면 429 Too Many Requests가 반환됩니다.

Circuit Breaker 통합

Gateway에서 서킷 브레이커를 설정하면 백엔드 장애 시 Fallback 응답을 반환합니다.

YAML
spring:
  cloud:
    gateway:
      routes:
        - id: order-with-cb
          uri: lb://order-service
          predicates:
            - Path=/api/orders/**
          filters:
            - name: CircuitBreaker
              args:
                name: orderCB
                fallbackUri: forward:/fallback/orders
JAVA
@RestController
public class FallbackController {

    @GetMapping("/fallback/orders")
    public ResponseEntity<Map<String, String>> orderFallback() {
        return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
                .body(Map.of(
                        "message", "주문 서비스가 일시적으로 이용 불가합니다",
                        "fallback", "true"
                ));
    }
}

서비스 디스커버리 연동

Eureka나 Kubernetes와 연동하면 서비스 주소를 하드코딩하지 않아도 됩니다.

YAML
spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true              # 서비스 디스커버리 자동 라우트
          lower-case-service-id: true

이 설정으로 lb://ORDER-SERVICE처럼 서비스 이름으로 라우팅할 수 있습니다.

CORS 설정

YAML
spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]':
            allowedOrigins: "https://my-frontend.com"
            allowedMethods:
              - GET
              - POST
              - PUT
              - DELETE
            allowedHeaders: "*"
            allowCredentials: true

실무 팁

  • Gateway는 단일 진입점 이므로 고가용성과 성능 이 최우선입니다
  • Gateway에서 비즈니스 로직은 최소화 하고, 인증/라우팅/로깅 같은 횡단 관심사만 처리하세요
  • Health Check 엔드포인트로 Gateway 자체의 상태를 모니터링하세요
  • Rate Limiting의 burstCapacity를 적절히 설정하여 정상 트래픽 급증에 대응하세요

주의할 점

1. Gateway에서 블로킹 코드를 실행하면 전체 처리량이 급락한다

Spring Cloud Gateway는 WebFlux(Netty) 기반이므로 이벤트 루프 스레드에서 JDBC 호출이나 Thread.sleep() 같은 블로킹 연산을 수행하면 전체 요청 처리가 멈춥니다. 인증 필터에서 DB를 조회해야 한다면 R2DBC나 WebClient 같은 논블로킹 API를 사용해야 합니다.

2. Gateway는 단일 진입점이므로 장애 시 전체 서비스가 접근 불가가 된다

모든 트래픽이 Gateway를 경유하므로 Gateway가 다운되면 백엔드 서비스가 정상이어도 클라이언트가 접근할 수 없습니다. 반드시 Gateway를 2대 이상 배포하고, 앞단에 로드밸런서(L4/L7)를 두어 고가용성을 확보하세요.

3. Rate Limiter 설정에서 Redis가 다운되면 모든 요청이 거부될 수 있다

RequestRateLimiter 필터가 Redis를 사용하는데, Redis에 장애가 발생하면 Rate Limit 판단 자체가 불가능하여 모든 요청이 429로 거부될 수 있습니다. Redis 장애 시 Rate Limiting을 우회하는 Fallback 전략을 미리 설계해야 합니다.

정리

  • Spring Cloud Gateway는 WebFlux 기반 비동기/논블로킹 API Gateway입니다
  • Route + Predicate + Filter 구조로 라우팅 규칙을 정의합니다
  • Global Filter 로 인증, 로깅 등 공통 관심사를 처리하고, Rate Limiter 로 트래픽을 제어합니다
  • 서비스 디스커버리와 연동하면 lb://서비스명으로 동적 라우팅이 가능합니다
댓글 로딩 중...