Architecture/MSA

Spring Cloud Gateway - Custom Filter

체리필터 2021. 1. 8. 10:38
반응형

Custom Filter의 기본적인 동작 방식에 대해 알아본다.

기본적인 내용은 docs.spring.io/spring-cloud-gateway/docs/2.2.6.BUILD-SNAPSHOT/reference/html/#writing-custom-global-filters 를 참고하면 되지만 매뉴얼의 특성상 자세하게 나오지 않아서 이해를 위해 www.baeldung.com/spring-cloud-custom-gateway-filters 를 참고하였다.

필터는 기본적으로 Proxied Service로 들어가기 전에 수행하는 Pre filter와 나오면서 수행하는 Post filter가 있다.

또한 org.springframework.core.Ordered 인터페이스를 구현하게 되면 순서에 따라 실행 되는데, 아래 그림에서 볼 수 있는 것 처럼 숫자가 작으면 pre 일경우 먼저 실행 되지만 post 일 경우에는 나중에 실행되게 된다.

https://www.baeldung.com/ 인용

우선 순서가 없는 pre filter를 작성해보자. 다음과 같이 하게 되면 기본적인 Pre Filter 가 된다. 즉 GlobalFilter를 구현하면서 filter method를 구현 하기만 하면 된다.

@Slf4j
@Component
public class LoggingGlobalPreFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("===================== pre filter =====================");

        return chain.filter(exchange);
    }
}

 chain.filter를 실행하기 전에 무엇인가를 실행 하는 것이기에 pre filter 이다.

위와 같이 한 상태에서 특정 서비스로 가는 api를 api gateway에게 호출하게 되면 아래와 같은 로그가 찍히게 된다.

GlobalFilter를 return 하는 Bean을 등록하는 방식으로도 동작하는 것 같다.

PostFilter는 아래와 같은 방식으로 작성해 본다.

@Slf4j
@Configuration
public class LoggingGlobalFiltersConfigurations {
    @Bean
    public GlobalFilter postGlobalFilter() {
        return (exchange, chain) -> {
            return chain.filter(exchange)
                    .then(Mono.fromRunnable(() -> {
                        log.info("===================== Global Post Filter executed =====================");
                    }));
        };
    }
}

작성 후 api를 호출해 보면 아래와 같이 로그가 찍히게 된다.

pre filter 실행 후 post filter가 실행되게 된다.

이제 처음에 GlobalFilter 를 구현한 클래스에 Ordered 까지 구현하면서 Pre, Post Filter를 함께 구현해 보면 아래와 같다.

@Slf4j
@Component
public class LoggingGlobalPreFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("===================== pre filter =====================");

        return chain.filter(exchange)
                .then(Mono.fromRunnable(() -> {
                    log.info("===================== post filter =====================");
                }));
    }

    @Override
    public int getOrder() {
        return -1;
    }
}

Order는 -1로 주었으니 이보다 작은 숫자가 없다면 Pre는 가장 먼저 Post는 가장 나중에 실행 될 것이다.

로그는 아래와 같다.

