스프링 클라우드 게이트웨이 내장 필터의 다양한 활용 패턴

1. FallbackHeaders 를 활용한 에러 컨텍스트 전달

Circuit Breaker 기능으로 인해 특정 서비스 요청이 실패하여 폴백 (Fallback) 로직이 수행될 때, 해당 오류에 대한 상세 정보를 다음 처리 단계로 전달할 필요가 있는 경우가 많습니다. FallbackHeaders 필터는 이러한 시나리오에 적합합니다. 이 필터를 적용하면 서킷 브레이커가 활성화되어 외부 URI 로 포워딩되는 요청의 헤더에 예외 발생 정보가 자동으로 추가됩니다.

spring:
  cloud:
    gateway:
      routes:
        - id: Inventory-Gateway
          uri: lb://stock-service
          predicates:
            - Path=/stock/{type}
          filters:
            - StripPrefix=1
            - AddRequestHeader=X-Trace-ID, trace-{type}
            - DedupeResponseHeader=Set-Cookie
            - name: CircuitBreaker
              args:
                name: service-failure-handler
                fallbackUri: forward:/error-handling
                statusCodes:
                  - SERVER_ERROR
                  - NOT_FOUND
        - id: Error-Handler
          uri: http://localhost:9090/
          predicates:
            - Path=/error-handling
          filters:
            - name: FallbackHeaders
              args:
                rootCauseExceptionMessageHeaderName: Internal-Error-Context

여기서 9090 포트를 사용하는 서비스는 실제 비즈니스 로직을 담당하는 마이크로서비스입니다. 서킷이 개방되면 요청은 해당 서비스의 /error-handling 엔드포인트로 라우팅되며, 이때 설정한 헤더명을 통해 원인 예외 메시지를 확인할 수 있습니다. 만약 폴백 컨트롤러를 게이트웨이 모듈 내부에 배치하게 되면 원본 헤더 정보가 유실될 수 있으므로 주의해야 합니다.

폴백 컨트롤러 예시:

@RestController
public class ErrorHandlerController {

    @GetMapping("/error-handling")
    public ResponseEntity<String> handleException(HttpServletRequest request) {
        String errorMsg = request.getHeader("Internal-Error-Context");
        // 로깅 또는 모니터링 시스템 연동
        log.info("Captured error context: {}", errorMsg); 
        return ResponseEntity.status(503).body("Service temporarily unavailable");
    }
}

기본 헤더 이름은 다음과 같이 설정을 통해 커스터마이징이 가능합니다:

  • executionExceptionTypeHeaderName: 예외 타입 정보 헤더
  • executionExceptionMessageHeaderName: 예외 메시지 헤더
  • rootCauseExceptionTypeHeaderName: 근본 원인 타입 헤더
  • rootCauseExceptionMessageHeaderName: 근본 원인 메시지 헤더

2. MapRequestHeader 를 통한 헤더 재매핑

MapRequestHeader 는 클라이언트로부터 들어온 HTTP 요청 헤더의 이름을 변경하거나 값을 복사하여 새로운 헤더로 생성할 때 사용됩니다. fromHeader에서 값을 추출하여 toHeader에 할당하며, 소스 헤더가 없을 경우 필터는 작동하지 않습니다.

filters:
  - StripPrefix=1
  - MapRequestHeader=Client-Origin, Proxy-Src

下游 서비스 측에서는 원래 헤더 대신 새로운 헤더명을 통해 값에 접근할 수 있습니다.

@GetMapping("/check-status")
public String checkStatus(HttpServletRequest request) {
    String origin = request.getHeader("Client-Origin"); // 원본 헤더 (없을 수 있음)
    String proxySrc = request.getHeader("Proxy-Src"); // 매핑된 헤더
    System.out.println("Proxy Source: " + proxySrc);
    return "OK";
}

3. PrefixPath 를 활용한 경로 조정

