API Gateway Service란 무엇인가?

API Gateway Service를 간략하게 설명하면 고객이 접근하는 클라이언트(모바일,웹)에서 서비스를 요청시, 모든 요청을 받는 중간자 역할을 수행하며,  요청에 맞는 마이크로서비스 인스턴스의 API를 찾아줍니다. 또한 통일 된 접근 경로를 가지기 때문에 공통 처리 기능인 로깅,인증,필터와 같은 기능을 수행할 수 있는 서비스입니다.

마이크로 서비스의 전체적인 호출 구조는 아래 사진과 같습니다.

 

MSA 호출 구조

 

위 그림처럼, API Gateway는 모든 클라이언트 APP의 서비스 요청 정보를 받아 통합적으로 마이크로 서비스의 호출을 도와주는 기능을 제공합니다. 해당 API Gateway는 모든 요청을 일관 되게 처리하기 위하여 요청 정보를 Service Discovery에 전달하며, 요청 정보에 맞는 서비스가 있을 시 게이트웨이 서비스의 설정 정보에 따라 마이크로 서비스의 호출을 보다 간편하게 도와줍니다. 좀 더 자세한 내용은 구현한 내용을 기반으로 설명하도록 하겠습니다.  

 

API Gateway Service 프로젝트 생성

build.gradle

