Spring

Spring Cloud Gateway - API Gateway 맛보기

일태우 2020. 10. 19. 14:03

1. 앞서 알고가기

Spring Cloud Gateway 로 API Gateway 구축에 앞서 필요한 개념을 먼저 간단하게 정리해봤다.

 

1-1. API Gateway 란?

인증 / 모니터링 / 오케스트레이션 등과 같은 기능을 포함한 향상된 Reverse Proxy다. Netflix zuul, Amazon API Gateway, Spring Cloud Gateway 같은 것들이 잘 알려진 Api Gateway 구현체들이다.

 

1-2. Spring Cloud Gateway (이하 SCG) 란?

Spring Reactvie 생태계에 구현된 API Gateway이다. Gateway Handler Mapping으로 들어오는 요청들을 적절한 대상으로 라우팅하는 간단하고 효과적인 방법을 제공한다.

그리고 Spring Cloud Gateway는 논블로킹(non-blocking), 비동기(Asynchronous) 방식의  Netty Server를 사용한다. 때문에 서블릿 컨테이너나 WAR로 빌드하면 동작하지 않는다.

 

아래는 SCG의 라우팅이 작동하는 방식에 대한 흐름도다.

Spring Cloud Gateway 아키텍쳐의 흐름

SCG는 3가지 핵심 단위로 이루어져있다.

1. Route: 대상 URI, 조건부 집합(Predicates), 각종 필터들로 이루어진 요청을 라우팅할 대상들이라고 생각하자.

2. Predicate: Java 8의 Function Predicate로, 라우팅에 필요한 조건이다. 예로 path = /abc 같은 조건이나, request header의 특정 키-값도 조건으로 사용할 수 있다.

3. Filter: Spring Framework의 WebFilter 인스턴스이다.  Filter에서는 요청 전후로 요청 / 응답을 추가/ 수정 할 수 있다.

 

2. 구현

샘플로 구현할 프로젝트의 구성은 1개의 Gateway와 2개의 MSA로 구성하고, Gateway에서 2개의 서비스를 라우팅할 것이다.

 

2-1 API Gateway

build.gradle

plugins {
    id 'org.springframework.boot' version '2.3.4.RELEASE'
    id 'io.spring.dependency-management' version '1.0.10.RELEASE'
    id 'java'
}

allprojects {
    group = "kr.taeu"
    version = "0.0.1-SNAPSHOT"
    sourceCompatibility = "1.8"
    targetCompatibility = "1.8"

    repositories {
        mavenCentral()
    }

    apply plugin: 'org.springframework.boot'
    apply plugin: 'io.spring.dependency-management'
    apply plugin: 'java'

    configurations {
        compileOnly {
            extendsFrom annotationProcessor
        }
    }

    dependencyManagement {
        imports {
            mavenBom 'org.springframework.cloud:spring-cloud-dependencies:Hoxton.SR8'
        }
    }

    dependencies {
        annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
        developmentOnly("org.springframework.boot:spring-boot-devtools")
        implementation("org.springframework.boot:spring-boot-starter-actuator")
        
        annotationProcessor("org.projectlombok:lombok")
        compileOnly("org.projectlombok:lombok")

        testImplementation("org.springframework.boot:spring-boot-starter-test") {
            exclude group: ("org.junit.vintage"), module: ("junit-vintage-engine")
        }
    }
}

project (':gateway-server') {
    dependencies {
        implementation("org.springframework.cloud:spring-cloud-starter-gateway")
    }
}

SCG의 라우팅 코드 작성방식은 Java-config 방식과 xml 방식이 있는데, 여기서는 xml 방식으로 빠르게 구현하겠다.

gateway-server의 resources 디렉토리에 application.yml을 생성하고 다음과 같이 입력한다.

 

application.yml

# Spring Cloud Gateway port
server:
  port: 8080

