본문 바로가기
Spring

Spring Cloud Gateway 구현 정리

by wadekang 2024. 2. 6.

 

버전 정보
> Spring Boot 3.2.2
> OpenJDK 17.0.8
> spring-cloud-starter-gateway 4.1.0
> spring-cloud-starter-netflix-eureka-client 4.1.0

 

Gradle Dependecies

dependencies {  
    implementation 'org.springframework.cloud:spring-cloud-starter-gateway'  
    implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
}

 

Eureka Client로 등록

@EnableDiscoveryClient  
@SpringBootApplication  
public class GatewayApplication {  
  
    public static void main(String[] args) {  
        SpringApplication.run(GatewayApplication.class, args);  
    }  
  
}

 

application.yml 설정을 통한 라우팅

server:  
  port: 8200  
  
eureka:  
  client:  
    service-url:  
      defaultZone: http://localhost:8761/eureka/  
  
spring:  
  application:  
    name: gateway  
  cloud:  
    gateway:  
      routes:  
        - id: first-service  
          uri: lb://first-service
          predicates:  
            - Path=/first-service/**
          filters:  
            - RewritePath=/first-service/?(?<segment>.*), /$\{segment} 
#      loadbalancer:  
#        use404: true

 

라우팅 정보 입력 필드 설명

  • id: 라우팅 Id
  • uri: 위에서는 lb://first-service를 사용했는데, 이는 유레카 서버에서 해당 서비스를 조회하여 라우팅 하는 것을 설정 (일반적인 http 경로로도 설정 가능)
  • predicates - Path: 해당 경로로 오는 모든 요청을 라우팅
  • filters - RewritePath: 경로를 수정함 (e.g. /first-service/hello -> /hello)
    • 서비스에서는 /first-service 경로를 신경 쓰지 않고, /hello 경로로 구현하면 됨
  • loadbalacers - use404: 유레카에서 경로 정보를 찾지 못했을 경우 기본적으로 503 에러코드를 응답하게 되는데, 404를 응답하고 싶다면 해당 필드를 true로 설정하면 됨

RouteLocator 통한 라우팅 구현

@Bean  
public RouteLocator routes(RouteLocatorBuilder builder) {  
    return builder.routes()  
            .route("first-service", r -> r  
                    .path("/first-service/**")  
                    .filters(f -> f  
                            .rewritePath("/first-service/(?<segment>.*)", "/${segment}")  
                    )  
                    .uri("lb://first-service")  
            )  
            .build();  
}

 

요청 처리 과정

 

  1. http://localhost:8200/first-service/hello 경로를 통해 Gateway로 요청
  2. /first-service/** 요청이기 때문에 lb://first-service 정보를 유레카 서버에서 조회
  3. lb://first-service 정보 응답
  4. FIRST-SERVICE로 요청 전달 (Rewrite: /first-serivce/hello -> /hello)
  5. FIRST-SERVICE에서 /hello 요청 응답
  6. Gateway에서 응답 전달

GlobalFilter 통해 요청, 응답 정보 로깅하는 LoggingGlobalFilter 구현

@Slf4j  
@Component  
public class LoggingGlobalFilter implements GlobalFilter, Ordered {  
  
    @Override  
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {  
  
        log.info("PreFilter ===> requestId: {}", exchange.getRequest().getId());  
        ServerHttpRequest request = getDecoratedRequest(exchange);  
        ServerHttpResponse response = getDecoratedResponse(exchange);  
  
        return chain.filter(exchange.mutate().request(request).response(response).build())  
                .then(Mono.fromRunnable(() -> {  
                    log.info("PostFilter ===> for requestId: {}, responseCode: {}", exchange.getRequest().getId(), response.getStatusCode());  
                }));  
    }  
  
    @Override  
    public int getOrder() {  
        return -1;  
    }  
  
    private ServerHttpRequest getDecoratedRequest(ServerWebExchange exchange) {  
  
        ServerHttpRequest request = exchange.getRequest();  
        log.info("Request ===> for requestId: {}, method: {}, url: {}", request.getId(), request.getMethod(), request.getURI());  
  
        Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);  
  
        if (route != null) {  
            log.info("Route ===> for requestId: {}, routeId: {}, routeUri: {}", request.getId(), route.getId(), route.getUri());  
        }  
  
        return new ServerHttpRequestDecorator(request) {  
            @Override  
            public Flux<DataBuffer> getBody() {  
                return super.getBody().publishOn(Schedulers.boundedElastic()).doOnNext(dataBuffer -> {  
                    try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {  
  
                        Channels.newChannel(byteArrayOutputStream).write(dataBuffer.asByteBuffer().asReadOnlyBuffer());  
                        String requestBody = removeWhiteSpacesFromJson(byteArrayOutputStream.toString(StandardCharsets.UTF_8));  
  
                        log.info("Request Body ===> for requestId: {}, requestBody: {}", request.getId(), requestBody);  
  
                    } catch (Exception e) {  
                        log.error(e.getMessage());  
                    }  
                });  
            }  
        };  
    }  
  
    private ServerHttpResponseDecorator getDecoratedResponse(ServerWebExchange exchange) {  
  
        ServerHttpResponse response = exchange.getResponse();  
        DataBufferFactory dataBufferFactory = response.bufferFactory();  
  
        return new ServerHttpResponseDecorator(response) {  
            @Override  
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {  
  
                if (body instanceof Flux) {  
                    Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;  
  
                    return super.writeWith(fluxBody.buffer().map(dataBuffers -> {  
  
                        DefaultDataBuffer joinedBuffers = new DefaultDataBufferFactory().join(dataBuffers);  
                        byte[] content = new byte[joinedBuffers.readableByteCount()];  
                        joinedBuffers.read(content);  
  
                        log.info("Response Body ===> for requestId: {}, responseBody: {}", exchange.getRequest().getId(), new String(content, StandardCharsets.UTF_8));  
  
                        return dataBufferFactory.wrap(content);  
                    })).onErrorResume(err -> {  
                        log.error(err.getMessage());  
                        return Mono.empty();  
                    });  
                }  
                return super.writeWith(body);  
            }  
        };  
    }  
  
    private String removeWhiteSpacesFromJson(String json) {  
        ObjectMapper om = new ObjectMapper();  
        try {  
            JsonNode jsonNode = om.readTree(json);  
            return om.writeValueAsString(jsonNode);  
        } catch (JsonProcessingException e) {  
            throw new RuntimeException(e);  
        }  
    }  
}

 

Request, Response의 Body를 로깅하기 위해 Decorator로 ServerHttpRequest, ServerHttpResponse를 재정의 하여 exchange에 mutate 해줌

 

@Override  
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {  
  
    log.info("PreFilter ===> requestId: {}", exchange.getRequest().getId());  
  
    ServerHttpRequest request = getDecoratedRequest(exchange);  
    ServerHttpResponse response = getDecoratedResponse(exchange);  
  
    return chain.filter(exchange.mutate().request(request).response(response).build())  
            .then(Mono.fromRunnable(() -> {  
                log.info("PostFilter ===> for requestId: {}, responseCode: {}", exchange.getRequest().getId(), response.getStatusCode());  
            }));  
}

 

Body 로깅이 필요 없는 경우 다음과 같이 간단하게 구현할 수 있음

 

@Override  
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {  
  
    ServerHttpRequest request = exchange.getRequest();  
    ServerHttpResponse response = exchange.getResponse();  
  
    log.info("PreFilter ===> requestId: {}, method: {}, url: {}", request.getId(), request.getMethod(), request.getURI());  
      
    return chain.filter(exchange).then(  
            Mono.fromRunnable(() -> {  
                log.info("PostFilter ===> for requestId: {}, responseCode: {}", request.getId(), response.getStatusCode());  
            })  
    );  
}

 

로깅 정보

2024-02-06T15:35:26.294+09:00  INFO 75898 --- [gateway] [ctor-http-nio-4] c.e.m.filter.LoggingGlobalFilter         : PreFilter ===> requestId: f0abf148-3
2024-02-06T15:35:26.294+09:00  INFO 75898 --- [gateway] [ctor-http-nio-4] c.e.m.filter.LoggingGlobalFilter         : Request ===> for requestId: f0abf148-3, method: GET, url: http://localhost:8200/first-service/hello
2024-02-06T15:35:26.294+09:00  INFO 75898 --- [gateway] [ctor-http-nio-4] c.e.m.filter.LoggingGlobalFilter         : Route ===> for requestId: f0abf148-3, routeId: first-service, routeUri: lb://first-service
2024-02-06T15:35:26.304+09:00  INFO 75898 --- [gateway] [ctor-http-nio-4] c.e.m.filter.LoggingGlobalFilter         : Response Body ===> for requestId: f0abf148-3, responseBody: {"message":"Hello from first service"}
2024-02-06T15:35:26.304+09:00  INFO 75898 --- [gateway] [ctor-http-nio-4] c.e.m.filter.LoggingGlobalFilter         : PostFilter ===> for requestId: f0abf148-3, responseCode: 200 OK

댓글