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로 서킷 브레이커까지 — 게이트웨이 하나로 횡단 관심사를 모두 처리할 수 있습니다.