Programming/Spring Cloud

Spring Cloud Gateway를 이용한 API Gateway 구축해보기

Jan92 2022. 11. 27. 22:44

어느 정도 규모 이상의 마이크로 서비스 아키텍처 기반 서비스에서는 API Gateway를 도입하는 것이 효율적이라고 하는데요.

API Gateway는 쉽게 클라이언트와 각각의 서비스들 사이에 위치하며, 요청을 라우팅 해주는 등의 역할을 수행하는 서비스로 아래 내용은 Spring Cloud Gateway(SCG)의 동작 원리 및 구현 예제입니다.

 

2022.11.20 - [Programming/Spring Cloud] - API Gateway란? 개념과 주요 기능

(API Gateway에 대한 조금 더 자세한 내용이 궁금하시다면 다음 포스팅을 참고해주시면 좋을 것 같습니다.)

 

 

 

Spring Cloud Gateway 구성

출처: https://spring.io/projects/spring-cloud-gateway

스프링 클라우드 게이트웨이는 MSA 환경에서 사용되는 API Gateway 중 하나로 Spring5, Spring Boot2, Project Reactor로 구축된 API Gateway인데요. SCG의 역할은 위에 설명처럼 API 라우팅 및 보안, 모니터링, 메트릭 등의 기능을 간단하고 효과적인 방법으로 제공한다고 합니다.

Spring Cloud Gateway의 구성은 크게 Route, Predicate, Filter 3가지로 구성되어 있는데요. 각각에 대해 살펴보면 다음과 같습니다.

 

Route

Route는 API Gateway에서 가장 기본이 되는 요소로 요청할 서비스의 고유한 값인 id, 요청할 uri, Predicate, Filter로 구성되어 있습니다. 요청된 uri의 조건이 predicate와 일치하는지 확인 후, 일치하는 경우 해당 uri 경로로 요청을 매칭 시켜줍니다.

 

Predicate

API Gateway로 들어온 요청이 주어진 조건을 만족하는지 확인하는 구성요소입니다.

하나 이상의 조건을 정의할 수 있으며, 만약 Predicate 조건에 맞지 않는 경우 HTTP 404 Not Found 응답을 반환합니다.

 

Filter

API Gateway로 들어오는 요청에 대해 Filter를 적용하여 선처리 및 후처리를 할 수 있게 해주는 구성요소입니다.

 

 

 

Spring Cloud Gateway 동작원리

spring cloud gateway 동작원리

위 Route, Predicate, Filter의 동작 원리를 조금 더 자세하게 살펴보면 다음과 같은데요.

 

클라이언트에서 들어온 요청은 Gateway Handler Mapping을 통해 요청 경로와 일치 여부를 판단하게 되고, Gateway Web Handler에서 요청과 관련된 필터 체인을 통해 요청이 전송되게 됩니다.

이후 적용되는 Filter를 통해 요청 또는 응답에 필요한 전처리, 후처리를 할 수 있으며 Proxy Filter는 프록시 요청이 처리될 때 수행됩니다.

 

 

 

Spring Cloud Gateway 기본적인 Routing 구현

first-service, second-service

먼저 구현해볼 SCG 예시는 클라이언트의 요청이 단일 진입점인 Spring Cloud Gateway를 통해 두 개의 서비스(first-service, second-service)에 요청을 하고 결과를 받는 간단한 예시입니다.

 

//first-service application.yml
server:
  port: 8081

(application.yml)

 

//first-service FirstServiceController
@RestController
@RequestMapping("/first-service")
public class FirstServiceController {

    @Value("${server.port}")
    private String port;

    @GetMapping("/check")
    public String check() {
        return String.format("First Service on PORT %s", port);
    }
}

(Controller)

 

해당 코드는 요청을 받을 두 개의 서비스인 first-service와 second-service에 대한 내용입니다.

해당 서비스들은 spring web, lombok dependency만 추가하여 포트 설정 및 테스트 API 하나만을 간단하게 만들었습니다.

(second-service의 port는 8082로 설정하였으며 사용되는 키워드만 first -> second로 바꿔 똑같이 생성하였습니다.)

 

 

 

apigateway-service

apigateway-service는 lombok, spring-cloud-starter-gateway dependency를 추가하여 생성했는데요.

 

스프링 클라우드 게이트웨이 동작을 위한 코드를 작성하는 방식에는 application.yml(또는. properties)에 작성하는 방법과 Java Code로 작성하는 두 가지 방법이 있습니다.

 

server:
  port: 8000