2021-01-08 12:33:27.640  INFO 29652 --- [ctor-http-nio-3] c.k.m.g.t.LoggingGlobalPreFilter         : ===================== pre filter =====================
2021-01-08 12:33:27.653 DEBUG 29652 --- [ctor-http-nio-3] s.c.a.AnnotationConfigApplicationContext : Refreshing LoadBalancerClientFactory-CHAUFFEUR
2021-01-08 12:33:27.653 DEBUG 29652 --- [ctor-http-nio-3] o.s.b.f.s.DefaultListableBeanFactory     : Creating shared instance of singleton bean 'org.springframework.context.annotation.internalConfigurationAnnotationProcessor'
2021-01-08 12:33:27.668 DEBUG 29652 --- [ctor-http-nio-3] o.s.b.f.s.DefaultListableBeanFactory     : Creating shared instance of singleton bean 'propertySourcesPlaceholderConfigurer'
2021-01-08 12:33:27.668 DEBUG 29652 --- [ctor-http-nio-3] o.s.b.f.s.DefaultListableBeanFactory     : Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerProcessor'
2021-01-08 12:33:27.669 DEBUG 29652 --- [ctor-http-nio-3] o.s.b.f.s.DefaultListableBeanFactory     : Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerFactory'
2021-01-08 12:33:27.669 DEBUG 29652 --- [ctor-http-nio-3] o.s.b.f.s.DefaultListableBeanFactory     : Creating shared instance of singleton bean 'org.springframework.context.annotation.internalAutowiredAnnotationProcessor'
2021-01-08 12:33:27.669 DEBUG 29652 --- [ctor-http-nio-3] o.s.b.f.s.DefaultListableBeanFactory     : Creating shared instance of singleton bean 'org.springframework.context.annotation.internalCommonAnnotationProcessor'
2021-01-08 12:33:27.669 DEBUG 29652 --- [ctor-http-nio-3] o.s.b.f.s.DefaultListableBeanFactory     : Creating shared instance of singleton bean 'eurekaLoadBalancerClientConfiguration'
2021-01-08 12:33:27.669 DEBUG 29652 --- [ctor-http-nio-3] ocalVariableTableParameterNameDiscoverer : Cannot find '.class' file for class [class org.springframework.cloud.netflix.eureka.loadbalancer.EurekaLoadBalancerClientConfiguration$$EnhancerBySpringCGLIB$$bafc2040] - unable to determine constructor/method parameter names
2021-01-08 12:33:27.670 DEBUG 29652 --- [ctor-http-nio-3] o.s.b.f.s.DefaultListableBeanFactory     : Autowiring by type from bean name 'eurekaLoadBalancerClientConfiguration' via constructor to bean named 'eurekaClientConfigBean'
2021-01-08 12:33:27.670 DEBUG 29652 --- [ctor-http-nio-3] o.s.b.f.s.DefaultListableBeanFactory     : Autowiring by type from bean name 'eurekaLoadBalancerClientConfiguration' via constructor to bean named 'eurekaInstanceConfigBean'
2021-01-08 12:33:27.670 DEBUG 29652 --- [ctor-http-nio-3] o.s.b.f.s.DefaultListableBeanFactory     : Autowiring by type from bean name 'eurekaLoadBalancerClientConfiguration' via constructor to bean named 'zoneConfig'
2021-01-08 12:33:27.670 DEBUG 29652 --- [ctor-http-nio-3] o.s.b.f.s.DefaultListableBeanFactory     : Autowiring by type from bean name 'eurekaLoadBalancerClientConfiguration' via constructor to bean named 'eurekaLoadBalancerProperties'
2021-01-08 12:33:27.670 DEBUG 29652 --- [ctor-http-nio-3] .l.EurekaLoadBalancerClientConfiguration : Setting the value of 'spring.cloud.loadbalancer.zone' to defaultZone
2021-01-08 12:33:27.670 DEBUG 29652 --- [ctor-http-nio-3] o.s.b.f.s.DefaultListableBeanFactory     : Creating shared instance of singleton bean 'propertyPlaceholderAutoConfiguration'
2021-01-08 12:33:27.670 DEBUG 29652 --- [ctor-http-nio-3] o.s.b.f.s.DefaultListableBeanFactory     : Creating shared instance of singleton bean 'loadBalancerClientConfiguration'
2021-01-08 12:33:27.670 DEBUG 29652 --- [ctor-http-nio-3] o.s.b.f.s.DefaultListableBeanFactory     : Creating shared instance of singleton bean 'org.springframework.cloud.loadbalancer.annotation.LoadBalancerClientConfiguration$ReactiveSupportConfiguration'
2021-01-08 12:33:27.671 DEBUG 29652 --- [ctor-http-nio-3] o.s.b.f.s.DefaultListableBeanFactory     : Creating shared instance of singleton bean 'discoveryClientServiceInstanceListSupplier'
2021-01-08 12:33:27.671 DEBUG 29652 --- [ctor-http-nio-3] o.s.b.f.s.DefaultListableBeanFactory     : Autowiring by type from bean name 'discoveryClientServiceInstanceListSupplier' via factory method to bean named 'org.springframework.context.annotation.AnnotationConfigApplicationContext@734b2858'
2021-01-08 12:33:27.673 DEBUG 29652 --- [ctor-http-nio-3] o.s.c.e.PropertySourcesPropertyResolver  : Found key 'loadbalancer.client.name' in PropertySource 'loadbalancer' with value of type String
2021-01-08 12:33:27.678 DEBUG 29652 --- [ctor-http-nio-3] o.s.b.f.s.DefaultListableBeanFactory     : Creating shared instance of singleton bean 'discoveryClientServiceInstanceSupplier'
2021-01-08 12:33:27.678 DEBUG 29652 --- [ctor-http-nio-3] o.s.b.f.s.DefaultListableBeanFactory     : Autowiring by type from bean name 'discoveryClientServiceInstanceSupplier' via factory method to bean named 'reactiveCompositeDiscoveryClient'
2021-01-08 12:33:27.678 DEBUG 29652 --- [ctor-http-nio-3] o.s.b.f.s.DefaultListableBeanFactory     : Autowiring by type from bean name 'discoveryClientServiceInstanceSupplier' via factory method to bean named 'environment'
2021-01-08 12:33:27.678 DEBUG 29652 --- [ctor-http-nio-3] o.s.b.f.s.DefaultListableBeanFactory     : Autowiring by type from bean name 'discoveryClientServiceInstanceSupplier' via factory method to bean named 'org.springframework.context.annotation.AnnotationConfigApplicationContext@734b2858'
2021-01-08 12:33:27.679 DEBUG 29652 --- [ctor-http-nio-3] o.s.c.e.PropertySourcesPropertyResolver  : Found key 'loadbalancer.client.name' in PropertySource 'loadbalancer' with value of type String
2021-01-08 12:33:27.682 DEBUG 29652 --- [ctor-http-nio-3] o.s.b.f.s.DefaultListableBeanFactory     : Creating shared instance of singleton bean 'org.springframework.cloud.loadbalancer.annotation.LoadBalancerClientConfiguration$BlockingSupportConfiguration'
2021-01-08 12:33:27.682 DEBUG 29652 --- [ctor-http-nio-3] o.s.b.f.s.DefaultListableBeanFactory     : Creating shared instance of singleton bean 'reactorServiceInstanceLoadBalancer'
2021-01-08 12:33:27.682 DEBUG 29652 --- [ctor-http-nio-3] o.s.b.f.s.DefaultListableBeanFactory     : Autowiring by type from bean name 'reactorServiceInstanceLoadBalancer' via factory method to bean named 'environment'
2021-01-08 12:33:27.683 DEBUG 29652 --- [ctor-http-nio-3] o.s.b.f.s.DefaultListableBeanFactory     : Autowiring by type from bean name 'reactorServiceInstanceLoadBalancer' via factory method to bean named 'loadBalancerClientFactory'
2021-01-08 12:33:27.683 DEBUG 29652 --- [ctor-http-nio-3] o.s.c.e.PropertySourcesPropertyResolver  : Found key 'loadbalancer.client.name' in PropertySource 'loadbalancer' with value of type String
2021-01-08 12:33:27.686 DEBUG 29652 --- [ctor-http-nio-3] o.s.c.e.PropertySourcesPropertyResolver  : Found key 'spring.liveBeansView.mbeanDomain' in PropertySource 'systemProperties' with value of type String
2021-01-08 12:33:27.756 DEBUG 29652 --- [ctor-http-nio-3] r.n.resources.PooledConnectionProvider   : Creating a new [proxy] client pool [PoolFactory{evictionInterval=PT0S, leasingStrategy=fifo, maxConnections=2147483647, maxIdleTime=-1, maxLifeTime=-1, metricsEnabled=false, pendingAcquireMaxCount=-1, pendingAcquireTimeout=0}] for [/172.30.1.40:8201]
2021-01-08 12:33:27.785 DEBUG 29652 --- [ctor-http-nio-4] r.n.resources.PooledConnectionProvider   : [id: 0xa9543309] Created a new pooled channel, now 1 active connections and 0 inactive connections
2021-01-08 12:33:27.788 DEBUG 29652 --- [ctor-http-nio-4] reactor.netty.channel.BootstrapHandlers  : [id: 0xa9543309] Initialized pipeline DefaultChannelPipeline{(BootstrapHandlers$BootstrapInitializerHandler#0 = reactor.netty.channel.BootstrapHandlers$BootstrapInitializerHandler), (PooledConnectionProvider$PooledConnectionAllocator$PooledConnectionInitializer#0 = reactor.netty.resources.PooledConnectionProvider$PooledConnectionAllocator$PooledConnectionInitializer), (reactor.left.httpCodec = io.netty.handler.codec.http.HttpClientCodec), (reactor.right.reactiveBridge = reactor.netty.channel.ChannelOperationsHandler)}
2021-01-08 12:33:27.800 DEBUG 29652 --- [ctor-http-nio-4] r.n.resources.PooledConnectionProvider   : [id: 0xa9543309, L:/172.30.1.40:50777 - R:/172.30.1.40:8201] Registering pool release on close event for channel
2021-01-08 12:33:27.801 DEBUG 29652 --- [ctor-http-nio-4] r.n.resources.PooledConnectionProvider   : [id: 0xa9543309, L:/172.30.1.40:50777 - R:/172.30.1.40:8201] Channel connected, now 1 active connections and 0 inactive connections
2021-01-08 12:33:27.801 DEBUG 29652 --- [ctor-http-nio-4] r.n.resources.PooledConnectionProvider   : [id: 0xa9543309, L:/172.30.1.40:50777 - R:/172.30.1.40:8201] onStateChange(PooledConnection{channel=[id: 0xa9543309, L:/172.30.1.40:50777 - R:/172.30.1.40:8201]}, [connected])
2021-01-08 12:33:27.803 DEBUG 29652 --- [ctor-http-nio-4] r.n.resources.PooledConnectionProvider   : [id: 0xa9543309, L:/172.30.1.40:50777 - R:/172.30.1.40:8201] onStateChange(GET{uri=/, connection=PooledConnection{channel=[id: 0xa9543309, L:/172.30.1.40:50777 - R:/172.30.1.40:8201]}}, [configured])
2021-01-08 12:33:27.805 DEBUG 29652 --- [ctor-http-nio-4] r.netty.http.client.HttpClientConnect    : [id: 0xa9543309, L:/172.30.1.40:50777 - R:/172.30.1.40:8201] Handler is being applied: {uri=http://172.30.1.40:8201/areas?areaType=STATE&parentCode=KR00, method=GET}
2021-01-08 12:33:27.805 DEBUG 29652 --- [ctor-http-nio-4] r.n.resources.PooledConnectionProvider   : [id: 0xa9543309, L:/172.30.1.40:50777 - R:/172.30.1.40:8201] onStateChange(GET{uri=/areas?areaType=STATE&parentCode=KR00, connection=PooledConnection{channel=[id: 0xa9543309, L:/172.30.1.40:50777 - R:/172.30.1.40:8201]}}, [request_prepared])
2021-01-08 12:33:27.812 DEBUG 29652 --- [ctor-http-nio-3] reactor.netty.channel.FluxReceive        : [id: 0xf497146a, L:/0:0:0:0:0:0:0:1:8100 - R:/0:0:0:0:0:0:0:1:50776] FluxReceive{pending=0, cancelled=false, inboundDone=true, inboundError=null}: subscribing inbound receiver
2021-01-08 12:33:27.819 DEBUG 29652 --- [ctor-http-nio-4] r.n.resources.PooledConnectionProvider   : [id: 0xa9543309, L:/172.30.1.40:50777 - R:/172.30.1.40:8201] onStateChange(GET{uri=/areas?areaType=STATE&parentCode=KR00, connection=PooledConnection{channel=[id: 0xa9543309, L:/172.30.1.40:50777 - R:/172.30.1.40:8201]}}, [request_sent])
2021-01-08 12:33:28.086 DEBUG 29652 --- [ctor-http-nio-4] r.n.http.client.HttpClientOperations     : [id: 0xa9543309, L:/172.30.1.40:50777 - R:/172.30.1.40:8201] Received response (auto-read:false) : [Content-Type=application/json;charset=UTF-8, Transfer-Encoding=chunked, Date=Fri, 08 Jan 2021 03:33:28 GMT]
2021-01-08 12:33:28.086 DEBUG 29652 --- [ctor-http-nio-4] r.n.resources.PooledConnectionProvider   : [id: 0xa9543309, L:/172.30.1.40:50777 - R:/172.30.1.40:8201] onStateChange(GET{uri=/areas?areaType=STATE&parentCode=KR00, connection=PooledConnection{channel=[id: 0xa9543309, L:/172.30.1.40:50777 - R:/172.30.1.40:8201]}}, [response_received])
2021-01-08 12:33:28.097 DEBUG 29652 --- [ctor-http-nio-4] reactor.netty.channel.FluxReceive        : [id: 0xa9543309, L:/172.30.1.40:50777 - R:/172.30.1.40:8201] FluxReceive{pending=0, cancelled=false, inboundDone=false, inboundError=null}: subscribing inbound receiver
2021-01-08 12:33:28.114 DEBUG 29652 --- [ctor-http-nio-4] r.n.http.client.HttpClientOperations     : [id: 0xa9543309, L:/172.30.1.40:50777 - R:/172.30.1.40:8201] Received last HTTP packet
2021-01-08 12:33:28.114 DEBUG 29652 --- [ctor-http-nio-4] r.n.resources.PooledConnectionProvider   : [id: 0xa9543309, L:/172.30.1.40:50777 - R:/172.30.1.40:8201] onStateChange(GET{uri=/areas?areaType=STATE&parentCode=KR00, connection=PooledConnection{channel=[id: 0xa9543309, L:/172.30.1.40:50777 - R:/172.30.1.40:8201]}}, [response_completed])
2021-01-08 12:33:28.114 DEBUG 29652 --- [ctor-http-nio-4] r.n.resources.PooledConnectionProvider   : [id: 0xa9543309, L:/172.30.1.40:50777 - R:/172.30.1.40:8201] onStateChange(GET{uri=/areas?areaType=STATE&parentCode=KR00, connection=PooledConnection{channel=[id: 0xa9543309, L:/172.30.1.40:50777 - R:/172.30.1.40:8201]}}, [disconnecting])
2021-01-08 12:33:28.114 DEBUG 29652 --- [ctor-http-nio-4] r.n.resources.PooledConnectionProvider   : [id: 0xa9543309, L:/172.30.1.40:50777 - R:/172.30.1.40:8201] Releasing channel
2021-01-08 12:33:28.116 DEBUG 29652 --- [ctor-http-nio-4] r.n.resources.PooledConnectionProvider   : [id: 0xa9543309, L:/172.30.1.40:50777 - R:/172.30.1.40:8201] Channel cleaned, now 0 active connections and 1 inactive connections
2021-01-08 12:33:28.122  INFO 29652 --- [ctor-http-nio-3] c.k.m.g.t.LoggingGlobalPreFilter         : ===================== post filter =====================
2021-01-08 12:33:28.127 DEBUG 29652 --- [ctor-http-nio-3] o.s.w.s.adapter.HttpWebHandlerAdapter    : [f497146a-1] Completed 200 OK

 

 

Global Filter 말고 특정 route 에서만 Filter를 적용하고 싶은 경우에는 FilterFactory를 쓰면 된다. Globa Filter는 GlobalFilter 인터페이스를 구현하면 되었지만 FilterFactory는 AbstractGatewayFilterFactory 추상 클래스의 apply 메소드를 구현하면 된다.

package com.kst.macaront.gw.testfilter;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

@Slf4j
@Component
public class LoggingGatewayFilterFactory extends AbstractGatewayFilterFactory<LoggingGatewayFilterFactory.Config> {
    public LoggingGatewayFilterFactory() {
        super(Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            if (config.isPreLogger()) {
                log.info("Pre GatewayFilter logging: " + config.getBaseMessage());
            }

            return chain.filter(exchange)
                    .then(Mono.fromRunnable(() -> {
                        if (config.isPostLogger()) {
                            log.info("Post GatewayFilter logging: " + config.getBaseMessage());
                        }
                    }));
        };
    }

    @NoArgsConstructor
    @AllArgsConstructor
    @Getter
    @Setter
    public static class Config {
        private String baseMessage;
        private boolean preLogger;
        private boolean postLogger;
    }
}

 

Pre는 chain.filter를 실행하기 전에 Post는 실행 후에 넣으면 된다.

이렇게 만들어 놓고 난 후 다음처럼 사용하면 된다.

yml 방식

spring:
  cloud:
    gateway:
      routes:
        - id: filter-factory
          uri: http://httpbin.org
          predicates:
          - Path=/get
          filters:
          - AddRequestHeader=Hello, World
          - name: Logging
            args:
              baseMessage: My Custom Message
              preLogger: true
              postLogger: true

 

Java 방식

    @Bean
    public RouteLocator loggingRoutes(RouteLocatorBuilder builder, UriConfiguration uriConfiguration, LoggingGatewayFilterFactory loggingGatewayFilterFactory) {
        String testUrl = uriConfiguration.getTest();

        return builder.routes()
                .route(p -> p
                    .path("/get")
                    .filters(f -> f
                        .addRequestHeader("Hello", "World")
                        .filter(loggingGatewayFilterFactory.apply(new LoggingGatewayFilterFactory.Config("message", true, true)))
                    )
                    .uri(testUrl)
                )
                .build();
    }

 

필터의 순서를 수정하고자 할 경우에는 OrderedGatewayFilter를 리턴하도록 만들면 된다.

    @Override
    public GatewayFilter apply(Config config) {
        return new OrderedGatewayFilter((exchange, chain) -> {
            if (config.isPreLogger()) {
                log.info("Pre GatewayFilter logging: " + config.getBaseMessage());
            }

            return chain.filter(exchange)
                    .then(Mono.fromRunnable(() -> {
                        if (config.isPostLogger()) {
                            log.info("Post GatewayFilter logging: " + config.getBaseMessage());
                        }
                    }));
        }, 1);
    }

순서 없는 filter를 만들 경우에는 로그에서 Sorted gatewayFilterFactories 가 다음과 같이 보인다. LoggingGatewayFilterFactory의 order가 2인 것을 볼 수 있다. 어떤 원리에 의해 순서가 정해지는지는 알 수 없다.

[
	[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RemoveCachedBodyFilter@75aa7703}, order = -2147483648],
	[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter@5e360c3b}, order = -2147482648],
	[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyWriteResponseFilter@124ff64d}, order = -1],
	[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardPathFilter@5e05a706}, order = 0],
	[
		[AddRequestHeader Hello = 'World'], order = 1
	],
	[com.kst.macaront.gw.testfilter.LoggingGatewayFilterFactory$$Lambda$641/638622177@5574e1c6, order = 2],
	[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter@79777da7}, order = 10000],
	[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter@737ff5c4}, order = 10150],
	[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.WebsocketRoutingFilter@7831d1aa}, order = 2147483646],
	[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyRoutingFilter@7e9a836}, order = 2147483647],
	[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardRoutingFilter@3395c2a7}, order = 2147483647]
]

