고딩왕 코범석

[MSA] API Gateway 본문

Language & Framework/Spring

[MSA] API Gateway

고딩왕 코범석 2021. 5. 11. 15:10
반응형

Reverse Proxy

리버스 프록시는 요청을 대신해서 보내는 것이다. 클라이언트의 요청을 받아 적절한 웹 서버로 요청을 전송한다. 요청을 받은 서버는 응답을 전송할 떄, 다시 이 리버스 프록시에게 전달하여 그 응답을 클라이언트에게 제공한다.

대표적으로는 nginx가 우리가 잘 알고있는 리버스 프록시 서버이다.

Api Gateway?

  • 사용자가 설정한 라우팅 설정에 따라서 각 엔드 포인트로 클라이언트 대신해서 요청을 하고 응답받으면 다시 클라이언트에게 전송하는 reverse proxy 역할
  • 클라이언트가 마이크로서비스를 직접 호출하는 구조는 별로 좋지 않음
  • 시스템의 내부 구조는 숨기고 외부의 요청에 대해 적절한 형태로 가공하여 응답을 클라이언트에게 전달하는 장점을 갖고 있다.
  • 클라이언트와 각 마이크로서비스 사이에 위치하여 단일 진입점 형태로 개발하며, 각각의 마이크로서비스 로 요청되는 모든 정보에 대해 모든 것들을 처리한다.
  • 즉, 클라이언트는 각각의 마이크로서비스들과 통신하는 것이 아닌 api gateway랑 통신하게 된다.

API Gateway가 하게 될 것

  • 인증과 권한 부여
  • 마이크로서비스들의 검색 통합
  • 속도 제한과 부하 분산(클라이언트의 원하는 서비스를 찾아줌)
  • 로깅
  • 헤더, 쿼리 문자열 및 청구 변환
  • IP 허용 목록에 추가

Spring cloud에서의 MSA간 통신

  • RestTemplate

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

    @FeignClient("stores")    // 호출하고 싶은 마이크로 서비스 명 작성
    public interface StoreClient {
        @GetMapping("/stores")
        List<Store> getStores();
    }

Ribbon

  • Client side Load Balncer
    • 클라이언트가 직접 서비스의 이름으로 호출한다.
  • 비동기와 호환이 잘 되지 않는다.
  • Health Check : 해당 서비스가 잘 작동하는지 확인
  • 현재 부트 2.4에서는 Maintenance 상태

Nexflix Zuul

  • Routing, Api Gateway 역할
  • Netflix Zuul도 부트 2.4에서 Maintenance 상태

Spring Cloud Gateway

  • Gateway Handler Mapping으로 들어오는 요청들을 적절한 대상으로 라우팅하는 간단하고 효과적인 방법 제공

  • 기존의 내장 서버는 톰캣이었다. 하지만 Gateway의 내장 톰캣은 Netty인 것을 알 수 있다.

    image

  • Netty는 Non-blocking, Asynchronous 방식이다. 때문에 서블릿 컨테이너나 war로 빌드하면 동작하지 않는다.

    image

  • Spring Cloud Gateway는 기본적으로 세 가지의 핵심 단위로 구성되어 있다.

    • Route : 목적지 URI, 충족해야 될 여러 조건들(Predicates), 각종 필터들로 이루어진 요청을 라우팅할 대상들이라고 가정
    • Predicate : 자바 8의 Function Predicate로 이루어져 있다. path 혹은 리퀘스트 헤더에 포함된 조건들을 의미한다. 즉, 라우팅에 필요한 조건!
    • Filter : 스프링 프레임워크의 WebFilter의 인스턴스이다. 사용자가 보내는 request, 응답 받는 response를 수정하거나 정제하는 것.

Spring Cloud Gateway의 동작 방식

image

  • 클라이언트는 스프링 클라우드 게이트웨이에 요청을 보낸다.
  • Gateway Handler Mapping이 요청 정보가 게이트웨이에 설정된 Route로 전달하는 것을 결정하고, Gateway Web Handler에게 전송한다.
  • Gateway Web Handler는 요청에 따른 필터체인을 통해 요청을 보낸다.
  • 그림에서 필터들이 점선으로 구분된 이유는, Proxied Service에 요청을 보내기 전에 해당 게이트웨이 서버에서 필터를 통한 로직을 수행해야하기 때문이다. 위에서 말했던 API Gateway가 하게 될 것들이라 생각하면 되겠다.
  • 사전에 필터들에 대한 로직들을 수행한 다음, proxied server에 원하는 정보를 얻고 사후 필터들에 대한 로직이 동작하여 클라이언트에게 응답이 간다.