라우팅된 요청 URL 에 특정 접두사를 강제로 추가하고 싶을 때 PrefixPath 필터를 사용합니다. 이는 주로 레거시 시스템과의 호환성 유지나 API 버전 관리에 유용합니다. StripPrefix와 함께 사용할 경우 경로의 변환이 더 유연해집니다.

예를 들어, 클라이언트는 /api/v1/order로 요청하지만, 백엔드 서비스는 내부적으로 /legacy/process를 기대한다고 가정해 봅시다.

routes:
  - id: Legacy-Adapter
    uri: lb://order-service
    predicates:
      - Path=/api/v1/{any}
    filters:
      - StripPrefix=3       # /api/v1 제거
      - PrefixPath=/legacy  # /legacy 추가

이때 실제 호출되는 서버의 엔드포인트는 /legacy/{any} 형태가 됩니다. 따라서 서비스側の Controller 정의도 이에 맞춰야 합니다.

@RequestMapping("/legacy/orders")
public class OrderController {
    @GetMapping
    public String processOrder() {
        return "Legacy order processing complete.";
    }
}

4. PreserveHostHeader 의 역할

게이트웨이를 통과하더라도 원본 Host 헤더 정보를 유지하고 싶을 때는 PreserveHostHeader 필터를 적용합니다. 기본적으로 게이트웨이는 프록시 후속 요청 시 자기 자신의 호스트 정보를 덮어쓰거나 수정할 수 있는데, 이 필터를 사용하면 그 동작을 억제하여 하위 서비스가 올바른 도메인 정보를 인식하도록 돕습니다. 별도의 파라미터 없이 활성화만 하면 됩니다.

filters:
  - PreserveHostHeader

5. Redis 기반 RequestRateLimiter 구현

과도한 트래픽으로부터 시스템을 보호하기 위한 레이트 리미팅 기능을 제공합니다. 이 필터는 Redis 를 저장소로 사용하여 '토큰 버킷 (Token Bucket)' 알고리즘을 기반으로 합니다. 설정된 임계치를 초과한 요청에는 HTTP 429 코드를 반환합니다.

먼저 pom.xml 에 반응형 레디스 의존성을 추가해야 합니다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>

그 다음으로는 요청 제한을 결정할 키 생성 전략 (KeyResolver) 을 정의하는 빈 클래스가 필요합니다.

@Configuration
public class RateLimitConfig {

    @Bean(name = "customKeyResolver")
    public KeyResolver createKeyResolver() {
        // IP 주소 대신 요청 URI 경로 기준으로 제한 (예: 특정 API 만 제한)
        return exchange -> Mono.just(exchange.getRequest().getURI().getPath());
    }
}

마지막으로 라우트 설정에서 해당 필터를 인스턴스화하여 토큰 재생 속도 및 버스트 용량을 지정합니다.

filters:
  - name: RequestRateLimiter
    args:
      redis-rate-limiter.replenishRate: 50       # 초당 허용 가능한 평균 요청 수
      redis-rate-limiter.burstCapacity: 100      # 순간 최대 허용 요청 수
      redis-rate-limiter.requestedTokens: 1      # 각 요청이 소비하는 토큰 수
      key-resolver: "#{@customKeyResolver}"

replenishRate는 토큰이 채워지는 속도를 의미하며, 이는 정상적인 트래픽 흐름을 보장하는 기준이 됩니다. 반면 burstCapacity는 갑작스러운 트래픽 피크를 얼마나 흡수할 수 있는지 결정합니다. 이 값을 지나치게 낮으면 정상 사용자까지 차단될 수 있으므로 주의가 필요합니다. 실제 동작 확인을 위해 JMeter 나 다른 부하 테스트 도구로 동시 접속 테스트를 진행하여 429 응답이 예상대로 발생하는지 검증할 수 있습니다.

태그: spring-cloud-gateway spring-boot Redis rate-limiting microservices

6월 29일 20:53에 게시됨