[SpringCloud MSA]API Gateway Service(Spring Cloud Gateway)

zzarbttoo·2021년 9월 6일
0

이 글은 인프런 강의 "Spring Cloud로 개발하는 마이크로서비스 애플리케이션(MSA)"를 정리한 글입니다. 문제/오류가 있을 시 댓글로 알려주시면 감사드리겠습니다


Spring Cloud Gateway 방식을 사용하면 Zuul과 다르게 비동기 처리가 가능해진다
Spring Cloud Gateway는 다음과 같이 동작한다

Client <-> Spring  Cloud Gateway <-> Services

Spring Cloud Gateway 내부는 다음과 같이 동작한다

Gateway Handler Mapping : client로부터 gateway에 어떤 요청이 들어왔는지 파악 
-> Predicate : 그 요청의 사전 조건(어떤 이름)으로 요청 됐는지 분기 
-> PreFilter(사전 필터, Global -> Custom -> Logging) 로 요청 정보 구성 
-> First Service, Second Service 등 서블릿으로 요청(Proxied Service)
->PostFilter(사후 필터, Logging -> Custom -> Global) 로 반환 정보 구성 
-> 원래의 client에 전달 (Gateway Client)

Spring Cloud Gateway 프로젝트 + Filter 설정(application.yml에 설정)

| Apigateway-service

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.7</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>apigateway-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>apigateway-service</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>11</java.version>
        <spring-cloud.version>2020.0.3</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</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>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
  • springboot 2.4 이상을 사용해야한다
  • spring-cloud-starter-gateway를 추가한다(gateway 역할)
  • 코드량을 줄이기 위해 lombok 추가

application.yml
server:
  port: 8000

