# Spring Cloud Gateway 면접 대비: 라우팅, 필터, 로드 밸런싱 > 기술 면접을 위한 Spring Cloud Gateway 정복: 라우팅, 필터, 로드 밸런싱, API 게이트웨이 패턴을 다루는 12개의 질문과 코드 예제. - Published: 2026-03-20 - Updated: 2026-05-01 - Author: SharpSkill - Tags: spring cloud gateway, microservices, api gateway, routing, technical interview - Reading time: 15 min --- Spring Cloud Gateway는 Spring 마이크로서비스 아키텍처에서 API 게이트웨이를 구현하기 위한 표준 솔루션입니다. 기술 면접에서는 라우팅 구성, 커스텀 필터 작성, 효과적인 로드 밸런싱 관리 능력을 평가합니다. > **면접 준비 팁** > > 면접관은 게이트웨이 패턴, 즉 중앙 집중식 인증, 레이트 리미팅, 서킷 브레이커에 대한 이해를 확인합니다. 다른 대안이 아닌 Spring Cloud Gateway를 선택해야 하는 이유를 설명할 수 있다면 차별화됩니다. ## Spring Cloud Gateway의 아키텍처와 기본 원리 ### 질문 1: Spring Cloud Gateway란 무엇이며 왜 사용합니까? Spring Cloud Gateway는 Spring WebFlux와 Project Reactor 위에 구축된 리액티브 API 게이트웨이입니다. 마이크로서비스로 향하는 모든 요청의 단일 진입점 역할을 하며, 라우팅, 필터링, 로드 밸런싱 기능을 제공합니다. ```java // GatewayApplication.java // Spring Cloud Gateway 기본 구성 @SpringBootApplication public class GatewayApplication { public static void main(String[] args) { // 리액티브 Netty 서버를 시작합니다(Tomcat이 아닙니다) SpringApplication.run(GatewayApplication.class, args); } } ``` ```yaml # application.yml # 게이트웨이 최소 구성 spring: cloud: gateway: routes: # 사용자 서비스로의 라우트 - id: user-service uri: http://localhost:8081 predicates: - Path=/api/users/** # 주문 서비스로의 라우트 - id: order-service uri: http://localhost:8082 predicates: - Path=/api/orders/** ``` Spring Cloud Gateway의 주요 장점은 다음과 같습니다. 고성능을 위한 논블로킹 아키텍처, Spring Cloud 생태계와의 네이티브 통합, 최신 리액티브 패턴 지원입니다. ### 질문 2: Route, Predicate, Filter 개념을 설명해 주십시오 Spring Cloud Gateway는 세 가지 핵심 개념으로 구성됩니다. Route는 목적지를 정의하고, Predicate는 라우트를 적용할 시점을 결정하며, Filter는 요청과 응답을 수정합니다. ```java // RouteConfiguration.java // 프로그래밍 방식의 라우트 구성 @Configuration public class RouteConfiguration { @Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { return builder.routes() // 여러 predicate를 가진 라우트 .route("product-service", r -> r // Predicate: URL 경로 .path("/api/products/**") // Predicate: HTTP 메서드 .and() .method(HttpMethod.GET, HttpMethod.POST) // Predicate: 헤더 존재 .and() .header("X-Api-Version", "v2") // Filter: 경로 재작성 .filters(f -> f .rewritePath("/api/products/(?.*)", "/products/${segment}") // Filter: 헤더 추가 .addRequestHeader("X-Gateway-Source", "spring-cloud-gateway") ) // 대상 URI .uri("http://localhost:8083")) .build(); } } ``` 처리 흐름은 다음 순서를 따릅니다. ```text 수신 요청 │ ▼ ┌─────────────────┐ │ Predicates │ → 조건 평가(path, method, header...) └────────┬────────┘ │ 일치 발견 ▼ ┌─────────────────┐ │ Pre-Filters │ → 라우팅 전에 요청 수정 └────────┬────────┘ │ ▼ ┌─────────────────┐ │ HTTP Proxy │ → 대상 서비스로 전달 └────────┬────────┘ │ ▼ ┌─────────────────┐ │ Post-Filters │ → 클라이언트에 반환하기 전에 응답 수정 └────────┬────────┘ │ ▼ 클라이언트로 응답 ``` ### 질문 3: 가장 자주 사용하는 predicate는 무엇입니까? Spring Cloud Gateway는 다양한 라우팅 조건을 위한 다수의 내장 predicate를 제공합니다. 여러 predicate를 결합하면 정교한 라우팅 규칙을 만들 수 있습니다. ```yaml # application.yml # 자주 쓰이는 predicate 예시 spring: cloud: gateway: routes: # 변수 캡처가 있는 path 라우팅 - id: user-details uri: http://user-service predicates: - Path=/users/{userId} # HTTP 메서드 기반 라우팅 - id: user-create uri: http://user-service predicates: - Path=/users - Method=POST # 헤더 기반 라우팅 - id: mobile-api uri: http://mobile-service predicates: - Header=X-Client-Type, mobile # 쿼리 파라미터 기반 라우팅 - id: search-api uri: http://search-service predicates: - Query=q # 호스트 기반 라우팅 - id: admin-portal uri: http://admin-service predicates: - Host=admin.example.com # 시간 기반 라우팅 - id: maintenance-mode uri: http://maintenance-service predicates: - Between=2026-03-20T02:00:00Z,2026-03-20T04:00:00Z ``` ```java // CustomPredicateFactory.java // 커스텀 predicate 생성 @Component public class ApiKeyRoutePredicateFactory extends AbstractRoutePredicateFactory { public ApiKeyRoutePredicateFactory() { super(Config.class); } @Override public Predicate apply(Config config) { return exchange -> { // API 키의 존재와 유효성을 확인합니다 String apiKey = exchange.getRequest() .getHeaders() .getFirst("X-Api-Key"); return apiKey != null && config.getValidKeys().contains(apiKey); }; } @Validated public static class Config { private List validKeys = new ArrayList<>(); public List getValidKeys() { return validKeys; } public void setValidKeys(List validKeys) { this.validKeys = validKeys; } } } ``` > **predicate의 순서** > > predicate의 순서는 평가에 영향을 주지 않지만, 라우트의 순서는 중요합니다. 라우트는 순차적으로 평가되며 첫 번째로 일치하는 라우트가 사용됩니다. ## 필터와 요청 변환 ### 질문 4: pre/post 처리 필터는 어떻게 동작합니까? GatewayFilter 필터는 정렬된 체인에서 실행됩니다. "pre" 필터는 라우팅 전에 요청을 수정하고, "post" 필터는 대상 서비스로부터 받은 응답을 수정합니다. ```java // LoggingFilter.java // 글로벌 로깅 필터 @Component @Slf4j public class LoggingFilter implements GlobalFilter, Ordered { @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { // PRE-FILTER: 라우팅 전 String requestId = UUID.randomUUID().toString(); long startTime = System.currentTimeMillis(); log.info("Request {} started: {} {}", requestId, exchange.getRequest().getMethod(), exchange.getRequest().getPath()); // 요청 ID를 헤더에 추가합니다 ServerHttpRequest modifiedRequest = exchange.getRequest() .mutate() .header("X-Request-Id", requestId) .build(); // 체인을 계속하고 응답을 처리합니다 return chain.filter(exchange.mutate().request(modifiedRequest).build()) .then(Mono.fromRunnable(() -> { // POST-FILTER: 응답 후 long duration = System.currentTimeMillis() - startTime; HttpStatusCode status = exchange.getResponse().getStatusCode(); log.info("Request {} completed: status={}, duration={}ms", requestId, status, duration); })); } @Override public int getOrder() { // 음수 순서 = 가장 먼저 실행 return -1; } } ``` ```java // AuthenticationFilter.java // JWT 인증 필터 @Component @RequiredArgsConstructor public class AuthenticationFilter implements GatewayFilter { private final JwtTokenValidator tokenValidator; @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { String authHeader = exchange.getRequest() .getHeaders() .getFirst(HttpHeaders.AUTHORIZATION); // 토큰 존재 여부를 확인합니다 if (authHeader == null || !authHeader.startsWith("Bearer ")) { return handleUnauthorized(exchange, "Missing or invalid Authorization header"); } String token = authHeader.substring(7); // 토큰을 리액티브하게 검증합니다 return tokenValidator.validate(token) .flatMap(claims -> { // 사용자 정보로 요청을 보강합니다 ServerHttpRequest enrichedRequest = exchange.getRequest() .mutate() .header("X-User-Id", claims.getSubject()) .header("X-User-Roles", String.join(",", claims.getRoles())) .build(); return chain.filter(exchange.mutate().request(enrichedRequest).build()); }) .onErrorResume(e -> handleUnauthorized(exchange, e.getMessage())); } private Mono handleUnauthorized(ServerWebExchange exchange, String message) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON); String body = String.format("{\"error\": \"%s\"}", message); DataBuffer buffer = exchange.getResponse() .bufferFactory() .wrap(body.getBytes(StandardCharsets.UTF_8)); return exchange.getResponse().writeWith(Mono.just(buffer)); } } ``` ### 질문 5: 가장 유용한 내장 필터는 무엇입니까? Spring Cloud Gateway는 URL 재작성, 헤더 수정, 재시도, 서킷 브레이커 등 흔한 사용 사례를 다루는 다수의 내장 필터를 제공합니다. ```yaml # application.yml # 자주 쓰이는 내장 필터 spring: cloud: gateway: routes: - id: order-service uri: lb://order-service predicates: - Path=/api/orders/** filters: # 경로 재작성 - RewritePath=/api/orders/(?.*), /orders/${segment} # 요청 헤더 추가 - AddRequestHeader=X-Gateway-Version, 1.0 # 민감한 응답 헤더 제거 - RemoveResponseHeader=X-Powered-By - RemoveResponseHeader=Server # 경로 접두사 - PrefixPath=/v2 # 접두사 제거 - StripPrefix=1 # 오류 시 자동 재시도 - name: Retry args: retries: 3 statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE methods: GET backoff: firstBackoff: 100ms maxBackoff: 500ms factor: 2 # 처리율 제한 - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 10 redis-rate-limiter.burstCapacity: 20 key-resolver: "#{@userKeyResolver}" ``` ```java // RateLimiterConfiguration.java // 사용자별 레이트 리미터 구성 @Configuration public class RateLimiterConfiguration { @Bean public KeyResolver userKeyResolver() { // 인증된 사용자 단위로 제한 return exchange -> Mono.just( exchange.getRequest() .getHeaders() .getFirst("X-User-Id") ).defaultIfEmpty("anonymous"); } @Bean public KeyResolver ipKeyResolver() { // IP 주소 단위로 제한 return exchange -> Mono.just( Objects.requireNonNull(exchange.getRequest() .getRemoteAddress()) .getAddress() .getHostAddress() ); } } ``` ### 질문 6: 본문(body)을 변경하는 필터는 어떻게 구현합니까? 요청이나 응답의 본문을 수정하려면 ModifyRequestBodyGatewayFilterFactory 또는 ModifyResponseBodyGatewayFilterFactory를 사용하는 특별한 접근이 필요합니다. ```java // RequestBodyModificationFilter.java // 요청 본문 수정 필터 @Component @RequiredArgsConstructor public class RequestBodyModificationFilter implements GlobalFilter, Ordered { private final ObjectMapper objectMapper; @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { // JSON을 포함한 POST/PUT 요청만 수정합니다 if (!isJsonRequest(exchange)) { return chain.filter(exchange); } return DataBufferUtils.join(exchange.getRequest().getBody()) .flatMap(dataBuffer -> { byte[] bytes = new byte[dataBuffer.readableByteCount()]; dataBuffer.read(bytes); DataBufferUtils.release(dataBuffer); try { // JSON을 파싱하고 수정합니다 Map body = objectMapper.readValue( bytes, new TypeReference>() {} ); // 메타데이터를 추가합니다 body.put("processedAt", Instant.now().toString()); body.put("gatewayVersion", "1.0"); byte[] modifiedBytes = objectMapper.writeValueAsBytes(body); // 수정된 본문으로 새 요청을 만듭니다 ServerHttpRequest modifiedRequest = new ServerHttpRequestDecorator( exchange.getRequest() ) { @Override public Flux getBody() { return Flux.just( exchange.getResponse() .bufferFactory() .wrap(modifiedBytes) ); } @Override public HttpHeaders getHeaders() { HttpHeaders headers = new HttpHeaders(); headers.putAll(super.getHeaders()); headers.setContentLength(modifiedBytes.length); return headers; } }; return chain.filter(exchange.mutate().request(modifiedRequest).build()); } catch (IOException e) { return Mono.error(new RuntimeException("Failed to parse request body", e)); } }); } private boolean isJsonRequest(ServerWebExchange exchange) { MediaType contentType = exchange.getRequest().getHeaders().getContentType(); return contentType != null && contentType.isCompatibleWith(MediaType.APPLICATION_JSON); } @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; } } ``` ```java // ResponseBodyModificationConfig.java // 응답 수정 구성 @Configuration public class ResponseBodyModificationConfig { @Bean public RouteLocator responseModifyingRoutes( RouteLocatorBuilder builder, ModifyResponseBodyGatewayFilterFactory modifyResponseBodyFilter ) { return builder.routes() .route("modify-response", r -> r .path("/api/users/**") .filters(f -> f.modifyResponseBody( String.class, String.class, MediaType.APPLICATION_JSON_VALUE, (exchange, responseBody) -> { // 응답을 표준 형식으로 감쌉니다 return Mono.just(String.format( "{\"success\": true, \"data\": %s, \"timestamp\": \"%s\"}", responseBody, Instant.now() )); } )) .uri("lb://user-service")) .build(); } } ``` ## 로드 밸런싱과 회복력 ### 질문 7: Spring Cloud LoadBalancer로 로드 밸런싱을 어떻게 구성합니까? Spring Cloud Gateway는 Spring Cloud LoadBalancer와 통합되어 서비스 인스턴스 간 트래픽을 분산합니다. URI 스킴 `lb://`를 사용하면 자동 로드 밸런싱이 활성화됩니다. ```yaml # application.yml # 로드 밸런싱 구성 spring: cloud: gateway: routes: - id: user-service # lb:// 가 로드 밸런싱을 활성화 uri: lb://user-service predicates: - Path=/api/users/** # 로드 밸런서 구성 loadbalancer: ribbon: enabled: false # Spring Cloud LoadBalancer 사용(Ribbon 아님) # 서비스별 구성 configurations: default # 로드 밸런싱용 헬스 체크 health-check: path: user-service: /actuator/health interval: 10s # 서비스 디스커버리(Eureka 등) eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/ ``` ```java // LoadBalancerConfiguration.java // 로드 밸런서 커스텀 구성 @Configuration @LoadBalancerClients(defaultConfiguration = CustomLoadBalancerConfig.class) public class LoadBalancerConfiguration { } // CustomLoadBalancerConfig.java // 커스텀 로드 밸런싱 전략 public class CustomLoadBalancerConfig { @Bean public ReactorLoadBalancer loadBalancer( Environment environment, LoadBalancerClientFactory clientFactory ) { String serviceId = environment.getProperty( LoadBalancerClientFactory.PROPERTY_NAME ); // 기본은 라운드 로빈 return new RoundRobinLoadBalancer( clientFactory.getLazyProvider(serviceId, ServiceInstanceListSupplier.class), serviceId ); } @Bean public ServiceInstanceListSupplier serviceInstanceListSupplier( ConfigurableApplicationContext context ) { // 인스턴스에 헬스 체크를 추가 return ServiceInstanceListSupplier.builder() .withDiscoveryClient() .withHealthChecks() .withCaching() .build(context); } } ``` ```java // WeightedLoadBalancer.java // 가중치 기반 로드 밸런서 @Component @RequiredArgsConstructor public class WeightedLoadBalancer implements ReactorServiceInstanceLoadBalancer { private final String serviceId; private final ObjectProvider supplierProvider; private final Random random = new Random(); @Override public Mono> choose(Request request) { return supplierProvider.getIfAvailable() .get() .next() .map(instances -> { if (instances.isEmpty()) { return new EmptyResponse(); } // 메타데이터로부터 가중치를 계산합니다 List weighted = instances.stream() .map(instance -> { int weight = Integer.parseInt( instance.getMetadata().getOrDefault("weight", "1") ); return new WeightedInstance(instance, weight); }) .toList(); // 가중치 기반 선택 int totalWeight = weighted.stream() .mapToInt(WeightedInstance::weight) .sum(); int randomWeight = random.nextInt(totalWeight); int currentWeight = 0; for (WeightedInstance wi : weighted) { currentWeight += wi.weight(); if (randomWeight < currentWeight) { return new DefaultResponse(wi.instance()); } } return new DefaultResponse(weighted.get(0).instance()); }); } private record WeightedInstance(ServiceInstance instance, int weight) {} } ``` ### 질문 8: 게이트웨이에서 서킷 브레이커는 어떻게 구현합니까? 서킷 브레이커는 연쇄 장애를 방지합니다. Spring Cloud Gateway는 Resilience4j와 통합되어 고급 장애 관리를 제공합니다. ```yaml # application.yml # Resilience4j 서킷 브레이커 구성 spring: cloud: gateway: routes: - id: order-service uri: lb://order-service predicates: - Path=/api/orders/** filters: # fallback이 있는 서킷 브레이커 - name: CircuitBreaker args: name: orderServiceCB fallbackUri: forward:/fallback/orders resilience4j: circuitbreaker: configs: default: # 평가되는 요청 수 slidingWindowSize: 10 # 회로를 여는 실패율 임계값 failureRateThreshold: 50 # 회로가 열린 채 유지되는 시간 waitDurationInOpenState: 30s # half-open에서 허용되는 요청 수 permittedNumberOfCallsInHalfOpenState: 3 # 자동 상태 전이 automaticTransitionFromOpenToHalfOpenEnabled: true instances: orderServiceCB: baseConfig: default failureRateThreshold: 60 timelimiter: configs: default: timeoutDuration: 5s instances: orderServiceCB: timeoutDuration: 3s ``` ```java // FallbackController.java // fallback 컨트롤러 @RestController @RequestMapping("/fallback") @Slf4j public class FallbackController { @GetMapping("/orders") public Mono>> ordersFallback( ServerWebExchange exchange ) { log.warn("Circuit breaker activated for orders service"); Map response = Map.of( "success", false, "error", "Service temporarily unavailable", "code", "SERVICE_UNAVAILABLE", "retryAfter", 30 ); return Mono.just(ResponseEntity .status(HttpStatus.SERVICE_UNAVAILABLE) .body(response)); } @PostMapping("/orders") public Mono>> ordersPostFallback() { Map response = Map.of( "success", false, "error", "Order creation temporarily unavailable", "code", "SERVICE_UNAVAILABLE", "message", "Please try again later" ); return Mono.just(ResponseEntity .status(HttpStatus.SERVICE_UNAVAILABLE) .body(response)); } } ``` ```java // CircuitBreakerEventListener.java // 서킷 브레이커 이벤트 모니터링 @Component @Slf4j @RequiredArgsConstructor public class CircuitBreakerEventListener { private final MeterRegistry meterRegistry; @EventListener public void onCircuitBreakerStateTransition( CircuitBreakerOnStateTransitionEvent event ) { CircuitBreaker.StateTransition transition = event.getStateTransition(); log.info("Circuit breaker {} state changed: {} -> {}", event.getCircuitBreakerName(), transition.getFromState(), transition.getToState()); // Micrometer 메트릭 meterRegistry.counter( "circuit_breaker.state_transition", "name", event.getCircuitBreakerName(), "from", transition.getFromState().name(), "to", transition.getToState().name() ).increment(); } @EventListener public void onCircuitBreakerFailure(CircuitBreakerOnErrorEvent event) { log.error("Circuit breaker {} error: {}", event.getCircuitBreakerName(), event.getThrowable().getMessage()); } } ``` > **타임아웃과 서킷 브레이커** > > 서킷 브레이커와 HTTP 클라이언트 사이의 타임아웃을 일관되게 설정하십시오. 너무 긴 타임아웃은 스레드를 점유하고, 너무 짧은 타임아웃은 오탐을 유발합니다. ### 질문 9: 지수 백오프를 적용한 재시도는 어떻게 구현합니까? 지수 백오프를 활용한 똑똑한 재시도는 어려움을 겪는 서비스에 부담을 주지 않으면서 성공 가능성을 극대화합니다. ```yaml # application.yml # 백오프를 적용한 재시도 구성 spring: cloud: gateway: routes: - id: payment-service uri: lb://payment-service predicates: - Path=/api/payments/** filters: - name: Retry args: # 시도 횟수 retries: 3 # 재시도를 유발하는 HTTP 코드 statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE,GATEWAY_TIMEOUT # 멱등 HTTP 메서드만 methods: GET,PUT # 재시도를 유발하는 예외 exceptions: - java.io.IOException - java.net.ConnectException - org.springframework.cloud.gateway.support.TimeoutException # 지수 백오프 backoff: firstBackoff: 100ms maxBackoff: 2000ms factor: 2 basedOnPreviousValue: true ``` ```java // CustomRetryFilter.java // 비즈니스 로직이 포함된 커스텀 재시도 @Component @Slf4j public class CustomRetryFilter implements GatewayFilter, Ordered { private static final int MAX_RETRIES = 3; private static final Duration INITIAL_BACKOFF = Duration.ofMillis(100); private static final double BACKOFF_MULTIPLIER = 2.0; private static final double JITTER_FACTOR = 0.1; @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { return Mono.defer(() -> attemptRequest(exchange, chain, 0)); } private Mono attemptRequest( ServerWebExchange exchange, GatewayFilterChain chain, int attempt ) { return chain.filter(exchange) .onErrorResume(throwable -> { if (attempt >= MAX_RETRIES || !isRetryable(throwable, exchange)) { return Mono.error(throwable); } Duration backoff = calculateBackoff(attempt); log.warn("Retry attempt {} after {}ms for {} {}", attempt + 1, backoff.toMillis(), exchange.getRequest().getMethod(), exchange.getRequest().getPath()); return Mono.delay(backoff) .then(Mono.defer(() -> attemptRequest(exchange, chain, attempt + 1) )); }); } private boolean isRetryable(Throwable throwable, ServerWebExchange exchange) { // 멱등 메서드만 재시도 HttpMethod method = exchange.getRequest().getMethod(); if (!Set.of(HttpMethod.GET, HttpMethod.PUT, HttpMethod.DELETE) .contains(method)) { return false; } // 예외 타입을 확인합니다 return throwable instanceof ConnectException || throwable instanceof TimeoutException || throwable instanceof ServiceUnavailableException; } private Duration calculateBackoff(int attempt) { // 지터가 적용된 지수 백오프 long baseBackoff = (long) ( INITIAL_BACKOFF.toMillis() * Math.pow(BACKOFF_MULTIPLIER, attempt) ); // 무작위 지터 추가(±10%) double jitter = 1.0 + (Math.random() - 0.5) * 2 * JITTER_FACTOR; long finalBackoff = (long) (baseBackoff * jitter); return Duration.ofMillis(finalBackoff); } @Override public int getOrder() { return Ordered.LOWEST_PRECEDENCE - 1; } } ``` ## 고급 패턴과 모범 사례 ### 질문 10: 요청 집계는 어떻게 구현합니까? 집계는 여러 마이크로서비스 호출을 클라이언트를 위한 단일 응답으로 묶어, 지연과 프런트엔드 복잡도를 줄입니다. ```java // AggregationController.java // 멀티 서비스 집계 @RestController @RequestMapping("/api/aggregate") @RequiredArgsConstructor @Slf4j public class AggregationController { private final WebClient.Builder webClientBuilder; private final CircuitBreakerRegistry circuitBreakerRegistry; @GetMapping("/user-dashboard/{userId}") public Mono getUserDashboard(@PathVariable Long userId) { // 서로 다른 서비스로의 병렬 호출 Mono userMono = fetchUserProfile(userId); Mono> ordersMono = fetchRecentOrders(userId); Mono notificationsMono = fetchNotificationCount(userId); // 결과 집계 return Mono.zip(userMono, ordersMono, notificationsMono) .map(tuple -> DashboardResponse.builder() .user(tuple.getT1()) .recentOrders(tuple.getT2()) .notificationCount(tuple.getT3()) .generatedAt(Instant.now()) .build()) .timeout(Duration.ofSeconds(5)) .onErrorResume(this::handleAggregationError); } private Mono fetchUserProfile(Long userId) { return webClientBuilder.build() .get() .uri("lb://user-service/users/{id}", userId) .retrieve() .bodyToMono(UserProfile.class) .transform(CircuitBreakerOperator.of( circuitBreakerRegistry.circuitBreaker("user-service") )) .onErrorReturn(new UserProfile(userId, "Unknown", null)); } private Mono> fetchRecentOrders(Long userId) { return webClientBuilder.build() .get() .uri("lb://order-service/orders?userId={id}&limit=5", userId) .retrieve() .bodyToFlux(Order.class) .collectList() .transform(CircuitBreakerOperator.of( circuitBreakerRegistry.circuitBreaker("order-service") )) .onErrorReturn(Collections.emptyList()); } private Mono fetchNotificationCount(Long userId) { return webClientBuilder.build() .get() .uri("lb://notification-service/notifications/count/{id}", userId) .retrieve() .bodyToMono(NotificationCount.class) .transform(CircuitBreakerOperator.of( circuitBreakerRegistry.circuitBreaker("notification-service") )) .onErrorReturn(new NotificationCount(0, 0)); } private Mono handleAggregationError(Throwable error) { log.error("Dashboard aggregation failed: {}", error.getMessage()); return Mono.just(DashboardResponse.builder() .error("Partial data available") .generatedAt(Instant.now()) .build()); } } ``` ```java // DashboardResponse.java // 집계된 응답 DTO @Data @Builder @NoArgsConstructor @AllArgsConstructor public class DashboardResponse { private UserProfile user; private List recentOrders; private NotificationCount notificationCount; private Instant generatedAt; private String error; } ``` ### 질문 11: OAuth2로 게이트웨이를 어떻게 보호합니까? OAuth2 통합은 인증을 게이트웨이 수준에서 중앙 집중화하여 각 마이크로서비스의 로직 중복을 피합니다. ```yaml # application.yml # OAuth2 리소스 서버 구성 spring: security: oauth2: resourceserver: jwt: issuer-uri: https://auth.example.com/realms/myrealm jwk-set-uri: https://auth.example.com/realms/myrealm/protocol/openid-connect/certs ``` ```java // SecurityConfig.java // 게이트웨이 보안 구성 @Configuration @EnableWebFluxSecurity public class SecurityConfig { @Bean public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { return http // 무상태 API이므로 CSRF 비활성화 .csrf(ServerHttpSecurity.CsrfSpec::disable) // 인가 구성 .authorizeExchange(exchanges -> exchanges // 공개 엔드포인트 .pathMatchers("/actuator/health", "/actuator/info").permitAll() .pathMatchers("/api/public/**").permitAll() .pathMatchers("/api/auth/**").permitAll() // 역할별 엔드포인트 .pathMatchers("/api/admin/**").hasRole("ADMIN") .pathMatchers(HttpMethod.DELETE, "/api/**").hasRole("ADMIN") // 그 외에는 인증 필요 .anyExchange().authenticated() ) // JWT 기반 OAuth2 리소스 서버 .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt .jwtAuthenticationConverter(jwtAuthenticationConverter()) ) ) .build(); } @Bean public ReactiveJwtAuthenticationConverter jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); // "roles" 클레임에서 역할 추출 grantedAuthoritiesConverter.setAuthoritiesClaimName("roles"); grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_"); ReactiveJwtAuthenticationConverter converter = new ReactiveJwtAuthenticationConverter(); converter.setJwtGrantedAuthoritiesConverter( new ReactiveJwtGrantedAuthoritiesConverterAdapter(grantedAuthoritiesConverter) ); return converter; } } ``` ```java // TokenRelayFilter.java // 다운스트림 서비스로 토큰 전달 @Component public class TokenRelayFilter implements GlobalFilter, Ordered { @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { return ReactiveSecurityContextHolder.getContext() .map(SecurityContext::getAuthentication) .filter(auth -> auth instanceof JwtAuthenticationToken) .cast(JwtAuthenticationToken.class) .map(JwtAuthenticationToken::getToken) .map(jwt -> { // 다운스트림 서비스로 토큰 전달 ServerHttpRequest request = exchange.getRequest() .mutate() .header(HttpHeaders.AUTHORIZATION, "Bearer " + jwt.getTokenValue()) // 토큰에서 추출한 사용자 정보를 추가 .header("X-User-Id", jwt.getSubject()) .header("X-User-Email", jwt.getClaimAsString("email")) .build(); return exchange.mutate().request(request).build(); }) .defaultIfEmpty(exchange) .flatMap(chain::filter); } @Override public int getOrder() { // 인증 후, 라우팅 전 return SecurityWebFiltersOrder.AUTHENTICATION.getOrder() + 1; } } ``` ### 질문 12: 모니터링과 옵저버빌리티의 모범 사례는 무엇입니까? 게이트웨이 모니터링은 마이크로서비스 아키텍처의 성능 및 가용성 문제를 빠르게 식별하기 위해 매우 중요합니다. ```yaml # application.yml # 옵저버빌리티 구성 management: endpoints: web: exposure: include: health,info,metrics,prometheus,gateway metrics: tags: application: api-gateway distribution: percentiles-histogram: http.server.requests: true percentiles: http.server.requests: 0.5, 0.95, 0.99 tracing: sampling: probability: 1.0 spring: cloud: gateway: metrics: enabled: true tags: path: enabled: true ``` ```java // MetricsFilter.java // 커스텀 메트릭 필터 @Component @RequiredArgsConstructor @Slf4j public class MetricsFilter implements GlobalFilter, Ordered { private final MeterRegistry meterRegistry; @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { long startTime = System.nanoTime(); String path = exchange.getRequest().getPath().value(); String method = exchange.getRequest().getMethod().name(); // 라우트에서 대상 서비스를 추출 Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR); String routeId = route != null ? route.getId() : "unknown"; return chain.filter(exchange) .doOnSuccess(v -> recordMetrics(exchange, startTime, routeId, "success")) .doOnError(e -> recordMetrics(exchange, startTime, routeId, "error")); } private void recordMetrics( ServerWebExchange exchange, long startTime, String routeId, String outcome ) { long duration = System.nanoTime() - startTime; HttpStatusCode status = exchange.getResponse().getStatusCode(); String statusCode = status != null ? String.valueOf(status.value()) : "0"; // 지연 시간을 위한 타이머 Timer.builder("gateway.request.duration") .tag("route", routeId) .tag("method", exchange.getRequest().getMethod().name()) .tag("status", statusCode) .tag("outcome", outcome) .register(meterRegistry) .record(duration, TimeUnit.NANOSECONDS); // 요청 카운터 meterRegistry.counter( "gateway.requests.total", "route", routeId, "status", statusCode ).increment(); // 느린 요청에 대한 로그 if (duration > TimeUnit.SECONDS.toNanos(1)) { log.warn("Slow request: {} {} - {}ms (route: {})", exchange.getRequest().getMethod(), exchange.getRequest().getPath(), TimeUnit.NANOSECONDS.toMillis(duration), routeId); } } @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; } } ``` ```java // TracingFilter.java // 트레이싱 컨텍스트 전파 @Component @RequiredArgsConstructor public class TracingFilter implements GlobalFilter, Ordered { private final Tracer tracer; @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 트레이싱 스팬을 생성하거나 가져옵니다 Span span = tracer.nextSpan() .name("gateway-request") .tag("http.method", exchange.getRequest().getMethod().name()) .tag("http.url", exchange.getRequest().getURI().toString()) .start(); // 트레이싱 헤더 주입 ServerHttpRequest request = exchange.getRequest() .mutate() .header("X-Trace-Id", span.context().traceId()) .header("X-Span-Id", span.context().spanId()) .build(); return chain.filter(exchange.mutate().request(request).build()) .doOnSuccess(v -> { HttpStatusCode status = exchange.getResponse().getStatusCode(); span.tag("http.status_code", status != null ? String.valueOf(status.value()) : "0"); span.end(); }) .doOnError(e -> { span.tag("error", e.getMessage()); span.end(); }); } @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE + 1; } } ``` 핵심 메트릭 표: ```text | 메트릭 | 설명 | |-----------------------------------|----------------------------------| | gateway.request.duration | 라우트/상태별 지연 시간 | | gateway.requests.total | 요청 카운터 | | resilience4j.circuitbreaker.state | 서킷 브레이커 상태 | | http.server.requests | 표준 HTTP 메트릭 | | spring.cloud.gateway.routes | 활성 라우트 | ``` > **권장 대시보드** > > 메트릭 시각화를 위해 Spring Cloud Gateway와 Resilience4j 대시보드를 갖춘 Grafana를 사용하십시오. P99 지연과 오류율에 대한 알림을 설정합니다. ## 결론 Spring Cloud Gateway는 현대 마이크로서비스 아키텍처의 핵심 구성 요소입니다. 면접에서 기억해야 할 주요 사항은 다음과 같습니다. **아키텍처와 개념:** - ✅ Routes, Predicates, Filters가 기본 모델을 형성 - ✅ WebFlux와 Netty 기반의 리액티브 아키텍처 - ✅ Spring Cloud 생태계와의 네이티브 통합 **핵심 기능:** - ✅ 다양한 기준에 기반한 동적 라우팅 - ✅ 요청과 응답 변환을 위한 pre/post 필터 - ✅ Spring Cloud LoadBalancer를 활용한 로드 밸런싱 **회복력과 보안:** - ✅ Resilience4j와 fallback을 활용한 서킷 브레이커 - ✅ 지수 백오프와 지터를 적용한 재시도 - ✅ 중앙 집중식 OAuth2/JWT 인증 **옵저버빌리티:** - ✅ 라우트별 태그가 있는 Micrometer 메트릭 - ✅ 컨텍스트 전파가 있는 분산 트레이싱 - ✅ 헬스 체크와 actuator 엔드포인트 Spring Cloud Gateway에 대한 숙련도는 마이크로서비스 패턴과 확장성 과제에 대한 깊은 이해를 보여줍니다. 이러한 역량은 견고하고 고성능의 API 게이트웨이를 설계하는 데 필수적입니다. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ko/blog/spring-boot/spring-cloud-gateway-interview-routing-filters