API Gateway Service

coding_ticket·2023년 11월 28일
0

msa

목록 보기
2/2

API Gateway Service

마이크로 서비스로 애플리케이션을 분리하면 서버에 대한 연결을 각각하게되면 클라이언트 입장에서는 굉장히 불편한 상황에 놓이게 된다. 따라서 나눠져 있는 애플리케이션을 하나의 어떤 장치를 통해 접근하고 싶은데, 이 장치를 API-GATEWAY가 담당하게 된다.

즉, Api Gateway서비스는 사용자가 설정한 라우팅 설정에 따라서 각각 엔드포인트로 클라이어트를 대신해서 요청하고, 응답을 받으면 다시 클라이언트에게 전달해주는 프록시 역할을 담당한다.
이런 구조를 통해 시스템의 내부는 숨기고 외부의 요청에 대해 적절한 응답을 해준다는 장점이 있다.

Spring Cloud Gateway

프로젝트 생성

dependency

  • implementation'org.springframework.cloud:spring-cloud-starter-gateway'
  • implementation'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
  • developmentOnly 'org.springframework.boot:spring-boot-devtools'

application.yml설정

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

spring:
  application:
    name: gateway-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/**

여러 Gateway service가 존재하지만, spring cloud gateway의 가장 큰 특징은 비동기방식인 점이다. 따라서 실행하였을때 다른 서비스들은 톰캣을 실행하는것에 반대로, 비동기 방식의 netty서버가 실행된것을 볼수 있다.

프로젝트 생성

dependency

  • implementation'org.springframework.cloud:spring-cloud-starter-gateway'
  • implementation'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
  • developmentOnly 'org.springframework.boot:spring-boot-devtools'

api gateway를 실험하기 위해 first-service와 second-service의 api를 만들어보자

First-Service

@RestController
@RequestMapping()
public class FirstServiceController {
    @GetMapping("/welcome")
    public String welcome(){
        return "Welcome to the Second Service";
    }
}

Second-Service

@RestController
@RequestMapping()
public class SecondServiceController {
    @GetMapping("/welcome")
    public String welcome(){
        return "Welcome to the First Service";
    }
}

application.yml(api gateway)

spring:
  application:
    name: apipgateway-service
  
  cloud:
    gateway:
      #라우트(연결될 서비스) 객체정보 등록
      routes: 
      	  
          #고유id
        - id: first-service 
          
          #어디로 포워딩될것인지 주소
          uri: http://localhost:8081/ 
          predicates:
             #조건절이라고 생각하면 됨, 사용자의 path정보가 설정과 같다면 uri쪽으로 이동하겠다는 의미
          - Path=/first-service/** 
          
        - id: second-service
          uri: http://localhost:8082/
          predicates:
            - Path=/second-service/**

하지만 위와같이 설정하면 first,second-service로 api전송이 되지 않는것을 확인할수 있다. 왜냐하면 스프링 클라우드의 설정에서 uri에 path가 그대로 붙기 때문이다(localhost:8081/first-service/welcome). 때문에 first,second-service의 컨트롤러에 path를 추가해줘야한다.

first-service

@RestController
@RequestMapping("/first-service")
public class FirstServiceController {
    @GetMapping("/welcome")
    public String welcome(){
        return "Welcome to the Second Service";
    }
}

second-service

@RestController
@RequestMapping("/second-service")
public class SecondServiceController {
    @GetMapping("/welcome")
    public String welcome(){
        return "Welcome to the First Service";
    }
}

Spring Cloud Gateway -Filter

클라이언트가 first,second-service를 이용하고싶은데 이때 spring cloud gateway가 중간에서 로드밸런싱을 처리해준다.

이때 spring cloud gateway의 안에서는 다음과 같이 동작하게 된다.

1. Gateway Handler Mapping : 어떤 동작의 요청인지를 판단하고, 각 서비스로 들어가기전에 공통적으로 처리해줄 사항들(헤더 추가 등등) 처리가능
2. Predicate : 어느 마이크로서비스로 메시징을 넘겨줘야하는지 판단
3. Pre Filter : 어떤 요청에 대한 사전처리
4. Post Filter : 어떤 응답에 대한 사후처리

Filter는 yml파일 설정으로 또는 자바 코드로 처리할수가 있다.

자바로 처리해줄때
yml파일로 설정하는것이 아닌 Config클래스를 만들고, RouteLocator를 빈으로 설정하여 같은 역할의 자바 코드 작성 가능. 이전에 만든 라우터의 요청에 (first,second-service)에 따라 필터링을 적용할수 있게 된다.

이때 addRequestHeader, addResponseHeader메서드가 Gateway Handler Mapping역할에서 게이트웨이가 로드밸런싱을 하면서 추가적으로 값을 넣어 보내줄때 사용되는 값이다.

실제로 first,seconde-service에 api를 추가하여 header정보를 확인해보겠다.

first-service

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

second-service

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


잘 헤더가 들어온것을 볼수 있다. 다음과 같이 헤더안에 값을 넣어 요청과 응답을 줄수 있는것을 알수있다.

자바코드가 아닌 yml파일로 설정하여도 같은 결과를 얻을수 있다.

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

Custom Filter적용

사용자 정의 필터이며, 자유롭게 로그 출력, 인증 등등을 커스텀 필터로 정의할수 있다.

컴포턴트로 등록(어노테이션)하고, 커스텀 필터는 반드시 AbstractGatewayFilterFactory를 상속받아서 apply메서드를 정의하여 작성하면 된다. 작동하고자 하는 내용을 GatewayFilter객체로 반환하여 행동을 정의해주면 된다.

(exchange,chain)을 매개변수로 람다를 이용하여 GatewayFilter를 반환하는데, 이때 ServletRequest,Response가 아닌 ServerHttpRequest를 이용한다. 왜냐면 톰캣(동기)서버가 아닌 네티(비동기)서버 이기때문이다

필터의 코드

@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) {
    	// 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
            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
            #포스트필터를 적용하기 위해 반환시켜주는 Chain에 연결시켜서 포스트필터 적용
            #모노는 웹플럭스에 추가되어있고, 비동기방식의 서버를 지원할때 단일값 전달할때 모노 객체 사용
                log.info("Custom POST filter: response id -> {}", response.getStatusCode());
            }));
        };
    }
}

해당 코드를 yml코드에 filter에 추가

filters:
  #삭제- AddRequestHeader=first-request,first-request-header2
  #삭제- AddResponseHeader=first-response,first-response-header2
  - CustomFilter

Global Filter

커스텀 필터는 원하는 라우트 정보에 개별적으로 등록하지만, GlobalFilter는 공통적으로 다 실행될수 있는 공통 필터이다.
공통적인 필터이기 때문에 가장먼저 실행되고, 가장 마지막에 종료되며, 그사이에 커스텀 필터들이 실행된다.

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

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

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            ServerHttpResponse response = exchange.getResponse();
            log.info("Global Filter baseMessage: request id -> {}", config.getBaseMessage());
            if(config.isPreLogger()){
                log.info("Global Filter Start: request id -> {}", request.getId());
            }
            //Custom Post Filter
            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                log.info("Global Filter End: response id -> {}", response.getStatusCode());
            }));
        };
    }
}
spring:
  application:
    name: apigateway-service
  cloud:
    gateway:
      default-filters:
        - name: GlobalFilter
          args:
            baseMessage: Spring Cloud Gateway Global Filter
            preLogger: true
            postLogger: true

Logging Filter

글로벌 필터를 이용하여 로깅 필터를 만들어보자
로깅 필터 역시 AbstractGatewayFilterFactory 상속받아 apply메서드 구현하여 사용한다.

//람다로 한번에 만들어도 되지만, 다음과 같이 GatewayFilter를 정의하고 해당 객체를 넘겨서 필터인자를 채워주는 형식
        GatewayFilter filter = new OrderedGatewayFilter((exchange, chain) -> { //OrderedGatewayFilter는 GatewayFilter를 구현해주는 자식객체정도로 생각
            ServerHttpRequest request = exchange.getRequest();
            ServerHttpResponse response = exchange.getResponse();
            log.info("Logging Filter baseMessage: request id -> {}", config.getBaseMessage());
            if (config.isPreLogger()) {
                log.info("Logging PRE Filter: request id -> {}", request.getId());
            }
            //Custom Post Filter
            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                log.info("Logging POST Filter: response id -> {}", response.getStatusCode());
            }));
        }, Ordered.HIGHEST_PRECEDENCE);
        return filter;

Load Balancer

지금까지의 아키텍쳐를 보면 아래의 그림과 같이 정리할수 있다.
1. api gateway로 요청이 들어옴
2. eureka서버에 해당 서비스가 등록되었는지 확인
3. 해당 마이크로 서비스에 접근
4. 응답 반환

위의 과정을 거치기 위해 api gateway와 eureka를 연동해보자

  1. 게이트웨이, 각서비스에 유레카 의존성을 추가하고, 유레카 정보를 추가해준다.
eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone:  http://localhost:8761/eureka
  1. 라우트 정보를 수정(로드밸러스로써 작동할것이기 때문에 URI 수정)
  • lb는 디스커버리 서비스 안에 포함되어 있는 인스턴스 이름을 찾겠다라는 기호이다.
      routes:
        - id: first-service
          uri: lb://MY-FIRST-SERVICE
          predicates:
          - Path=/first-service/**
          ...

정리

이번 게시글에서는 API Gateway에 대해 알아보았다.
API Gateway는 흩어져있는 마이크로서비스에 접근하기 위한 문과 같은 역할을 하며, 여러 필터를 통해 각 서비스에 여러 처리작업을 진행하였고, API Gateway와 유레카서버를 함께 사용하여 로드밸런싱과정까지 함께 살펴보았습니다.

0개의 댓글