개발/MSA

API Gateway Service

오승미 2023. 7. 21. 15:56

API Gateway Service

ㄴ Proxy 역할

ㄴ 단일 진입로를 통해 일관적으로 작업을 처리하고자 하여 필요성 증가

    --> api gateway 만 상대하면 된다.

1. 인증, 권한 부여

2. 검색 통합

3. 응답캐시

4. 정책, qos 다시 시도, 회로차단기

5. 속도제한

6. 부하분산

7. 로깅, 추적, 상관 관계

8. 헤더, 쿼리 문자열 청구 변환

9. ip 허용 목록에 추가

 

Spring Cloud 에서의 msa 간 통신

ㄴ RestTemplate

RestTemplate resTemplate = new RestTemplate();
restTemplate.getForObject("http://localhost:8080/", User.class, 200);

ㄴ Feign Client: 마이크로 서비스 이름으로 호출

@FeignClient("stores")
publci interface StoreClient{
	@RequestMapping(method= RequestMEthod.GET ,value="/stores")
Lists<Store>getSTroe();

 

Ribbon

ㄴ 스프링클라우드가 Load Balancer로 사용하는 서비스

    --> 비동기화 방식이 안맞아서 잘 사용하지 않음 (client side load balancer)

*Load Balancer: 서버에 가해지는 부하(=로드) 를 분산(=밸런싱) 해주는 장치 또는 기술을 통칭

 

1. client 안에 ribbon이 존재하며 이것이 gateway 역할을 했다.

2. 현재 ribbon은 Spring Boot 2.4에서 Maintenance 상태 (더이상 지원 X)

 

Netflix Zuul

ㄴ api gateway 역할

 

1. 현재 2.4에서 Maintenance 상태

2. client - netflix zuul - service

 

Spring gateway

ㄴ 현재 사용 중

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/** #조건절로 이 주소로 접속하면 8081로 보낸다
        - id: second-service
          uri: http://localhost:8082/
          predicates:
            - Path=/second-service/** #조건절로 이 주소로 접속하면 8082로 보낸다

 

--> 비동기 방식으로 netty 서버가 실행됨

 

first service, second service

@RestController
@RequestMapping("/first-service") -> 게이트웨이 yml파일에 적은 path를 써줘야함
public class Controller {
    @GetMapping("/welcome")
    public String welcome()
    {
        return "Welcome first service";
    }
}
server:
  port: 8081

spring:
  application:
    name: my-first-service

eureka:
  client:
    fetch-registry: false
    register-with-eureka: false
@RestController
@RequestMapping("/second-service")
public class Controller {
    @GetMapping("/welcome")
    public String welcome()
    {
        return "Welcome second service";
    }
}
server:
  port: 8082

spring:
  application:
    name: my-second-service

eureka:
  client:
    fetch-registry: false
    register-with-eureka: false

 

Spring Cloud Gateway + Filter

 

Gateway Handler Mapping(요청 정보 들어옴)+request header -> Predicate(조건분기) -> Pre Filter(작업이 일어나기 전 사전 필터), Post Filter(처리 끝난 후 호출되는 필터) -> Gateway Handler Mapping으로 반환+response header

 

--> property, javacode로 작업 설정 가능

 

Filter using Java Code

ㄴ 람다: 익명 클래스, 인스턴스를 바로 생성하고 소멸 시킬 수 있음

ㄴ 메소드체이닝: 비슷한 메소드를 연속으로 호출

@Configuration
public class FilterConfig {

    //yml 파일에 있는 설정을 자바 코드로 바꿈
    //yml 파일은 주석처리
    @Bean
    public RouteLocator gatewayRoutes(RouteLocatorBuilder builder){
        return builder.routes() //라우터 등록
                .route(r -> r.path("/first-service/**") //r이라는 값이 전달되면 path를 확인하고
												
												//filter를 적용시킴
                        .filters(f -> f.addRequestHeader("first-request","first-request-header") //request 헤더가 삽입됨
                                .addResponseHeader("first-response","first-response-header")) //response헤더가 삽입됨

                        .uri("http://localhost:8081")) //이 uri로 전달된다
                .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 service, second service

(http://localhost:8000/first-service/message)

@GetMapping("/message")
    public String message(@RequestHeader("first-request") String header){
        log.info(header);
        return "Hello first service";
    }

 

(http://localhost:8000/second-service/message)

@GetMapping("/message")
    public String message(@RequestHeader("second-request") String header){
        log.info(header);
        return "Hello second service";
    }

 

Filter using Property

ㄴ yml 파일로 설정

ㄴ config 파일의 @Configuration, @Bean 주석 처리

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/**
          filters: <-추가된 부분
            - AddRequestHeader=first-request, first-reqeust-header2
            - AddResponseHeader=first-request, first-response-header2
        - id: second-service
          uri: http://localhost:8082/
          predicates:
            - Path=/second-service/**
          filters:<-추가된 부분
            - AddRequestHeader=second-request, second-reqeust-header2
            - AddResponseHeader=second-request, second-response-header2

 

Custom Filter

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;

//reactive -> webflux를 사용하는 클래스에 포함됨
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> { // AbstractGatewayFilterFactory<데이터타입>
                                                                                        //여기서 데이터 타입은 우리가 작성한 CustomFilter이고
    public CustomFilter(){ //디폴트 생성자에, Config 상위 클래스만 호출한다.
        super(Config.class);
    }

    @Override //우리가 구현해야할 Config를 오버라이드 한다.
    public GatewayFilter apply(Config config) {
        return (exchange, chain)-> { //람다에서 여기부터는 GatewayFilter의 반환값을 작성한다
            ServerHttpRequest request = exchange.getRequest(); //비동기화된 webflux을 사용할 경우 동기화된 톰캣과 다르게
            ServerHttpResponse response = exchange.getResponse(); //serveletrequest가 아닌 severrequest를 받아야 한다

            //pre필터 적용
            log.info("Custom PRE filter: request id-> {}", request.getId());

            //post 필터 적용
            return chain.filter(exchange).then(Mono.fromRunnable(()-> { //Mono는 webflux처럼 비동기 방식에서 단일값을 전달한다고 지정해주기 위해 작성하는 것
                log.info("Custom POST filter: response code -> {}", response.getStatusCode()); //단일값 하나 전달
            }));
        };
    }

    public static class Config {
        //
    }
}
spring:
  application:
    name: apigateway-service

  cloud:
    gateway:
      routes:
        - id: first-service
          uri: http://localhost:8081/
          predicates:
            - Path=/first-service/**
          filters:
#            - AddRequestHeader=first-request, first-reqeust-header2
#            - AddResponseHeader=first-request, first-response-header2
             - CustomFilter #우리가 작성한 필터를 여기에 등록
        - id: second-service
          uri: http://localhost:8082/
          predicates:
            - Path=/second-service/**
          filters:
#            - AddRequestHeader=second-request, second-reqeust-header2
#            - AddResponseHeader=second-request, second-response-header2
             - CustomFilter #우리가 작성한 필터를 여기에 등록

first service, second service

@GetMapping("/check")
    public String check(){
        return "HI This is first customservice";
    }
@GetMapping("/check")
    public String check(){
        return "HI This is second customservice";
    }

 

Custom Filter(GLOBAL Filter)

ㄴ 커스텀 필터는 개별적 라우팅 정보마다 다 등록 해놓아야 한다.(id 별 등록)

ㄴ 공통적인 부분은 글로벌 필터로 적용 시키면 편하다.

ㄴ preLogger 처럼 bool 타입일 경우 isLogger() 처럼 is를 앞에 붙여서 메소드 호출

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:
      default-filters:
        - name: GlobalFilter #우리가 작성한 클래스 이름
          args:
            baseMessage: Spring Cloud Gateway Global Filter #우리가 클래스 파일에 적은 baseMessaege파라미터값에 들어갈 말을 적음
            preLogger: true
            postLogger: true

      routes:
        - id: first-service
          uri: http://localhost:8081/
          predicates:
            - Path=/first-service/**
          filters:
#            - AddRequestHeader=first-request, first-reqeust-header2
#            - AddResponseHeader=first-request, first-response-header2
             - CustomFilter #우리가 작성한 필터를 여기에 등록
        - id: second-service
          uri: http://localhost:8082/
          predicates:
            - Path=/second-service/**
          filters:
#            - AddRequestHeader=second-request, second-reqeust-header2
#            - AddResponseHeader=second-request, second-response-header2
             - CustomFilter #우리가 작성한 필터를 여기에 등록
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;

//reactive -> webflux를 사용하는 클래스에 포함됨
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();

            //pre필터 적용
            log.info("Custom PRE filter baseMessage: request id-> {}", config.getBaseMessage());

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

            //post 필터 적용
            return chain.filter(exchange).then(Mono.fromRunnable(()-> {
                if (config.isPostLogger()){
                    log.info("Global Filter start: request id -> {}", response.getStatusCode());
                }
            }));
        };
    }

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

 

1. 시작

글로벌 필터 적용 -> 커스텀 필터 적용

2. 종료

커스텀 필터 적용 -> 글로벌 필터 적용

 

Custom Filter(Logging Filter)

1. 요청

gatway client → gateway handler → globla filter → custom filter → logging filter → proixed service(우리가 구현한 firts, second service)

2. 응답

요청의 반대 순서

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;

//reactive -> webflux를 사용하는 클래스에 포함됨
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);
    }

    //구현시켜야할 객체 : apply
    //반환타입 : gatewayFilter
    @Override
    public GatewayFilter apply(Config config) {
        GatewayFilter filter = new OrderedGatewayFilter((exchange, chain)->{
            ServerHttpRequest request = exchange.getRequest();
            ServerHttpResponse response = exchange.getResponse();

            //pre필터 적용
            log.info("Logging filter baseMessage: request id-> {}", config.getBaseMessage());

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

            //post 필터 적용
            return chain.filter(exchange).then(Mono.fromRunnable(()-> {
                if (config.isPostLogger()){
                    log.info("Logging POST Filter start: response code -> {}", response.getStatusCode());
                }
            }));
        }, Ordered.HIGHEST_PRECEDENCE);//gatewayFilter의 두번재 인자값은 필터의 우선순위를 지정하는 인자값을 넣는다.
        return filter;
    }

    //configuration정보는 내가 자유롭게 지정할 수 있다.
    //내가 여기서 메소드를 지정하면 필터에서 해당 메소드를 지정할 수 있다. 
    @Data
    public static class Config {
        private String baseMessage;
        private boolean preLogger;
        private boolean postLogger;
       //private String sayHello;
    }
}

특히 gatewayfilter는 객체 생성시
oredredGAtewayFilter를 사용해야 한다. 
이건 gatewayfilter를 구현하는 자식 클래스 역할을 한다. 

public class OrderedGatewayFilter implements GatewayFilter, Ordered {
    private final GatewayFilter delegate;
    private final int order;

//우리가 구현함
    public OrderedGatewayFilter(GatewayFilter delegate, int order) {
        this.delegate = delegate;
        this.order = order;
    }

		public GatewayFilter getDelegate() {
        return this.delegate;
    }

	//필터가 해야할 역할을 재정의 할 수 있다. 
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        return this.delegate.filter(exchange, chain);
    }

    public int getOrder() {
        return this.order;
    }

    public String toString() {
        return "[" + this.delegate + ", order = " + this.order + "]";
    }
}

--> Webflux에서는 serveltrequest, response 지원 X

--> 여기선 ServerWebExchange 사용(serverRequest, serverResponse)

 

GatewayFilterChain

ㄴ pre, post 체인 필터 연결

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:
      default-filters:
        - name: GlobalFilter #우리가 작성한 클래스 이름
          args:
            baseMessage: Spring Cloud Gateway Global Filter #우리가 클래스 파일에 적은 baseMessaege파라미터값에 들어갈 말을 적음
            preLogger: true
            postLogger: true

      routes:
        - id: first-service
          uri: http://localhost:8081/
          predicates:
            - Path=/first-service/**
          filters:
             - CustomFilter #우리가 작성한 필터를 여기에 등록
             # 비교를 위해 pre필터는 Logging 필터를 넣지 않는다
        - id: second-service
          uri: http://localhost:8082/
          predicates:
            - Path=/second-service/**
          filters:
             - name: CustomFilter
             - name: LoggingFilter
               args:
                 baseMessage: Logging, filter.
                 preLogger: true
                 postLogger: true

--> 실행하면 기대와 다르게 logging filter 가 가장 먼저 나온다.

--> orderedGateWayFilter 설정 시 LoggingFilter을 Ordered.HIGHEST_PRECDENCE로 설정했기 때문

--> Ordered.LOWEST_PRECEDENCE로 설정하면 우리가 기대했던 순서로 작동

/post 필터 적용
            return chain.filter(exchange).then(Mono.fromRunnable(()-> {
                if (config.isPostLogger()){
                    log.info("Logging POST Filter start: response code -> {}", response.getStatusCode());
                }
            }));
        }, Ordered.HIGHEST_PRECEDENCE);//gatewayFilter의 두번재 인자값은 필터의 우선순위를 지정하는 인자값을 넣는다.
        return filter;

 

Load Balancer(Eureka 연동)

클라이언트 호출 → api gateway → eureka server → 서버 확인 → api gateway → 해당서버로 이동

 

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
          args:
            baseMessage: Spring Cloud Gateway Global Filter 
            preLogger: true
            postLogger: true

      routes:
        - id: first-service
          uri: lb://MY-FIRST-SERVICE //lb//등록한 네임으로 uri 지정
          predicates:
            - Path=/first-service/**
          filters:
#            - AddRequestHeader=first-request, first-reqeust-header2
#            - AddResponseHeader=first-request, first-response-header2
             - CustomFilter #우리가 작성한 필터를 여기에 등록
       
        - id: second-service
          uri: lb://MY-SECOND-SERVICE
          predicates:
            - Path=/second-service/**
          filters:
#            - AddRequestHeader=second-request, second-reqeust-header2
#            - AddResponseHeader=second-request, second-response-header2
             - name: CustomFilter
             - name: LoggingFilter
               args:
                 baseMessage: Logging, filter.
                 preLogger: true
                 postLogger: true

first-service.yml, second-service.yml

server:
  port: 8081

spring:
  application:
    name: my-first-service

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

spring:
  application:
    name: my-second-service

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

총 4개의 서버 실행

ㄴ eureka 서버(discoveryService)

ㄴ api gateway

ㄴ first, second service

 

--> 서비스를 2개씩 띄워 총 4개가 실행되고 postman으로 확인해 보면 어느 서비스로 가는지 알 수 없다.

--> Welcome first 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;

@RestController
@RequestMapping("/first-service")
@Slf4j
public class Controller {
    //변수에 바로 Autowired를 쓰지말고
    //이건 yml 파일의 값을 갖고오는 방법 중 하나이다. 
    Environment env;

    //생성자를 통해 주입 후 @Autowired를 통해 빈 등록
    @Autowired
    public Controller(Environment env){
        this.env = env;
    }

    @GetMapping("/welcome")
    public String welcome()
    {
        return "Welcome first service";
    }

    @GetMapping("/message")
    public String message(@RequestHeader("first-request") String header){
        log.info(header);
        return "Hello first service";
    }

    @GetMapping("/check")
    public String check(HttpServletRequest request){
        log.info("Server port= {}", request.getServerPort());
        return String.format("This message indicate First service PORT %S",
                env.getProperty("local.server.port"));//getProperty()안에는
        //우리가 갖고오고 싶은 값을 명시해주면 된다. 우리는 포트 번호를 갖고오고 싶음 
    }
}

--> 라운드 로빈 방식으로 호출

프로세스들 사이에 우선순위를 두지 않고, 순서대로 시간단위로 CPU를 할당하는 방식의 CPU 스케줄링 알고리즘