spring:
  cloud:
    gateway:
      routes:
        - id: first-service
          uri: http://localhost:8081/
          predicates:
            - Path=/first-service/**
        - id: second-service
          uri: http://localhost:8082/
          predicates:
            - Path=/second-service/**
          filters:
            - AddRequestHeader=second-request, second-request-header
            - AddResponseHeader=second-response, second-response-header

(application.yml 예시)

application.yml으로 Spring Cloud Gateway를 적용했을 때의 동작 방식은 아래와 같습니다.

  1. locallost:8000/first-service/~~ 요청이 게이트웨이로 들어옵니다.
  2. 요청받은 uri의 조건을 predicates에서 탐색합니다.
    (cloud.gateway.routes.id.predicate의 /first-service/** 가 확인됩니다.)
  3. 해당 route에 해당되는 uri인 http://localhost:8081/ 으로 요청을 전달합니다.

(second-service에 적용된 filters 부분은 RequestHeader와 ResponseHeader를 추가하기 위해 사용되는 방법입니다.)

 

 

@Configuration
public class FilterConfig {

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

(java code 예시)

그리고 똑같은 내용을 .yml 파일이 아닌 java code로 구현하였을 때는 다음과 같은데요. 동작 내용은 위와 같습니다.

 

 

 

Spring Cloud Gateway Filter 구현

 

1. Custom Filter

간단한 request header, response header 추가 작업은 위 예시와 같이 적용할 수도 있는데요.

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

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

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

    @Override
    public GatewayFilter apply(Config config) {
        // Custom Pre Filter. Suppose we can extract JWT and perform Authentication
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            ServerHttpResponse response = exchange.getResponse();

            log.info("Custom PRE filter: request id -> {}", request.getId());
            // Custom Post Filter. Suppose we can call error response handler based on error code.
            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
               log.info("Custom POST filter: response code -> {}", response.getStatusCode());
            }));
        };
    }
}

Custom Filter를 적용하기 위해서는 AbstractGatewayFilterFactory<C> 추상 클래스를 상속받는 클래스를 생성해야 합니다.

 

이때 실제 필터가 동작하는 apply() 메서드를 보면, 내부에서 동작하는 람다 함수의 첫 번째 매개변수로 ServerWebExchange 타입이 들어가고, 두 번째 매개변수로는 GatewayFilterChain이 들어가서 동작하게 되는데요.

주의해야 할 점은 이후 사용되는 request, response가 ServletRequest, ServletResponse가 아니라 Spring Reactive의 ServerHttpRequest, Response라는 점입니다.

 

/*

import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;

*/

 

 

routes:
  - id: first-service
    uri: http://localhost:8081/
    predicates:
      - Path=/first-service/**
    filters:
      - CustomFilter

생성된 필터는 다음과 같이 각각의 Route에 적용할 수 있습니다.

 

 

 

2. Global Filter

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

    @Data
    public static class Config {
        private String baseMessage;
        private boolean preLogger;
        private boolean postLogger;
    }

    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()) {
                // Custom Pre Filter.
                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());
                }
            }));
        });
    }
}

Global Filter 역시 기본적인 구현 방법은 같은데요.

 

 

spring:
  cloud:
    gateway:
      default-filters:
        - name: GlobalFilter
          args:
            baseMessage: Spring Cloud Gateway GlobalFilter
            preLogger: true
            postLogger: true
      routes:
        - id: first-service
          uri: http://localhost:8081/
          predicates:
            - Path=/first-service/**

해당 Filter를 Spring Cloud Gateway 전역에 적용시키기 위한 application.yml 파일 설정 부분이 조금 차이가 있으며, 여기서는 .yml 파일에 Global Filter 동작을 위한 인자를 추가로 입력하여 내부적으로 사용되는 방식이 적용되었습니다.

 

 

 

Gateway Route 노출

필요에 따라 Gateway Route 적용 내용 전체를 확인할 수 있는 방법도 있는데요.

 

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-actuator -->
<dependency>
    <groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

(pom.xml)

 

management:
  endpoints:
    web:
      exposure:
        include:
          - "gateway"
  endpoint:
    gateway:
      enabled: true  # default: true

(application.yml)

 

/actuator/gateway/routes

spring-boot-starter-actuator 의존성을 추가하고 yml 파일에 다음 설정을 추가하여 예시와 같이 Gateway의 url mapping 정보를 확인할 수 있습니다.

 

/*

management.endpoint.gateway.enabled 설정은 default 값이 true이기 때문에 따로 설정할 필요는 없지만, 운영환경 등 노출을 하고 싶지 않은 경우에는 false로 설정해야 합니다.

*/

 

 

이상으로 Spring Cloud Gateway 구축 예시였습니다. 잘못된 내용이나 궁금하신 부분은 댓글 주시면 답변드리도록 하겠습니다.

이어지는 포스팅에서는 구현된 Spring Cloud Gateway에 Service Discovery 역할을 하는 Eureka를 연동하는 내용을 기록하여 추후 함께 링크하도록 하겠습니다.

 

 

 

 

< 함께 보면 좋은 자료 >

2022.11.09 - [Programming/Spring Cloud] - 클라우드 환경 Service Discovery 개념 정리

 

< 참고 자료 >

https://saramin.github.io/2022-01-20-spring-cloud-gateway-api-gateway/

https://wonit.tistory.com/500

 

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%EC%84%9C%EB%B9%84%EC%8A%A4/dashboard

(해당 강의는 직접 결제해서 듣고 있는 인프런의 Spring Cloud 강의로 광고가 아님을 알려드립니다.)