Filter 설정

Java code로 설정

@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();
    }
}
  • 람다 표현식을 통해 라우팅 설정
  • route 메서드 내에 마이크로서비스 path를 설정한다.
  • filters 메서드를 통해 request, response header를 각각 추가
  • 마지막에 마이크로서비스의 uri로 매핑시켜주기

application.yml로 라우팅 설정

server:
  port: 8080
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-request-header-withyml
            - AddResponseHeader=first-response, first-response-header-withyml
        - id: second-service
          uri: http://localhost:8082/
          predicates:
            - Path=/second-service/**
          filters:
            - AddRequestHeader=second-request, second-request-header-withyml
            - AddResponseHeader=second-response, second-response-header-withyml
  • spring.cloud.gateway.routes : 프로젝트 각각의 마이크로서비스에 대한 라우팅 설정
  • id : 마이크로서비스 고유 id
  • uri : 실행 중인 마이크로서비스의 uri(포트까지!)
  • predicates : 위에서 설명한 조건절
    • 만약 'localhost:8080/first-service/hihi' url을 요청하면 yml 파일 설정에 적힌 것 처럼 first-service path는 8081포트를 사용하는 마이크로서비스인 id : first-service로 라우팅되어 있으므로 8081/first-service/hihi로 라우팅된다.
  • filters : 위의 자바 코드로 설정하는 방법처럼, 요청이나 응답에 대한 값들을 정제할 수 있다.

개인적으로는 자바코드로 설정하는 것이 훨씬 나아보인다. 컴파일로 내가 잘못 작성했을 경우 에러가 발생하기 때문에!

Custom Filter application.yml

@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 Prefilter
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            ServerHttpResponse response = exchange.getResponse();

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

            // Custom Postfilter
            return chain.filter(exchange)
                    .then(Mono.fromRunnable(() -> {
                        log.info("Custom Post filter : response statusCode -> {}", response.getStatusCode());
                    }));
        };
    }
}

Global Filter와 Custom Filter의 차이

각 라우팅 정보마다 커스터마이징한 필터를 등록해주어야 한다. 앞에서 설정했던 Custom Filter가 그렇다. 하지만 Global Filter의 경우 라우팅 정보들 마다 공통적으로 필요한 처리들을 한번의 설정으로 해결해준다.

둘 다 적용되었을 경우의 순서 : Global Filter PRE -> Custom Filter PRE -> Custom Filter POST -> Global Filter POST

application.yml 설정

server:
  port: 8080
eureka:
  client:
    register-with-eureka: false
    fetch-registry: false
    service-url:
      defaultZone: http://localhost:8761/eureka
spring:
  application:
    name: apigateway-service
  cloud:
    gateway:
      # Global Filter 설정하기
      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
#            - AddRequestHeader=first-request, first-request-header-withyml
#            - AddResponseHeader=first-response, first-response-header-withyml
        - id: second-service
          uri: http://localhost:8082/
          predicates:
            - Path=/second-service/**
          filters:
            - CustomFilter
#            - AddRequestHeader=second-request, second-request-header-withyml
#            - AddResponseHeader=second-response, second-response-header-withyml

LoggingFilter

Api Gateway에서는 로깅을 위한 필터도 존재하는데, 여기서 로깅 필터도 Custom Filter라서 마이크로 서비스 마다 등록을 해주어야 한다. 강의 내용 처럼 second-service에 등록하자.

이 Logging Filter를 등록하게 되면 요청에 대한 흐름은 다음과 같다.

image

Load Balancer

기본적으로 라운드 로빈 방식으로 각 마이크로서비스를 한번씩 호출한다.

API Gateway에서uri에 lb://서비스명 이라고 설정해두었는데, 이 때 랜덤포트로 설정한 경우는 서비스 명으로 게이트웨이에서 인스턴스를 찾아 각 서비스를 한번씩 호출한다.

설정에 따라 랜덤하게 서버 인스턴스를 호출할 수 있다.

출처

반응형