spring:
  application:
    name: apigateway-service
  cloud:
    gateway:
      routes:
        - id: first-service
          uri : http://localhost:8081/
          predicates:
            - Path=/first-service/**
          filters:
            - AddRequestHeader=first-request, first-requests-header1
            - AddResponseHeader=first-response, first-response-header1
        - id: second-service
          uri : http://localhost:8082/
          predicates:
            - Path=/second-service/**
          filters:
            - AddRequestHeader=second-request, second-requests-header2
            - AddResponseHeader=second-response, second-response-header2
  • port 8000번에서 실행
  • cloud.gateway.routes를 이용해 라우팅 설정
  • predicates로 분기 조건문 설정
  • request를 보낼 uri 값, port 번호를 정확하게 적어준다
  • filters 설정을 해 RequestHeader과 ResponseHeader 를 추가해줬다(headerName:headerValue의 key:value형태)

| Service 1, 2

application.yml
server:
  port: 8081 #service 2의 경우 8082로 설정 

spring:
  application:
    name : my-first-service #service 2의 경우 my-second-service로 설정 

Controller
package com.example.firstservice;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

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

    //header(first-request)를 받아서 header에 저장
    @GetMapping("/message")
    public String message(@RequestHeader("first-request") String header){
        log.info(header);
        return "Hello world in First Service";
    }
}

  • Apigateway에서 요청을 할 때 http://uri주소:port/first-service/이후url 형식으로 요청하게 된다
  • 때문에 @RequestMapping("/first-service")를 하여 기본 진입점을 /first-service로 만들었다
  • @RequestHeader로 받은 header을 출력하도록 했다(header key : first-request)

| 실행

ApiGateway-Service

  • 실행 시 console에 Netty 라고 되어있는 것을 확인할 수 있으며(이전에는 내장 서버인 tomcat을 사용했다) 비동기 방식으로 실행이 된다
Service 1, 2

  • service 1, service 2에 postman으로 요청을 보냈다
  • request header을 출력해보니 return 값과 Header의 Response value가 잘 출력되는 것을 확인할 수 있었다

Filter 설정(Java)

위에서는 application.yml 파일을 이용해 filter 설정을 했는데 java 코드를 이용한 설정도 가능하다
Apigateway-service만 수정하면 된다

application.yml
server:
  port: 8000
  • 포트 설정만 진행

FilterConfig.java
package com.example.apigatewayservice.config;

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 객체에 path 정보 추가
                .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();

    }

}
  • @Configuration 을 이용해 설정 진행
  • RouteLocator Bean 생성
  • 람다함수 이용
  • 필터체이닝을 이용해 여러개의 필터를 한번에 등록할 수 있다
  • route와 header을 설정해주었다
  • Spring Cloud Gateway filter은 Gateway Handler Mapping 시 requestHeader을 추가해주고
    전체 과정이 끝난 후 Gateway Handler Mapping에서 client로 response 할 때 responseHeader을 추가해줌

이후 실행을 하면 yml로 설정했을 때와 동일한 것을 확인할 수 있었다


CustomFilter, GlobalFilter, LoggingFilter

이전 경우는 제공해주는 필터를 적용한 것이었고, 지금부터는 CustomFilter, GlobalFilter, LoggingFilter 을 이용할 것이다
이를 위해서 이전에 설정한 filter 설정은 모두 지워줘야한다

Filter 순서(보편적, 요청 후 돌아오는 형태이기 때문에 사전필터 순서<-> 사후필터 순서)
PreFilter(사전 필터, Global -> Custom -> Logging -> Proxied Service -> PostFilter(사후 필터, Logging -> Custom -> Global) 

| Apigateway-Service

CustomFilter
package com.example.apigatewayservice.filter;

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

@Slf4j
@Component
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(chain)
            //비동기 방식에서 사용 -> Mono (단일값)
            return chain.filter(exchange).then(Mono.fromRunnable(() ->
            {
                log.info("Custom Post filter : response code -> {}", response.getStatusCode());
            }));
        };
    }

    public static class Config{
        //Put the configuration properties

    }
}
  • request id, response status를 출력
  • AbstractGatewayFilterFactory<Config.class> 상속
  • 부모 class의 inner class인 Config 그대로 상속
  • GatewayFilter을 return 하게 된다
  • webflux 환경에서는 HttpServletRequest, HttpServletexchange를 지원하지 않고 ServerRequest, ServerResponse를 사용해야한다
    exchange는 ServerRequest, ServerResponse를 사용할 수 있도록 도와준다
  • Mono는 비동기 방식에서 단일값일 때 사용, response 할 때 response status code를 출력

GlobalFilter

Custom Filter이 router 하나하나에 직접 적용해야 했던 것이라면 Global Filter은 전체 router에 적용
Global Filter은 가장 먼저 실행되고 가장 마지막에 종료된다

package com.example.apigatewayservice.filter;

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;

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

    public GlobalFilter(){
        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("Global Filter baseMessage: -> {}", config.getBaseMessage());


            if(config.isPreLogger()){
                log.info("Global Filter Start: request id -> {}", request.getId());
            }
            //custom Post Filter(chain)
            //비동기 방식에서 사용 -> Mono (단일값)
            return chain.filter(exchange).then(Mono.fromRunnable(() ->
            {

                if(config.isPostLogger()){
                    log.info("Global Filter end: response code -> {}", response.getStatusCode());
                }
            }));
        };
    }

    @Data //getter setter
    public static class Config{
        //Put the configuration properties
        private String baseMessage;
        private boolean preLogger;  // boolean은 is~ 로 자동 생성
        private boolean postLogger;



    }
}
  • inner Class인 Config class를 만들었고, @Data를 이용해 getter setter 설정
  • 출력할 메시지 값, preLogger 여부, postLogger 여부를 입력받도록 함(application.yml)
  • preLogger, postLogger은 boolean 값이므로 getter가 is~로 시작함(Spring 특성상 args -> Config 인자로 자동 매핑됨)
  • preLogger일 경우 인자로 받은 baseMessage 출력
  • postLogger일 경우 결과 값의 status 코드를 출력

LoggingFilter
package com.example.apigatewayservice.filter;

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;

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

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

    @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(chain)
            //비동기 방식에서 사용 -> Mono (단일값)
            return chain.filter(exchange).then(Mono.fromRunnable(() ->
            {

                if(config.isPostLogger()){
                    log.info("Logging Post Filter: response code -> {}", response.getStatusCode());
                }
            }));

        }, Ordered.LOWEST_PRECEDENCE); //filter의 order 지정
        //우선 순위를 가장 높게 잡았기 때문에 가장 먼저 실행 (Highest_Precedence)



        return filter;
    }

    @Data //getter setter
    public static class Config{
        //Put the configuration properties
        private String baseMessage;
        private boolean preLogger;  // boolean은 is~ 로 자동 생성
        private boolean postLogger;

    }
}
  • lambda로 실행한 것은 사실 위의 코드를 실행한 것과 같다
  • OrderedGatewayFilter로 생성했으므로 filter의 순서를 정할 수 있다(위의 경우에는 Ordered.LOWEST_PRECEDENCE로 가장 나중에 실행)

application.yml
server:
  port: 8000

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:
            - 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
  • args 로 Config 정보를 전달했다
  • second-service의 경우 GlobalFilter, CusomFilter, LoggingFilter을 모두 적용시켰다
  • filter을 여러개 사용할 때 추가적인 parameter을 사용하기 위해서는 name: Filter이름 형식으로 써야한다

| 실행

service 2로의 실행을 했으며, 앞서 작성한 controller에 check라는 함수를 추가했고, check로 요청을 보냈다

@Slf4j
@RestController
@RequestMapping("/second-service")
public class SecondServiceController {

 
    @GetMapping("/check")
    public String check(){
        return "Hi, there This is a message from Second Service";
    }
}
ApiGateway-service 콘솔창

  • Global -> Custom -> Logging -> Logging -> Custom -> Global의 순서로 요청이 된 것을 확인할 수 있었다
  • 우선순위를 바꾼다면 다른 결과가 나올 수 있다

Spring Cloud Gateway - Eureka 연동

| ApiGateway-service

pom.xml
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
  • eureka client 추가

application.yml
server:
  port: 8000

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


spring:
  application:
    name: apigateway-service
  cloud:
    gateway:
      default-filters:
        - name : GlobalFilter # Component 이름
          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:
            - CustomFilter
        - id: second-service
          uri: lb://MY-SECOND-SERVICE
          predicates:
            - Path=/second-service/**
          filters:
            - name: CustomFilter
            - name: LoggingFilter
              args:
                baseMessage: Hi, there
                preLogger: true
                postLogger: true
  • eureka client 관련 값들 설정
  • uri 값을 lb://서비스 이름(대문자) 로 설정 -> discovery service에 등록된 이름으로 포워딩 시켜준다

| Service 1, 2

pom.xml

ApiGateway-Service와 마찬가지로 eureka-client 설정

application.yml
server:
  port: 0

spring:
  application:
    name : my-first-service
eureka:
  client:
    fetch-registry: true
    register-with-eureka: true
    service-url:
      defaultZone: http://localhost:8761/eureka
  instance:
    instance-id: ${spring.application.name}:${spring.application.instance_id:${random.value}} #random port 때문에 instance 로 구분


  • eureka client 설정
  • random port로 설정하기 위해 port : 0으로 설정
  • random port이기 때문에 instance-id 를 설정해줌

| 실행

  • eureka server에 등록된 서비스들을 볼 수 있다
  • 서비스명으로 요청을 할 수 있다
  • 하나의 서비스를 여러개 실행할 수 있으며, 요청이 라운드앤 로빈 방식으로 할당이 된다
profile
나는야 누워있는 개발머신

0개의 댓글