Spring Cloud로 개발하는 마이크로서비스 애플리케이션(MSA)-2.API Gateway Service

Published: by Creative Commons Licence

Spring Cloud Netflix Zuul 은 Spring Boot 2.4에서 Maintenance 상태이다. 오늘은 Spring Cloud Gateway에 대한 정리만 할 예정이다.

Spring Cloud Gateway

pom.xml

프로젝트 생성 시 Spring Cloud Routing > gateway(spring-cloud-starter-gateway)를 선택한다.
spring-cloud-starter-netflix-eureka-client, lombok 은 나중 작업을 위해 미리 추가한 라이브러리이다.

 <dependencies>
        <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>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

Spring Cloud Gateway-Filter 적용

아래 두개의 예시의 결과는 동일하다.

  • @Configuration Bean을 등록해서 적용 - Java 레벨에서 RequestHeader, ResponseHeader에 값을 설정한 예시이다.
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FilterConfig {

    @Bean
    public RouteLocator gatewayRoutes(RouteLocatorBuilder builder){
        return builder.routes()
                .route(r -> r.path("/first-service/**")
                        .filters(f -> f.addRequestHeader("first-request", "first-request-header")
                                        .addResponseHeader("first-response", "first-response-header"))
                        .uri("http://localhost:8081"))
                .route(r -> r.path("/second-service/**")
                        .filters(f -> f.addRequestHeader("second-request", "second-request-header")
                                .addResponseHeader("second-response", "second-response-header"))
                        .uri("http://localhost:8082"))
                .build();

    }
}
  • yaml 파일로 적용 - Yaml 설정파일로 RequestHeader, ResponseHeader에 값을 설정한 예시이다.
    server:
    port: 8000
    eureka:
    client:
      register-with-eureka: false
      fetch-registry: false
    spring:
    application:
      name: apigateway-service
    cloud:
      gateway:
        routes:
          - id: first-service
            uri: http://localhost:8081
            predicates:
              - Path=/first-service/**
            filters:
              - AddRequestHeader=first-request, first-request-header
              - AddResponseHeader=first-response, first-response-header
          - id: second-service
            uri: http://localhost:8082
            predicates:
              - Path=/second-service/**
            filters:
              - AddRequestHeader=second-request, second-request-header
              - AddResponseHeader=second-response, second-response-header
    

Spring Cloud Gateway-Custom Filter 적용

Custom Filter 클래스 생성

AbstractGatewayFilterFactory 상속받고 apply 메소드를 재정의 한다.
Config inner class 를 만든다.
기본생성자를 만든다.

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

@Component
@Slf4j
public class CustomFilter extends AbstractGatewayFilterFactory<CustomFilter.Config> {

    public CustomFilter() {
        super(Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {
        // Custom Pre Filter
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            ServerHttpResponse response = exchange.getResponse();

            log.info("Custom PRE filter : request id -> {}", request.getId());

            // Custom Post Filter
            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                log.info("Custom POST filter : response code -> {}", response.getStatusCode());
            }));
        };
    }

    public static class Config {
        // Put the configuration properties
    }
}

application.yml 파일에 CustomFilter 등록

AddRequestHeader, AddResponseHeader를 주석하고 CustomFilter를 등록한다.

server:
  port: 8000
eureka:
  client:
    register-with-eureka: false
    fetch-registry: false
spring:
  application:
    name: apigateway-service
  cloud:
    gateway:
      routes:
        - id: first-service
          uri: http://localhost:8081
          predicates:
            - Path=/first-service/**
          filters:
#            - AddRequestHeader=first-request, first-request-header2
#            - AddResponseHeader=first-response, first-response-header2
            - CustomFilter
        - id: second-service
          uri: http://localhost:8082
          predicates:
            - Path=/second-service/**
          filters:
#            - AddRequestHeader=second-request, second-request-header2
#            - AddResponseHeader=second-response, second-response-header2
            - CustomFilter

Spring Cloud Gateway-Global Filter 적용

Global Filter 클래스 생성

Global Filter를 만드는 방법은 Custom Filter와 동일하다.
AbstractGatewayFilterFactory 상속받고 apply 메소드를 재정의 한다.
Config inner class 를 만든다.
기본생성자를 만든다.

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

@Component
@Slf4j
public class GlobalFilter extends AbstractGatewayFilterFactory<GlobalFilter.Config> {

    public GlobalFilter() {
        super(Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            ServerHttpResponse response = exchange.getResponse();

            log.info("Global filter baseMessage : {}", config.getBaseMessage());

            if(config.isPreLogger()){
                log.info("Global filter Start : request id -> {}", request.getId());
            }

            // Custom Post Filter
            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                if(config.isPostLogger()){
                    log.info("Global filter End : response code -> {}", response.getStatusCode());
                }
            }));
        };
    }

    @Data
    public static class Config {
        // Put the configuration properties
        private String baseMessage;
        private boolean preLogger;
        private boolean postLogger;
    }
}

application.yml 파일에 GlobalFilter 등록

spring.cloud.gateway.default-filters에 Global Filter를 등록한다.

server:
  port: 8000
eureka:
  client:
    register-with-eureka: false
    fetch-registry: false
spring:
  application:
    name: apigateway-service
  cloud:
    gateway:
      default-filters:
        - name: GlobalFilter
          args:
            baseMessage: Spring Cloud Gateway Global Filter
            PreLogger: true
            PostLogger: true
      routes:
        - id: first-service
          uri: http://localhost:8081
          predicates:
            - Path=/first-service/**
          filters:
#            - AddRequestHeader=first-request, first-request-header2
#            - AddResponseHeader=first-response, first-response-header2
            - CustomFilter
        - id: second-service
          uri: http://localhost:8082
          predicates:
            - Path=/second-service/**
          filters:
#            - AddRequestHeader=second-request, second-request-header2
#            - AddResponseHeader=second-response, second-response-header2
            - CustomFilter

Spring Cloud Gateway-Logging Filter 적용

Logging Filter 클래스 생성

기존 작업한 Global Filter와 거의 동일하기 때문에 GlobalFilter.java 파일을 복사해서 작업한다.
OrderedGatewayFilter 를 구현한다.

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.OrderedGatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

@Component
@Slf4j
public class LoggingFilter extends AbstractGatewayFilterFactory<LoggingFilter.Config> {

    public LoggingFilter() {
        super(Config.class);
    }

    // Ordered.HIGHEST_PRECEDENCE
    // Ordered.LOWEST_PRECEDENCE
    @Override
    public GatewayFilter apply(Config config) {
        GatewayFilter filter = new OrderedGatewayFilter((exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            ServerHttpResponse response = exchange.getResponse();

            log.info("Logging filter baseMessage : {}", config.getBaseMessage());

            if(config.isPreLogger()){
                log.info("Logging PRE filter : request id -> {}", request.getId());
            }

            // Custom Post Filter
            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                if(config.isPostLogger()){
                    log.info("Logging POST filter : response code -> {}", response.getStatusCode());
                }
            }));
        }, Ordered.LOWEST_PRECEDENCE);

        return filter;
    }

    @Data
    public static class Config {
        // Put the configuration properties
        private String baseMessage;
        private boolean preLogger;
        private boolean postLogger;
    }
}

application.yml 파일에 LoggingFilter 등록

second 서비스에만 Logging Filter 를 적용.

server:
  port: 8000
eureka:
  client:
    register-with-eureka: false
    fetch-registry: false
spring:
  application:
    name: apigateway-service
  cloud:
    gateway:
      default-filters:
        - name: GlobalFilter
          args:
            baseMessage: Spring Cloud Gateway Global Filter
            PreLogger: true
            PostLogger: true
      routes:
        - id: first-service
          uri: http://localhost:8081
          predicates:
            - Path=/first-service/**
          filters:
#            - AddRequestHeader=first-request, first-request-header2
#            - AddResponseHeader=first-response, first-response-header2
            - CustomFilter
        - id: second-service
          uri: http://localhost:8082
          predicates:
            - Path=/second-service/**
          filters:
            - name: CustomFilter
            - name: LoggingFilter
              args:
                baseMessage: Hi, there
                PreLogger: true
                PostLogger: true

Ordered.HIGHEST_PRECEDENCE 실행결과

OrderedGatewayFilter 생성시 우선순위 Ordered.HIGHEST_PRECEDENCE 로 설정

2022-02-06 14:11:02.101  INFO 21280 --- [ctor-http-nio-2] c.s.a.filter.LoggingFilter               : Logging filter baseMessage : Hi, there
2022-02-06 14:11:02.101  INFO 21280 --- [ctor-http-nio-2] c.s.a.filter.LoggingFilter               : Logging PRE filter : request id -> 37903ef2-3
2022-02-06 14:11:02.101  INFO 21280 --- [ctor-http-nio-2] c.s.a.filter.GlobalFilter                : Global filter baseMessage : Spring Cloud Gateway Global Filter
2022-02-06 14:11:02.101  INFO 21280 --- [ctor-http-nio-2] c.s.a.filter.GlobalFilter                : Global filter Start : request id -> 37903ef2-3
2022-02-06 14:11:02.101  INFO 21280 --- [ctor-http-nio-2] c.s.a.filter.CustomFilter                : Custom PRE filter : request id -> 37903ef2-3
2022-02-06 14:11:02.115  INFO 21280 --- [ctor-http-nio-2] c.s.a.filter.CustomFilter                : Custom POST filter : response code -> 200 OK
2022-02-06 14:11:02.115  INFO 21280 --- [ctor-http-nio-2] c.s.a.filter.GlobalFilter                : Global filter End : response code -> 200 OK
2022-02-06 14:11:02.116  INFO 21280 --- [ctor-http-nio-2] c.s.a.filter.LoggingFilter               : Logging POST filter : response code -> 200 OK

Ordered.LOWEST_PRECEDENCE 실행결과

OrderedGatewayFilter 생성시 우선순위 Ordered.LOWEST_PRECEDENCE 로 설정

2022-02-06 14:15:44.650  INFO 18540 --- [ctor-http-nio-2] c.s.a.filter.GlobalFilter                : Global filter baseMessage : Spring Cloud Gateway Global Filter
2022-02-06 14:15:44.650  INFO 18540 --- [ctor-http-nio-2] c.s.a.filter.GlobalFilter                : Global filter Start : request id -> 7abe4ddd-1
2022-02-06 14:15:44.651  INFO 18540 --- [ctor-http-nio-2] c.s.a.filter.CustomFilter                : Custom PRE filter : request id -> 7abe4ddd-1
2022-02-06 14:15:46.625  INFO 18540 --- [ctor-http-nio-2] c.s.a.filter.LoggingFilter               : Logging filter baseMessage : Hi, there
2022-02-06 14:15:46.626  INFO 18540 --- [ctor-http-nio-2] c.s.a.filter.LoggingFilter               : Logging PRE filter : request id -> 7abe4ddd-1
2022-02-06 14:15:46.626  INFO 18540 --- [ctor-http-nio-2] c.s.a.filter.LoggingFilter               : Logging POST filter : response code -> 200 OK
2022-02-06 14:15:46.626  INFO 18540 --- [ctor-http-nio-2] c.s.a.filter.CustomFilter                : Custom POST filter : response code -> 200 OK
2022-02-06 14:15:46.626  INFO 18540 --- [ctor-http-nio-2] c.s.a.filter.GlobalFilter                : Global filter End : response code -> 200 OK

Spring Cloud Gateway- Eureka 연동

apigateway-service, first-service, second-service pom.xml 확인

Eureka Client 라이브러리 확인

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

apigateway-service, first-service, second-service application.yml 확인

Eureka 서버에 등록을 하기 위한 Eureka Client 설정 확인

eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://localhost:8761/eureka

apigateway-service application.yml spring.cloud.routes.uri 변경

ip:port로 작성된 uri를 lb://{서비스명}으로 변경한다.

spring:
  application:
    name: apigateway-service
  cloud:
    gateway:
      default-filters:
        - name: GlobalFilter
          args:
            baseMessage: Spring Cloud Gateway Global Filter
            PreLogger: true
            PostLogger: true
      routes:
        - id: first-service
          uri: lb://MY-FIRST-SERVICE
          predicates:
            - Path=/first-service/**
          filters:
#            - AddRequestHeader=first-request, first-request-header2
#            - AddResponseHeader=first-response, first-response-header2
            - CustomFilter
        - id: second-service
          uri: lb://MY-SECOND-SERVICE
          predicates:
            - Path=/second-service/**
          filters:
#            - AddRequestHeader=second-request, second-request-header2
#            - AddResponseHeader=second-response, second-response-header2
            - name: CustomFilter
            - name: LoggingFilter
              args:
                baseMessage: Hi, there
                PreLogger: true
                PostLogger: true

first-service application.yml 로드밸런싱을 위한 설정

server.port: 0으로 변경
eureka.instance.instance-id 추가

server:
  port: 0

spring:
  application:
    name: my-first-service

eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://localhost:8761/eureka
  instance:
    instance-id: ${spring.application.name}:${spring.application.instance_id:${random.value}}

Eureka Dashboard 화면

APIGATEWAY-SERVICE 1개, MY-FIRST-SERVICE 2개, MY-SECOND-SERVICE 2개가 등록된 것을 확인할 수 있다.
MY-FIRST-SERVICE는 두 개의 Random PORT 로 설정된 것을 확인 할 수 있다.

Github

https://github.com/sisipapa/spring-cloud-inflearn.git