다만 위에서 OrderedGatewayFilter의 두 번째 인수로 order 값을 1로 지정할 경우에는 다음과 같이 Sorted gatewayFilterFactories 가 정해지는 것을 볼 수 있다.

Sorted gatewayFilterFactories: 
[
	[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RemoveCachedBodyFilter@124ff64d}, order = -2147483648],
	[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter@55397d15}, order = -2147482648],
	[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyWriteResponseFilter@7e9a836}, order = -1],
	[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardPathFilter@3395c2a7}, order = 0],
	[
		[AddRequestHeader Hello = 'World'], order = 1
	],
	[com.kst.macaront.gw.testfilter.LoggingGatewayFilterFactory$$Lambda$641/168549346@164eeb96, order = 1],
	[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter@75aa7703}, order = 10000],
	[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter@360a3106}, order = 10150],
	[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.WebsocketRoutingFilter@5e05a706}, order = 2147483646],
	[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.NettyRoutingFilter@737ff5c4}, order = 2147483647],
	[GatewayFilterAdapter{delegate=org.springframework.cloud.gateway.filter.ForwardRoutingFilter@79777da7}, order = 2147483647]
]

 

기본적인 Filter의 사용법에 대해 알았으니 실질적으로 어떻게 만드는지에 대해서도 확인이 필요하다.

