Spring Cloud Gateway - API Gateway 구축


Spring Cloud Gateway는 Spring 생태계에서 제공하는 API Gateway 솔루션입니다. 마이크로서비스 아키텍처에서 라우팅, 인증, 로드밸런싱, 속도 제한 등을 담당합니다.



언제 사용하나요?



  • 마이크로서비스 단일 진입점 구성

  • 인증/인가 중앙 처리

  • 요청 라우팅 및 로드밸런싱

  • API 버전 관리, 속도 제한



의존성 추가


<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>


기본 라우팅 설정


# application.yml
spring:
cloud:
gateway:
routes:
# 사용자 서비스 라우팅
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/api/users/**
filters:
- StripPrefix=1

# 주문 서비스 라우팅
- id: order-service
uri: lb://ORDER-SERVICE
predicates:
- Path=/api/orders/**
filters:
- StripPrefix=1

# 특정 호스트로 라우팅
- id: external-api
uri: https://api.external.com
predicates:
- Path=/external/**
- Method=GET,POST


Java Config 라우팅


@Configuration
public class GatewayConfig {

@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("user-service", r -> r
.path("/api/users/**")
.filters(f -> f
.stripPrefix(1)
.addRequestHeader("X-Gateway", "true")
.retry(3))
.uri("lb://USER-SERVICE"))
.route("order-service", r -> r
.path("/api/orders/**")
.and()
.method(HttpMethod.GET, HttpMethod.POST)
.filters(f -> f
.stripPrefix(1)
.circuitBreaker(c -> c
.setName("orderCB")
.setFallbackUri("forward:/fallback/order")))
.uri("lb://ORDER-SERVICE"))
.build();
}
}


Global Filter (인증)


@Component
@Slf4j
public class AuthenticationFilter implements GlobalFilter, Ordered {

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

// 인증 제외 경로
if (isPublicPath(request.getPath().toString())) {
return chain.filter(exchange);
}

// 토큰 검증
String token = request.getHeaders().getFirst("Authorization");
if (token == null || !token.startsWith("Bearer ")) {
return onError(exchange, "인증 토큰이 없습니다", HttpStatus.UNAUTHORIZED);
}

try {
String jwt = token.substring(7);
Claims claims = validateToken(jwt);

// 사용자 정보를 헤더에 추가하여 하위 서비스로 전달
ServerHttpRequest modifiedRequest = request.mutate()
.header("X-User-Id", claims.getSubject())
.header("X-User-Role", claims.get("role", String.class))
.build();

return chain.filter(exchange.mutate().request(modifiedRequest).build());

} catch (Exception e) {
return onError(exchange, "유효하지 않은 토큰", HttpStatus.UNAUTHORIZED);
}
}

private Mono<Void> onError(ServerWebExchange exchange, String message, HttpStatus status) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(status);
return response.setComplete();
}

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


Rate Limiting


# Redis 기반 속도 제한
spring:
cloud:
gateway:
routes:
- id: rate-limited
uri: lb://API-SERVICE
predicates:
- Path=/api/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
key-resolver: "#{@userKeyResolver}"

# KeyResolver Bean
@Bean
public KeyResolver userKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest().getRemoteAddress().getHostString()
);
}


Circuit Breaker


spring:
cloud:
gateway:
routes:
- id: with-circuit-breaker
uri: lb://BACKEND-SERVICE
predicates:
- Path=/api/**
filters:
- name: CircuitBreaker
args:
name: backendCB
fallbackUri: forward:/fallback

# Fallback Controller
@RestController
public class FallbackController {

@GetMapping("/fallback")
public ResponseEntity<String> fallback() {
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body("서비스를 일시적으로 사용할 수 없습니다");
}
}


로깅 필터


@Component
@Slf4j
public class LoggingFilter implements GlobalFilter {

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

log.info("[{}] Request: {} {}",
requestId,
exchange.getRequest().getMethod(),
exchange.getRequest().getURI());

return chain.filter(exchange).then(Mono.fromRunnable(() -> {
long duration = System.currentTimeMillis() - startTime;
log.info("[{}] Response: {} ({}ms)",
requestId,
exchange.getResponse().getStatusCode(),
duration);
}));
}
}


CORS 설정


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