Develop/Java & Spring

[MSA] MSA 전환 프로젝트 - 3. API Gateway & Spring Cloud Gateway

jjh0119 2025. 3. 26. 13:27

Chapter 2에서는 Service Discovery의 개념과 Eureka 라이브러리를 활용하여 Client-Side Discovery를 구현하고, 각 서비스의 연결 정보를 관리하는 체계를 구축하였다.

이번 글에서는 이렇게 Eureka에 등록된 여러 서비스를 외부 클라이언트가 사용할 수 있도록 애플리케이션에 진입하는 API Gateway에 대해 알아보고, 이를 활용한 예제를 구현하려고 한다.


1. API Gateway란?

해당 개념에 대해 Chapter1에서 간략하게 소개한 적이 있다.

Monolithic Architecture에서는 하나의 애플리케이션만 존재했기 때문에 모든 요청이 하나의 애플리케이션에 전달되면 되었다.
그러나 MSA에서는 서비스 별로 애플리케이션이 분리되었기 때문에 클라이언트의 요청에 대해서 분기 처리가 필요하게 되었다.
이러한 처리를 MSA에서는 API Gateway를 도입하여 해결할 수 있다.

Monolithic Architecture와 다르게 서비스별로 애플리케이션이 다 나눠져 있는 MSA 구조에서 외부의 요청이 각각의 마이크로 서비스로 진입하기 위한 서비스 전체의 공통 진입로 역할을 하는 것이 API Gateway인 것이다.
이렇게 API Gateway를 통하게 됨으로써 외부에서는 각 마이크로 서비스를 몰라도 API Gateway에서 라우팅해주는 서비스를 호출할 수 있고 API Gateway에 대한 연결정보만 가지고 Api Gateway만 상대하면 되기 때문에 일관적으로 작업을 처리할 수 있게 된다.

또한 앞선 프로젝트에서 진행했던 인증, 권한부여를 포함한 요청에 대한 더 많은 관심사를 API Gateway에서 처리할 수 있다.

  • 인증, 권한 부여
  • 서비스 검색 통합
  • 응답 캐싱
  • 정책, QOS 다시 시도, 회로차단기
  • 속도 제한
  • 부하분산(Load Balancing)
  • 로깅, 추적, 상관관계
  • 헤더, 쿼리 문자열 청구 변환
  • IP 허용 목록에 추가

2. API Gateway 라이브러리

API Gateway를 구현하기 위한 라이브러리 중 대표적으로 2가지를 꼽을 수 있다.

  • Netflix Zuul
  • Spring Cloud Gateway

차이를 간단하게만 말해보면
Netflix Zuul은 Servlet 기반의 동기/Blocking 방식이고, Spring Cloud Gateway는 Netty 기반의 비동기/Non-Blocking 방식이다.

Spring Cloud Gateway는 Spring Reactive 환경에서 구현된 API Gateway이기 때문에 Reactive 환경에서 활용하기 위해 Spring 5에서 새롭게 추가된 웹 프레임워크인 WebFlux 위에서 동작하게 된다.

현재 스프링에서는 NetFlix Zuul에 대한 지원을 중지하고 Spring Cloud Gateway를 사용할 것을 권장하고 있고 높은 확장성과 성능을 가진 WebFlux가 여러 사용자의 동시 요청환경에 놓이게 되는 MSA 구조와 잘 어울리기 때문에 Spring Cloud Gateway 라이브러리를 활용해서 프로젝트를 진행해보고자 한다.


3. Spring Cloud Gateway 동작 과정

Spring Cloud Gateway의 동작과정은 다음과 같다.

  1. 클라이언트가 Spring Cloud Gateway에 요청을 보낸다.
  2. Gateway Handler Mapping에서 요청 경로를 파악한 후 Gateway Web Mapping으로 요청을 전달한다.
  3. 핸들러에서 해당 요청과 관련한 Pre Filter 로직이 실행된다.
  4. 프록시된 서비스로 라우팅 된다.
  5. 프록시된 서비스가 실행되고 Response를 반환한다.
  6. Post Filter 로직이 실행되고 반환받은 Response를 핸들러가 클라이언트에게 전달한다.