그와 관련하여 www.baeldung.com/spring-cloud-custom-gateway-filters#advanced-scenarios 에서 자세한 내용은 다루고 있지만 해당 내용을 직접 테스트 해 보고 아래 정리해 둔다.

위 링크에서 다루고자 하는 시나리오는

  1. Accept-Language header를 받으면 OK
  2. 그게 아니면 locale query parameter 사용
  3. 둘 다 존재하지 않으면 기본 locale 값 사용
  4. 마지막으로 local query param 제거

exchange에서 request object를 가져올 수 있으며 header 값을 검사해 볼 수 있다.

    @Override
    public GatewayFilter apply(Config config) {
        return new OrderedGatewayFilter((exchange, chain) -> {
            if (config.isPreLogger()) {
                log.info("Pre GatewayFilter logging: " + config.getBaseMessage());
            }

            if (exchange.getRequest().getHeaders().getAcceptLanguage().isEmpty()) {
                log.info("populate the Accept-Language header...");
            }

            return chain.filter(exchange)
                    .then(Mono.fromRunnable(() -> {
                        if (config.isPostLogger()) {
                            log.info("Post GatewayFilter logging: " + config.getBaseMessage());
                        }
                    }));
        }, 1);
    }

시나리오에서 헤더 값에 없을 경우 parameter 값을 사용하고 둘 다 없을 경우 기본 Locale 값을 사용하겠다고 했으니 아래와 같이 작성을 하면 된다.

    @Override
    public GatewayFilter apply(Config config) {
        return new OrderedGatewayFilter((exchange, chain) -> {
            if (config.isPreLogger()) {
                log.info("Pre GatewayFilter logging: " + config.getBaseMessage());
            }

            if (exchange.getRequest().getHeaders().getAcceptLanguage().isEmpty()) {
                log.info("populate the Accept-Language header...");
                String queryParamLocale = exchange.getRequest().getQueryParams().getFirst("locale");
                Locale requestLocale = Optional.ofNullable(queryParamLocale)
                        .map(l -> Locale.forLanguageTag(l))
                        .orElse(config.getDefaultLocale());

                exchange.getRequest()
                        .mutate()
                        .headers(h -> h.setAcceptLanguageAsLocales(Collections.singletonList(requestLocale)));
            }

            return chain.filter(exchange)
                    .then(Mono.fromRunnable(() -> {
                        if (config.isPostLogger()) {
                            log.info("Post GatewayFilter logging: " + config.getBaseMessage());
                        }
                    }));
        }, 1);
    }
    
    @NoArgsConstructor
    @AllArgsConstructor
    @Getter
    @Setter
    public static class Config {
        private String baseMessage;
        private boolean preLogger;
        private boolean postLogger;
        private Locale defaultLocale = Locale.KOREA;
    }