plugins {
	id 'java'
	id 'org.springframework.boot' version '2.7.17'
	id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

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

java {
	sourceCompatibility = '11'
}

repositories {
	mavenCentral()
}

ext {
	set('springCloudVersion', "2021.0.8")
}

dependencies {
	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'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
}

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

tasks.named('test') {
	useJUnitPlatform()
}

https://start.spring.io/ 사이트 혹은 Intellij를 통해 생성한 스프링 부트 프로젝트의 build.gradle 파일은 위와 같습니다.

필자는 강의 정보에 따라 스프링 부트 2.7.17 버전에 호환 되는 스프링 클라우드 2021.0.8 버전을 사용하였습니다. 스프링 클라우드 사용시 자바 및 스프링 부트의 버전 호환성을 고려하여 생성하여야 오류 없이 동작합니다. dependency로는 Gateway, Lombok, Eureka Discovery Client를 추가하게 되면 다음과 같은 설정 정보를 가지게 됩니다.  

 

application.yml

server:
  port: 8000

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/**

초기 프로젝트 생성시, application.yml의 설정 정보는 위와 같습니다. 게이트웨이 서비스의 포트는 8000번으로 등록하였습니다. 또한 이전에 예시로 만들었던 두 개의 마이크로 서비스인 first-service와 second-service의 uri 주소를 게이트웨이에 등록하였습니다.

해당 설정의 하나의 예시로써  게이트웨이에서 first-service를 호출할때에는 http://localhost:8000/first-service/**  호출시 first-service의 주소인 http://localhost:8081/first-service/**를 불러 온다는 설정입니다. 이렇게 마이크로서비스를 설정 정보에 등록하게 되면, 여러 개의 마이크로 서비스의 호출을 게이트웨이 서비스에서 관리 할 수 있습니다.

 

Spring Cloud Gateway - FilterConfig 적용

앞에서 설명한 것처럼, 모든 서비스 요청시 하나의 게이트웨이를 지나가기 때문에 공통의 기능으로 필터를 적용할 수 있습니다.  필터를 제공하는 설정 클래스 정보는 아래와 같습니다. 

 

FilterConfig.java

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-value")
                                .addResponseHeader("first-resonse","first-resonse-value"))
                        .uri("http://localhost:8081"))
                .route(r -> r.path("/second-service/**")
                        .filters(f -> f.addRequestHeader("second-request","second-request-value")
                                .addResponseHeader("second-resonse","second-resonse-value"))
                        .uri("http://localhost:8082"))
                .build();
    }
}

RouteLocator 함수를 정의하여 빈 설정 정보로 등록하게 되면, 게이트웨이 호출시 원하는 필터 기능을 구현할 수 있습니다.

위와 같은 설정의 의미로는 [게이트웨이주소]/first-service/** 경로 호출시 first-service의 주소인http://localhost:8081/first-service/**  로 매핑 시켜주는 라우팅 설정 기능이며, RequestHeader에는 key:value 쌍으로 first-request:first-request-value를 필터에 추가하여 입력해주겠다는 의미입니다. 다음과 같이 설정하면 first-service의 Controller에서는 아래와 같이 받게 됩니다.

 

FirstServiceController.java

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 {

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

위 필터를 적용하게 되면 [게이트웨이주소] = http://localhost:8000 이라고 가정하였을 때, http://localhost:8000/first-service/message를 호출 시, first-service의 주소인 http://localhost:8081/first-service/message를 호출하게 됩니다.

또한 @RequestHeader를 통해 필터에서 설정한 first-request의 정보를 가져옵니다.

로그를 찍게 되면 first-request의 값인 first-request-value를 정상적으로 가져오는 것을 알 수 있습니다.

이렇듯 FilterConfig.java 파일을 통해 서비스 라우팅 및 필터 기능이 수행 되는 것을 알 수 있었습니다.

다만 주의할 점은 FilterConfig 클래스에서 RouteLocator를 빈으로 등록시, application.yml의 설정 정보는 주석을 쳐주셔야 합니다. 해당 부분은 아래와 같습니다.

 

application.yml - FilterConfig.java 파일 적용시

server:
  port: 8000

spring:
  application:
    name: gateway-service

위와 같이 spring.application.cloud 부분을 지워줘야 FilterConfig.java 설정이 정상적으로 동작합니다.

다음은 FilterConfig.java를 사용하여 설정 정보를 잡는 것이 아닌 application.yml의 설정으로 라우팅과 필터 기능을 적용하는 방법을 알아보겠습니다. 적용하기 전 FileterConfig.java에서는 @Configuration에 '//' 주석을 달아 해당 클래스가 동작하지 않도록 사전에 바꿔줍니다.

 

application.yml - FilterConfig.java 파일 미적용 시

server:
  port: 8000

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

기존에 FilterConfig.java에서 설정했던 RequestHeader 정보를 yml에서는 AddRequestHeader와 AddResponseHeader를 설정하여 세팅할 수 있습니다. 위 yml 파일 사용시 FilterConfig.java와 같이 라우팅과 필터가 동작하는 것을 알 수 있습니다. 다음은 사용자 정의에 따라 CustomFilter를 등록하는 방법을 알아보겠습니다.

 

Spring Cloud Gateway - CustomFilter 적용

CustomFilter.java

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; // 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(CustomFilter.Config config){
        // Pre Filter
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            ServerHttpResponse response = exchange.getResponse();

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

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

    public static class Config{

    }
}

CustomFilter 클래스는 위와 같이 설정합니다. 위와 같이 설정할 시, Webflux 모듈에서 지원하는 reactive 클래스인 ServerHttpRequest와 ServerHttpResponse 클래스를 정의하여 GatewayFilter 함수 안에서 선처리 필터인 Pre Filter와 후처리 필터인 Post Filter를 만들 수 있습니다. 위와 같이 동작하면 서비스가 동작하기 전, request.getId()를 로그로 출력할 것이며, 서비스가 동작한 후에는 response.getStatusCode()를 로그로 출력할 것입니다. 위와 같은 Pre Filter를 사용하게 되는 예시로는 JWT 토큰을 가져와 인증 및 인가를 하는 기능을 추가할 수 있습니다. 위 클래스를 정의하고 적용하기 위해서는 아래와 같이 application.yml 파일을 수정합니다.

   

 

application.yml - CustomFilter.java 클래스 적용시

server:
  port: 8000

spring:
  application:
    name: gateway-service
  cloud:
    gateway:
      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:
             - CustomFilter

위와 같이 이전에 yml 파일에서 설정했던 FilterConfig 정보를 주석으로 설정하고, filters에 CustomFilter를 추가해주면 정상적으로 동작하는 것을 확인 할 수 있습니다.

 

Spring Cloud Gateway - GlobalFilter 적용

 

GlobalFilter.java

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(GlobalFilter.Config config){
        // 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());
            }
            //Post Filter
            return chain.filter(exchange).then(Mono.fromRunnable(() ->{
                if(config.isPostLogger()){
                    log.info("Global Filter End : response status -> {}",response.getStatusCode());
                }
            }));
        };
    }

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

GlobalFilter 클래스를 만든 이유는 기존에 만들었던 CustomFilter와 같이 사용하여 GlobalFilter -> CustomFilter 순으로 동작하는 기능을 만들기 위함입니다. GlobalFilter가 CustomFilter와 다른 부분은 Config 부분의 baseMessage, preLogger, postLogger와 같은 변수의 선언 부분과 application.yml 파일에서 default-filters를 설정하는 부분입니다.

GlobalFilter를 적용한 application.yml 파일은 아래와 같습니다.

 

application.yml

server:
  port: 8000

spring:
  application:
    name: gateway-service
  cloud:
    gateway:
      default-filters:
        - name: GlobalFilter
          args:
            baseMessage: This is Global Filter Message
            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:
             - CustomFilter

위와 같이 default-filters 설정에 GlobalFilter 클래스명을 입력하고 args 파라미터 정보를 입력합니다. 기존에 filters로 설정해놓은 CustomFilter를 그대로 사용하면 동작하는 순서는 아래 사진과 같습니다.

 

GlobalFilter 동작 예시

위의 설정으로 서비스 동작시, Global Filter와 CustomFilter가 순서대로 동작하는 것을 볼 수 있습니다. 이와 같은 설정으로 전역 또는 지역적인 성격을 띄는 필터를 적용할 수 있습니다.

 

Spring Cloud Gateway - LoggingFilter 적용

다음으로 만들어 볼 필터는 우선순위를 설정할 수 있는 필터입니다. 그 예시로 로그를 찍는 역할을 하는 LoggingFilter를 적용하고자 합니다.

 

LoggingFilter.java

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(LoggingFilter.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 Start : request id -> {}",request.getId());
            }
            //Post Filter
            return chain.filter(exchange).then(Mono.fromRunnable(() ->{
                if(config.isPostLogger()){
                    log.info("Logging POST Filter End : response code -> {}",response.getStatusCode());
                }
            }));
        }, Ordered.LOWEST_PRECEDENCE);
        return filter;
    }

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

LoggingFilter 클래스는 우선순위를 설정할수 있는 클래스인 OrderedGatewayFilter 클래스로 작성합니다. 여기서 중요한 것은 우선순위를 설정하는 부분입니다. 

Ordered.LOWEST_PRECEDENCE : 가장 낮은 우선순위의 필터

Ordered.HIGHEST_PRECEDENCE :  가장 높은 우선순위의 필터

위와 같이 2개의 설정을 통해 해당 필터가 가장 우선적으로 동작할지, 가장 마지막으로 동작할지를 결정합니다.

LoggingFilter를 적용한 applcation.yml은 아래와 같습니다.

 

application.yml - LoggingFilter 적용 후

server:
  port: 8000

spring:
  application:
    name: gateway-service
  cloud:
    gateway:
      default-filters:
        - name: GlobalFilter
          args:
            baseMessage: This is Global Filter Message
            preLogger: true
            postLogger: true
      routes:
        - id: first-service
          uri: http://localhost:8081/
          predicates:
            - Path=/first-service/**
          filters:
             - name: CustomFilter
             - name: LoggingFilter
               args:
                 baseMessage: This is Logging Filter Message
                 preLogger: true
                 postLogger: true
        - id: second-service
          uri: http://localhost:8082/
          predicates:
            - Path=/second-service/**
          filters:
             - name: CustomFilter
             - name: LoggingFilter
               args:
                 baseMessage: This is Logging Filter Message
                 preLogger: true
                 postLogger: true

filters에 다음과 같이 LoggingFilter의 설정을 추가하면 GlobalFilter, CustomFilter, LoggingFilter를 적용할 수 있게 됩니다.

다음은 우선순위에 따른 필터의 동작 순서를 보겠습니다.

 

Ordered.HIGHEST_PRECEDENCE

 

Ordered.LOWEST_PRECEDENCE

LoggingFilter에 우선순위를 각각 HIGHEST와 LOWEST로 적용했을때 필터가 동작하는 순서가 바뀌는것을 위와 같이 볼 수 있습니다.

 

Spring Cloud Gateway - Eureka 연동

다음은 Service Discovery에 등록되어 있는 서비스를 호출하기 위해 Eureka Client로 등록하는 작업을 할 것입니다.

Eureka Client로 등록할 시, Service Discovery(Eureka Server)에 등록 되므로, 해당 서비스가 존재하는지 확인하여 호출하는 구조를 만들 수 있게 됩니다. Eureka Client로 등록하기 위해 필요한 application.yml 설정 파일은 아래와 같습니다.

 

application.yml - Eureka Client 등록

server:
  port: 8000

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


spring:
  application:
    name: gateway-service
  cloud:
    gateway:
      default-filters:
        - name: GlobalFilter
          args:
            baseMessage: This is Global Filter Message
            preLogger: true
            postLogger: true
      routes:
        - id: first-service
          uri: lb://MY-FIRST-SERVICE
          predicates:
            - Path=/first-service/**
          filters:
             - name: CustomFilter
             - name: LoggingFilter
               args:
                 baseMessage: This is Logging Filter Message
                 preLogger: true
                 postLogger: true
        - id: second-service
          uri: lb://MY-SECOND-SERVICE
          predicates:
            - Path=/second-service/**
          filters:
             - name: CustomFilter
             - name: LoggingFilter
               args:
                 baseMessage: This is Logging Filter Message
                 preLogger: true
                 postLogger: true

eureka.client 설정의 service-url은 Service Discovery 서버의 주소로 해당 서버에 Eureka Client로 등록하겠다는 설정입니다. 해당 설정을 사용하게 되면 서버 기동시 Service Discovery에 등록됩니다. Dependency로써 Eureka Client를 등록하였다면 해당 설정을 할 수 있습니다. 추가적으로 변경된 first-service와 second-service의 uri를 수정하였는데, Service Discovery에 등록 된 application name을 참조하여 lb:// 문구로 선언하면, 해당 서비스 정보를 가져오게 됩니다. 

first-service 프로젝트와 second-service 프로젝트를 Service Discovery에 등록하기 위해서는 각 프로젝트의 application yml 파일을 수정해줘야 합니다. 

 

first-service - application.yml

server:
  port: 8081
spring:
  application:
    name: my-first-service

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

 

second-service - application.yml

server:
  port: 8082
spring:
  application:
    name: my-second-service

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

위와 같이 spring.application.name을 각각 my-first-service, my-second-service로 등록하였기 때문에 Service Discovery의 application 명이 해당 설정을 참조하여 게이트웨이에 등록할 수 있습니다.

 

그 외의 마이크로 서비스를 임의의 인스턴스로 여러 개를 띄우기 위해서는 application.yml 설정 파일의 server.port = 0으로 선언하여 랜덤 포트 형식으로 마이크로 서비스를 매번 다르게 띄울 수 있습니다.

 

 

정리

지금까지 스프링 클라우드에서 제공하는 Eureka Server, Eureka Client, Gateway Dependency를 참조하여 API Gateway의 역할을 알아보았습니다. 스프링 게이트웨이를 구현해보면서 마이크로 서비스를 호출하기 위해 어떤 설정이 필요한지, 필터는 어떤식으로 사용하는지, 여러 개의 인스턴스 사용시 로드 밸런싱을 어떻게 하는지 등을 알 수 있었습니다. MSA 아키텍처의 핵심 서비스 중 하나인 API Gateway의 개념은 중요한 부분이기에 기본 개념만이 아닌 응용 환경에서도 어떻게 동작하는지 꾸준히 공부해야 하는 중요 사항이라고 생각합니다.

'MSA' 카테고리의 다른 글

[MSA]Service Discovery와 Spring Cloud Netflix Eureka  (0) 2023.08.27
[MSA]MSA와 Monolithic의 차이점  (0) 2023.08.26

+ Recent posts