spring:
  cloud:
    gateway:
      # Gateway 공통 필터
      default-filters:
        - name: GlobalFilter
          args:
            baseMessage: Spring Cloud Gateway GlobalFilter
            preLogger: true
            postLogger: true
      # MSA 라우팅 정의
      # ID, 목적지(uri), 조건(Predicate), 필터로 구성된다.
      routes:
        - id: user-service
          # 목적지
          uri: http://localhost:8081
          # 조건부 집합 Header나 Parameter같은 HTTP 요청의 모든 항목 비교
          # 아래와 같은 경우 localhost:8080/user 호출하게 되면 localhost:8081/user로 라우팅 된다.
          predicates:
            - Path=/user/**
          # GatewayFilter 인스턴스, Filter에서는 다운스트림 요청 전후에 요청/응답 가능
          filters:
            - name: UserFilter
              args:
                baseMessage: Taeu UserFilter
                preLogger: true
                postLogger: true
        - id: shop-service
          uri: http://localhost:8082
          predicates:
            - Path=/shop/**
          filters:
            - name: ShopFilter
              args:
                baseMessage: Taeu ShopFilter
                preLogger: true
                postLogger: true

default-filters: 전역 필터를 설정한다.

routes: SCG의 Route 설정을 담당한다. predicates의 조건 구문을 통해 uri로 라우팅하게 된다, 필터들은 직접 구현하거나 Spring이 제공하는 필터를 사용해도 되는데 여기서는 직접 구현하여 추가해보도록 한다.

 

위 설정을 토대로 상황을 시뮬레이션 해보면

http://localhost:8080/user 호출시 path=/user/** 조건에 의해 http://localhost:8081/user로 라우팅 될 것이고, 그 과정에서 GlobalFilter를 거쳐 UserFilter를 거치게 될 것이다.

 

필터의 구현은 GlobalFilter, UserFilter, ShopFilter를 구현할 것인데, 셋의 내용은 동일하므로 여기서는 GlobalFilter만 기술한다.

(참고: https://cloud.spring.io/spring-cloud-gateway/multi/multi__developer_guide.html)

 

GlobalFilter.java - 전역필터

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 GlobalFilter extends AbstractGatewayFilterFactory<Config> {
    public GlobalFilter() {
        super(Config.class);
    }

    @Override
    public GatewayFilter apply(final Config config) {
        return (exchange, chain) -> {
            log.info("GlobalFilter baseMessage: {}", config.getBaseMessage());

            if (config.isPreLogger()) {
                log.info("GlobalFilter Start: {}", exchange.getRequest());
            }

            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                if (config.isPostLogger()) {
                    log.info("GlobalFilter End: {}", exchange.getResponse());
                }
            }));
        };
    }
}

AbstractGatewayFilterFactory: GatewayFilter 구현을 위한 추상클래스이다.

exchange: ServerWebExchange 인스턴스로, HTTP 엑세스를 제공한다. (요청 / 응답 에 대한 접근 등) GatewayFilter (exchange, chain) 를 통하여 사용가능하다. 예제에서는 (exchange, chain) 이후로 request에 대한 접근을 하고 chain.filter(exchange)를 통하여 response를 얻은 후의 접근은 .then(Mono.fromRunnable 이후로 한다.)

 

 

Config.java - Filter에 설정한 인자값을 위한 값 클래스

package kr.taeu.gateway.filters;

import lombok.Getter;

@Getter
public class Config {
    private String baseMessage;
    private boolean preLogger;
    private boolean postLogger;

    public Config(String baseMessage,
                  boolean preLogger,
                  boolean postLogger) {
        this.baseMessage = baseMessage;
        this.preLogger = preLogger;
        this.postLogger = postLogger;
    }
}

위까지 설정을 했으면 gateway가 정상 작동하는지 http://localhost:8080/user를 호출해보자. 아래와 같이 에러가 나면 정상적으로 라우팅은 되는것, 아직 서비스를 구현안했으니 에러가 발생한다.

라우팅은 정성작동

2-2 Services

gateway server에서 설정한대로, user 서비스는 8081, shop 서비스는 8082로 설정한다.

 

user-service의 resources/application.yml

server:
  port: 8081

shop-service의 resources/application.yml

server:
  port: 8082

 

Controller를 각 서비스마다 만들어준다.

 

user-service의 controller/UserController.java

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @GetMapping
    public Mono<String> get(final ServerHttpRequest request,
                            final ServerHttpResponse response) {
        log.info("User MSA Start");
        final HttpHeaders httpHeader = request.getHeaders();
        httpHeader.forEach((key, values) -> log.info("{}: {}", key, values));

        log.info("User MSA End");

        return Mono.just("User MSA Response");
    }
}

shop-service의 controller/ShopController.java

package kr.taeu.shop.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

@Slf4j
@RestController
@RequestMapping("/shop")
public class ShopController {

    @GetMapping
    public Mono<String> get(final ServerHttpRequest request,
                            final ServerHttpResponse response) {
        log.info("Shop MSA Start");
        final HttpHeaders httpHeader = request.getHeaders();
        httpHeader.forEach((key, values) -> log.info("{}: {}", key, values));
        
        log.info("Shop MSA End");

        return Mono.just("Shop MSA Response");
    }
}

 

3. 결과

gateway와 서비스들을 전부 실행 시켜주고, api를 호출해보자

 

http://localhost:8080/user

gateway에서의 필터 로그는 다음과 같이 찍힌다

GlobalFilter -> Service Filter 순으로 실행되는 걸 볼 수 있다.

 

API Gateway는 reverse proxy를 향상시켜 확장한 개념이다. 모든 요청이 gateway를 통하므로, 인증/보안, 모니터링으로의 확장이 용이하다.


Github

https://github.com/lteawoo/spring_cloud_gateway_demo

 

참고

https://medium.com/@niral22/spring-cloud-gateway-tutorial-5311ddd59816

https://cloud.spring.io/spring-cloud-gateway/multi/multi__developer_guide.html