Config에는 기본 Local 값을 설정하고 pre filter 부분에서 header 값과 param 값을 검증한 다음 없을 경우 Config의 default Locale 값을 사용하게 된다.

Request Header에서 Accept-Language 를 "ko"로 셋팅해서 호출하게 되면 아래와 같다.

요청한 대로 잘 전달됨을 볼 수 있다.

반면 Accept-Language를 빼고 호출하게 되면 다음과 같이 결과가 나온다. 기본 값으로 설정한 java.util.Locale의 Locale.KOREA 값인 ko-kr로 변경된다.

ko가 아닌 ko-kr로 전달되게 된다.

param으로 locale 값을 전달하게 될 경우에는 다음과 같이 전달하게 된다.

param으로 전달 된 ko 값을 사용한다.

Target Service에서 필요없는 param을 제거해서 넘겨야 하기 때문에 아래와 같이 exchange 값을 변경하여 chain에 넘기게 된다.

    @Override
    public GatewayFilter apply(Config config) {
        return new OrderedGatewayFilter((exchange, chain) -> {
            if (config.isPreLogger()) {
                log.info("Pre GatewayFilter logging: " + config.getBaseMessage());
            }

            if (exchange.getRequest().getHeaders().getAcceptLanguage().isEmpty()) {
                log.info("populate the Accept-Language header...");
                String queryParamLocale = exchange.getRequest().getQueryParams().getFirst("locale");
                Locale requestLocale = Optional.ofNullable(queryParamLocale)
                        .map(l -> Locale.forLanguageTag(l))
                        .orElse(config.getDefaultLocale());

                exchange.getRequest()
                        .mutate()
                        .headers(h -> h.setAcceptLanguageAsLocales(Collections.singletonList(requestLocale)));
            }

            ServerWebExchange modifiedExchange = exchange.mutate()
                    .request(
                            originalRequest -> originalRequest.uri(
                                    UriComponentsBuilder.fromUri(exchange.getRequest().getURI())
                                            .replaceQueryParams(new LinkedMultiValueMap<String, String>())
                                            .build().toUri()
                            )
                    )
                    .build();

            return chain.filter(modifiedExchange)
                    .then(Mono.fromRunnable(() -> {
                        if (config.isPostLogger()) {
                            log.info("Post GatewayFilter logging: " + config.getBaseMessage());
                        }
                    }));
        }, 1);
    }