복잡하게 보일 수 있지만 요청들을 각 서비스 별로 라우팅해주고 그 과정에 Pre Filter 와 Post Filter를 통해서 여러 관심사를 처리할 수 있다는 뜻이다.


4. Spring Cloud Gateway 적용하기

이전 챕터에서 Eureka Server와 Eureka Client로 등록할 마이크로 서비스를 만들고 연결하는 것까지 해봤는데
이번 챕터에서는

  1. API Gateway 구현
  2. API Gateway를 Eureka Client로 등록해서 Eureka Client를 통해 각각의 마이크로 서비스에 대한 연결정보를 획득
  3. API Gateway로 들어온 요청을 알맞는 마이크로 서비스로 분기
  4. Custom Filter와 Global Filter를 통해 요청에 대한 관심사 처리
  5. Spring Cloud Gateway의 LoadBalancer 기능을 통해 Load Balancing 처리

이렇게 API Gateway를 활용하는 예제를 직접 구현해보려고 한다.

1) 기본 설정 & 라우팅 처리

1-1) build.gradle 의존성 추가

dependencies {
	implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
	implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
}

1-2) Eureka Client 활성화

package com.example.apigatewayservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class ApigatewayServiceApplication {

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

}

메인 클래스에는 @EnableDiscoveryClient 어노테이션을 부착해서 Eureka Client로 등록해줬다.
사실 안해줘도 Spring Cloud에서 자동으로 등록해주지만 그래도 예제이기 때문에 명시하는 차원에서 해준다.

1-3) application.yml 라우팅 설정

server:
  port: 8000

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka

spring:
  application:
    name: apigateway-service
  cloud:
    gateway:
      routes:
        - id: first-service
          uri: lb://MY-FIRST-SERVICE
          predicates:
            - Path=/first-service/**
            
        - id: second-service
          uri: lb://MY-SECOND-SERVICE
          predicates:
            - Path=/second-service/**

기존 Eureka Client를 등록할 때와 크게 다르지 않다.
spring.cloud.gateway 하위에 라우팅 관련 설정만 추가로 해주면 되는데

  • spring.cloud.gateway.routes.id : 라우팅을 구분하기 위한 route id를 지정한다.
  • spring.cloud.gateway.routes.uri : 요청이 라우팅되는 경로 URI를 의미한다.
    • 원래는 http://localhost:8001/처럼 직접적인 경로를 넣는 것이 일반적이나, lb 프로토콜(lb://애플리케이션 이름)의 경로를 지정하면 Eureka Server에서 호스트에 해당하는 서비스를 찾고 라운드로빈 방식의 로드밸런싱을 수행한다.
  • spring.cloud.gateway.routes.predicates : 해당 라우팅을 진행할 URI 조건으로, Gateway로 들어오는 요청 URI를 지정한다.
    • 예를 들어 /first-service/welcome이라는 요청이 들어오면 해당 라우팅이 진행되어 경로 URI로 지정한 lb://MY-FIRST-SERVICE가 호출되고 first-service에서 api 요청에 대한 응답이 반환된다.

2) Filter 처리 구현

Spring Cloud Gateway에서 필터를 구현하는 방법은 여러가지가 있다.

  • configuration 클래스 구현
  • application.yml에 직접 구현
  • custom filter 클래스 구현

우리는 이 중에서 custom filter 클래스를 구현해 볼 예정이고 나머지는 짧게만 소개하도록 하겠다.

FilterConfig.class를 구현하는 방법

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

파라미터로 넘겨받은RouteLocationBuilder 객체를 통해서 만든Routelocator를 반환하는 메서드를 만들고 이것을 @Bean으로 등록해준다.

이렇게 메서드를 만들게되면 application.yml에서 설정했던 라우트에 관한 설정도 여기서 진행할 수 있게 된다.

RouteLocationBuilder 객체의 routes().route()메서드를 통해 필터를 등록할 수 있는데 간단하게 path()는 application.yml의 spring.cloud.gateway.routes.predicates 설정과 같고 이 경로로 들어온 요청에 대해서 처리하겠다는 뜻이다.
filter()를 통해 필터 기능을 구현하게 되는데 여기서는 addRequestHeader()로 Reqeust Header에 key-value 쌍으로 새로운 헤더를 등록하는 간단한 기능만 구현해보았다.
마찬가지로 addResponseHeader()로 Response Header에도 새로운 헤더를 등록해주고 uri() 메서드를 통해 라우팅 될 경로를 지정해준다.

applicaion.yml에 직접 설정하는 방법

spring:
  application:
    name: apigateway-service
  cloud:
    gateway:
      routes:
        - id: first-service
          uri: lb://MY-FIRST-SERVICE
          predicates:
            - Path=/first-service/**
          filters:
            - AddRequestHeader=first-request, first-request-header2
            - AddResponseHeader=first-response, first-response-header2
        - id: second-service
          uri: lb://MY-SECOND-SERVICE
          predicates:
            - Path=/second-service/**
          filters:
            - AddRequestHeader=second-request, second-request-header2
            - AddResponseHeader=second-response, second-response-header2

위의 FilterConfig.class와 동일한 결과를 수행하는 설정이다.
아까 해줬던 라우팅 설정에 더해 spring.cloud.gateway.routes.filters를 설정해서 필터 기능을 추가한다.

custom filter 구현
이제 custom filter를 만들어서 적용해보겠다.

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

            }));
        };
    }

    @Data
    public static class Config{
        private String baseMessage;
        private boolean preLogger;
        private boolean postLogger;
    }
}
public class CustomFilter extends AbstractGatewayFilterFactory<CustomFilter.Config>

Custom Filter는 AbstractGatewayFilterFactory라는 클래스를 상속받고 apply()를 오버라이딩 해야한다.
특이한 점은 Spring Cloud Gateway는 앞서 말했듯 WebFlux 기반의 라이브러리이기 때문에
기존 Spring MVC에서 사용하던 ServletHttpReqeust,ServletHttpResponse 대신에 다음과 같은 객체를 사용한다.

  • ServerHttpRequset : 요청 관련 정보 객체
  • ServerHttpResponse : 응답 관련 정보 객체
  • Mono : 비동기적으로 단일 결과를 나타내는 객체

지금은 간단하게 log를 출력하는 정도의 기능만 하는 필터지만 추후에 실제 프로젝트를 전환하게 된다면 인증, 인가에 관한 로직등 관심사 처리에 대한 부분들이 실제로 여기서 구현되게 될 것이다.

application.yml에 custom filter 설정
만들어 놓은 custom filter를 설정할 때도 두 가지의 방법이 있다.

  • deault-filter로 설정해 전역에 적용하는 방법
  • spring.cloud.gateway.routes.filters에서 Custom Filter를 불러와 해당 라우팅에만 적용하는 방법
server:
  port: 8000

eureka:
  client:
    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
          predicates:
            - Path=/first-service/**
          filters:
#           일반 필터
            - CustomFilter
        - id: second-service
          uri: lb://MY-SECOND-SERVICE
          predicates:
            - Path=/second-service/**
          filters:
            - name: CustomFilter
            - name: LoggingFilter
              args:
                baseMessage: Hi,there.
                preLogger: true
                postLogger: true

필터로 설정할 때 설정해줘야 하는 값으로 name과 args가 있는데

name은 Custom Filter 클래스의 이름을 넣어주면 되고 적용되는 하나만 있을 땐 name:을 생략해도 되지만 2개 이상일 때는 꼭 명시해서 적어주어야 한다.

args는 Custom Filter로 전달하는 값으로 args 내부에 정의된 값들은 해당 필터의 Config 클래스에 매핑되어 전달되고 이것을 바인딩해 Custom Filter의 부모클래스인 AbstractGatewayFilterFactory에 전달한다.
이를 통해 설정 클래스를 정의할 수 있으며, Spring Cloud Gateway는 apply 메서드를 호출할 때 이것을 참조한다.