FrameWork/Spring Cloud

API Gateway Service

태윤2 2021. 8. 25. 20:56

API Gateway Service

  • 서비스로 전달되는 모든 API요청의 관문(Gateway) 역할을 하는 서버
  • 시스템의 아키텍처를 내부로 숨기고 외부의 요청에 대한 응답만을 적절한 형태로 응답
  • 클라이언트는 서비스를 호출하는 게 아닌 API Gateway에 서로 약속한 형태의 요청만 보내면 됨\

 

장점

  • 인증 및 권한 부여 단일 작업
  • 서비스(마이크로 서비스) 검색 통합
  • 응답 캐싱
  • 정책, 회로 차단 및 Qos 다시 시도
  • 속도 제한
  • 부하 분산(로드 밸런싱)
  • 로깅, 추적, 상관관계
더보기

클라이언트의 요청을 일괄적으로 처리

전체 시스템의 부하를 분산시키는 로드 밸런서의 역할

동일한 요청에 대한 불필요한 반복 작업을 줄일 수 있는 캐싱

시스템상을 오고 가는 요청과 응답에 대한 모니터링

시스템 내부에 아키텍처를 숨길 수 있음

Spring Cloud에서의 MSA 간 통신

  • RestTemplate 
RestTemplate restTemplate = new RestTemplate();
restTemplate.getForObject("http://localhost:8080/", User.calss, 200);
  • Feign Client
@FeignClient("stores")
public interface StoreClient {
	@RequestMapping(method = RequestMethod.GET, value="/store")
	List<Store> getStores();
}

Ribbon: Client sid Load Balancer

비동기화 처리가 잘되지 않아 잘 사용하지 않음

Spring Cloud Ribbon, Zuul 은 Spring Boot 2.4에서 maintenance 상태(유지하고 기능을 추가하지 않는 상태)

  • 서비스 이름으로 호출
  • Health Check

 

구성

  • First Service
  • Second Service
  • Netflix Zuul
    • Routing
    • Gate way

 

First, Second Service

package com.example.firstservice;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/")
public class FirstServiceController {

    @GetMapping("/welcome")
    public String welcome() {
        return "Welcome to the First service.";
    }
}
server:
  port: 8081

spring:
  application:
    name: my-first-service

eureka:
  client:
    register-with-eureka: false
    fetch-registry: false
  • Second 서비스는 First->Second, 8081 -> 8082로 바꿔서 만들어줌
  • 의존성은 lombok, Spring Web, Eureka Discovery Client

ZuulService

Zuul로 구현된 게 많아 Zuul사용법을 익히고 Gateway를 공부하는 게 도움된다는 글이 많아 Spring boot 변경을 통해 Zuul 사용법을 먼저 익혀보기로 함!

현재(2021-08-25) 기준 IntelliJ나 SpringInitializr에서 2.4.x 이후 버전만 빌드되기 때문에 직접 build.gradle을 수정함