modifiedExchange 값을 chain에 넘기도록 수정 후 param에 locale 값을 붙여 넘기게 되면 args 값이 사라졌음을 볼 수 있다.

args가 빈 값이다.

Target Service로부터 받은 헤더를 이용하여 response에 특정 이름으로 헤더를 셋팅해서 내려줄 경우에는 아래와 같이 하면 된다.

참조한 www.baeldung.com/ 사이트에서는 getContentLanguage를 사용하였는데 개인적으로 사용한 테스트 Target Site에서는 content-lanage header가 없어서 NPE가 발생하였다. 테스트를 위한 것이기에 아래 코드에서는 존재하는 헤더 값인 Access-Control-Allow-Origin 를 사용하였다. 

            return chain.filter(modifiedExchange)
                    .then(Mono.fromRunnable(() -> {
                        if (config.isPostLogger()) {
                            log.info("Post GatewayFilter logging: " + config.getBaseMessage());
                        }

                        ServerHttpResponse response = exchange.getResponse();

                        Optional.ofNullable(exchange.getRequest().getQueryParams().getFirst("locale"))
                                .ifPresent(qp -> {
                                    String responseContentLanquage = response.getHeaders().getAccessControlAllowOrigin();
                                    response.getHeaders().add("Bael-Custom-Language-Header", responseContentLanquage);
                                });
                    }));

Access-Control-Allow-Origin 값을 그대로 사용하여 Bael-Custom-Language-Header 라는 헤더가 추가 되었다

 

기본적인 config properties 관련된 내용은 docs.spring.io/spring-cloud-gateway/docs/2.2.6.BUILD-SNAPSHOT/reference/html/appendix.html 를 참고

반응형