plugins {
	id 'org.springframework.boot' version '2.3.10.RELEASE' ## 수정 1번 
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

ext {
	set('springCloudVersion', "Hoxton.SR10") ## 수정 2번 (2020.0.0 이하 버전을 사용)
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web:2.3.10.RELEASE' # 수정 3번
	implementation 'org.springframework.cloud:spring-cloud-starter-zuul:1.4.7.RELEASE'# 수정 4번
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'io.projectreactor:reactor-test'
}

dependencyManagement {
	imports {
		mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
	}
}

test {
	useJUnitPlatform()
}

버전에 관한 부분은 https://spring.io/projects/spring-cloud 에서 확인가능

  • 의존성은 lombok, Spring Web, Eureka Discovery Client

@EnableZuulProxy 추가

package com.example.zuulservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.core.SpringVersion;

@SpringBootApplication
@EnableZuulProxy
public class ZuulServiceApplication {

	public static void main(String[] args) {
		SpringApplication.run(ZuulServiceApplication.class, args);
	}

}

application.yml 설정

server:
  port: 8000
spring:
  application:
    name: my-zuul-service

# 라우팅 등록
zuul:
  routes:
    first-service:
      path: /first-service/** # 8000/first-service 로 요청되면 8081 포트로
      url: http://localhost:8081
    second-service:
      path: /second-service/** # 8000/second-service 로 요청되면 8082 포트로
      url: http://localhost:8082

ZuulFilter

  • 사전, 사후 처리를 해주는 역할(사전 필터, 사후 필터)
package com.example.zuulservice.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

@Slf4j // lombok
@Component // Component 등록 (용도가 정확하지않을 때 사용 일반적인 Bean 으로 사용하겠다)
public class ZuulLoggingFilter extends ZuulFilter {


    @Override
    public Object run() throws ZuulException {
        // 로거 목적에 따라 info,debug,error,warning 등등 쓰면됨
        log.info("************************** printing logs: ");

        RequestContext ctx = RequestContext.getCurrentContext();

        HttpServletRequest request = ctx.getRequest();

        log.info("************************** " + request.getRequestURI());
        return null;
    }

    //필터의 타입
    @Override
    public String filterType() {
        return "pre";
    }

    // 여러개가 있을때 순서
    @Override
    public int filterOrder() {
        return 1;
    }

    // 필터를 쓰겠다 쓰지않겠다
    @Override
    public boolean shouldFilter() {
        return true;
    }


}

Spring Cloud Gateway 사용

  • 의존성은 Spring Boot DevTools, Eureka Discovery Client, Gateway
  • 비동기 처리가 가능(Zuul도 비동기 처리가 추가되었지만 호환성 문제 때문에 Gateway를 현재는 사용함)

 

application.yml 설정

server:
  port: 8000

eureka:
  client:
    register-with-eureka: false
    fetch-registry: false
    service-url:
      defaultZone: http://localhost:8761/eureka
spring:
  application:
    name: apigateway-service
  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/**
  • 톰캣이 아닌 비동기 Netty 서버 기동

  • gateway는 아래 uri 와같은 방식으로 요청을 하게 됨
  • zuul은 8000/first-service 로 요청하면 8081 포트로 변경돼서 요청하게 됨

gateway는 위와같은 형식으로 요청을 하게됨

  • first, second service contoller 변경

 

Spring Cloud Gateway - Filter

  • Gateway가 요청정보를 받음
  • 요청 정보의 사전 조건 분기해주는 Predicate 영역이 있음
  • Pre Filter와 Post Filter로 처리함
  • Pre Filter의 사전작업을 하고 서비스에 요청 및 응답 이후 Post Filter의 사후 작업을 하고 Client에 응답해줌

 

Filter using Java Code - FilterConfig.java

  • application.yml에 등록한 정보를 java 코드로 구현
  • RequestHeader를 추가해서 요청을 보낼 수 있음
  • ResponseHeader를 추가해서 응답을 보낼 수 있음

 

FilterConfig - GatewayService

package com.example.gatewayservice.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(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();
    }
}

 

  • first, second controller
package com.example.firstservice;

import lombok.extern.slf4j.Slf4j;
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;

// http://localhost:8081/welcome
// http://localhost:8081/first-service/welcome
@Slf4j
@RestController
@RequestMapping("/first-service/")
public class FirstServiceController {

    @GetMapping("/welcome")
    public String welcome() {
        return "Welcome to the First service.";
    }

    @GetMapping("/message")
    public String message(@RequestHeader("first-request") String header) {
        log.info(">>>>>>>>>>>>>>>>>>>>"+header);
        return "Hello World in First Service.";
    }
}
package com.example.secondservice;

import lombok.extern.slf4j.Slf4j;
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;

// http://localhost:8082/welcome
// http://localhost:8082/second-service/welcome
@Slf4j
@RestController
@RequestMapping("/second-service/")
public class SecondServiceController {

    @GetMapping("/welcome")
    public String welcome() {
        return "Welcome to the Second service.";
    }

    @GetMapping("/message")
    public String message(@RequestHeader("second-request") String header) {
        log.info(">>>>>>>>>>>>>>>>>>>>"+header);
        return "Hello World in First Service.";
    }
}

헤더 확인

 

application.yml에 Filter 추가하기

 

Spring Cloud Gateway - CustomFilter

  • AbstractGatewayFilterFactory<CustomFilter.Config> 상속받기
  • apply 오버라이딩하기

CustomFilter

package com.example.gatewayservice.filter;

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);
    }

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

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

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

            // Custom Post Filter
            return chain.filter(exchange)
                    // Mono-> Spring5 에서 추가된 비동기방식에서 단일값 전달할 때 사용
                    .then(Mono.fromRunnable(() -> log.info("Custom POST filter: response code -> {}", response.getStatusCode())));
        });
    }
}

application.yml

customFilter 추가

 

first, second service Controller 추가

 

 

result

 

Spring Cloud Gateway - Global Filter

GlobalFilter

package com.example.gatewayservice.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;

@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)
                    // Mono-> Spring5 에서 추가된 비동기방식에서 단일값 전달할 때 사용
                    .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

 

result

 

Spring Cloud Gateway - Custom Filter (Logging)

LoggingFilter

package com.example.gatewayservice.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;

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

    @Override
    public GatewayFilter apply(Config config) {

        return 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.HIGHEST_PRECEDENCE);
    }

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

application.yml

  • second filter에만 추가

result

 

  • first-service

first-service

  • second-service

second-service

  • Global start -> Custom start -> Logging Start, End -> Custom end -> Global end 여야함

우선순위를 가장 높은 값을 줘서 Logging이 먼저 실행됨

 

우선순위 가장낮은값

우선순위를 조절해 순서를 커스텀 할 수 있다

 

  • reference

SpringCloud로 개발하는 마이크로어플리케이션