API Gateway 란?
API Gateway는 클라이언트와 백엔드 서비스 사이에서 중간 관리자 역할을 하는 서버입니다. 마이크로서비스 아키텍처나 복잡한 API 구조에서 자주 사용되며, 여러 서비스로의 요청을 단일 진입점에서 받아 요청을 적절한 내부 서비스로 라우팅하고, **공통 기능(인증, 로깅, 모니터링 등)**을 처리하는 역할을 합니다.
API Gateway란?
- 여러 개의 마이크로서비스를 하나의 진입점(API 엔드포인트)로 묶어주는 프록시 서버
- 클라이언트는 개별 서비스와 직접 통신하지 않고 Gateway를 통해 간접적으로 요청을 보냄
API Gateway 주요 기능 정리
기능 |
항목 설명 |
1. 라우팅 | 클라이언트 요청을 적절한 백엔드 서비스로 전달 |
2. 인증 및 권한 부여 | 토큰 검증, 사용자 인증, 역할 기반 접근 제어 등 보안 처리 |
3. 서비스 검색 통합 | 서비스 레지스트리(Eureka 등)와 연동하여 동적으로 서비스 위치 탐색 |
4. 응답 캐싱 | 자주 호출되는 응답을 캐싱하여 응답 속도 향상 및 서버 부하 감소 |
5. 정책, 회로 차단기, QoS 재시도 | 장애 대응(재시도, Circuit Breaker 등) 및 품질 보장 기능 제공 |
6. 속도 제한 (Rate Limiting) | 사용자별 요청 수 제한으로 API 남용 방지 |
7. 부하 분산 (Load Balancing) | 백엔드 인스턴스 간 트래픽을 분산시켜 안정적인 서비스 제공 |
8. 로깅, 추적, 상관 관계 | 요청/응답 로그 기록, 분산 트랜잭션 추적(Tracing), 문제 진단 지원 |
9. 요청/응답 변환 | 헤더, 쿼리 파라미터, 본문(body) 등을 수정하여 서비스 호환성 확보 |
10. IP 허용 목록 관리 | 지정된 IP만 접근 가능하도록 제어(IP 화이트리스트 설정) |
Spring Cloud Gateway
Spring Cloud Gateway는, API Gateway 역할을 수행하는 라이브러리로 Netty 기반의 비동기 방식으로 동작합니다.
Spring Cloud Gateway는 다음과 같은 방식으로 동작하게 됩니다.
1. 클라이언트는 Spring Cloud Gateway에 요청을 보냅니다.
2. Gateway Handler Mapping은 요청이 경로와 일치한다고 판단하면 Gateway Web Handler로 전달합니다.
3. Gateway WebHandler는 요청에 관련된 필터를 실행합니다.
4. 필터를 모두 실행한 후 원래 요청을 기반으로 프록시 요청을 생성하고 목적지 서비스로 라우팅합니다.
5. 응답이 Gateway로 들어오면 사후 필터 로직이 실행됩니다.
API Gateway 설정 방법
1. 의존성 주입
Spring Boot의 pom.xml 또는 build.gradle 파일에 Spring Cloud Routing의 Gateway 의존성을 추가한다.
dependencies {
// 아래 중 원하는 것을 하나 선택하여 의존성 주입
// Netty 가동 (비동기)
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
// Tomcat 가동 (동기) - 기본값
implementation 'org.springframework.cloud:spring-cloud-starter-gateway-mvc'
}
2. application.yml 파일 설정
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:
// Tomcat으로 구동하는 경우 mvc: 필요
routes:
- id: first-service
uri: http://localhost:8081/
predicates:
- Path=/first-service/**
- id: second-service
uri: http://localhost:8082/
predicates:
- Path=/second-service/**
- routes : Gatway에서 설정한 각 서비스로 요청을 전달하는 라우트 모음
- 이 설정은 JAVA 단에서 설정할 수 있다.
- id : Route의 고유 식별자
- uri : 요청이 매칭되었을 경우 전달될 대상 서버의 URI
- predicates : 지정된 경로와 일치하는 요청을 해당 서비스로 Routing
주의 사항
- 기본적으로 클라이언트가 http://localhost:8000/first-service/helloworld로 요청을 보내면 Gateway는 이 요청을 http://localhost:8081/first-service/helloworld로 라우팅 한다.
추가) Routes 설정을 JAVA 코드로 설정
1. application.yml 설정
server:
port: 8000
eureka:
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://localhost:8761/eureka
spring:
application:
name: apigateway-service
2. Route 등록
@Configuration
public class RouteConfig {
@Bean
public RouteLocator gatewayRoutes(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("/first-service/**")
.uri("http://localhost:8081"))
.route(r -> r.path("/second-service/**")
.uri("http://localhost:8082"))
.build();
}
}
API Gateway Filter
API Gateway에서 필터를 통해 요청 및 응답을 수정할 수 있으며 Filter는 application.yml 또는 Java 코드에서 설정할 수 있다.
1. Route와 Filter를 JAVA 로 설정
application.yml에서 routes 설정을 제거하고 Java 코드에서 Filter와 Route를 설정하는 방법이다.
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();
}
}
2. 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-requests-header2
- AddResponseHeader=first-response, first-response-header2
- id: second-service
uri: http://localhost:8082/
predicates:
- Path=/second-service/**
filters:
- AddRequestHeader=second-request, second-requests-header2
- AddResponseHeader=second-response, second-response-header2
- filters : 지정된 라우터에서 실행할 필터를 설정할 수 있다.
- AddRequestHeader=key, value : 요청 헤더를 추가
- AddResponseHeader=key, value : 응답 헤더를 추가
Spring 공식 블로그에서 yml에서 설정할 수 있는 다양한 필터 사용법이 소개되어 있다.
3. Custom Filter 설정
별도의 Filter Component 를 정의한 뒤 applications.yml 에 적용하는 방법이다.
1. 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;
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 CustomFilter extends AbstractGatewayFilterFactory<CustomFilter.Config> {
public CustomFilter() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
// Custom Pre Filter. Suppose we can extract JWT and perform Authentication.
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
log.info("Custom PRE filter: request id -> {}", request.getId());
request.mutate().header("custom-header-key", "custom-header-value");
// Custom Post Filter. Suppose we can call error response handelr based on error code.
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
log.info("Custom POST filter: response code -> {}", response.getStatusCode());
}));
};
}
public static class Config {
// Put the configuration properties
}
}
- serverHttpRequest는 불변 객체이기 때문에 해당 request에 직접적으로 수정하는 것은 불가능하다.
- 때문에 mutate()를 통해 해당 불변객체를 복사하여 수정하여야 한다.
2. application.yml에 적용
Bean으로 등록된 필터를 Route에 적용시킬 수 있다.
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-requests-header2
# - AddResponseHeader=first-response, first-response-header2
- CustomFilter
- id: second-service
uri: http://localhost:8082/
predicates:
- Path=/second-service/**
filters:
# - AddRequestHeader=second-request, second-requests-header2
# - AddResponseHeader=second-response, second-response-header2
- CustomFilter
단일 필터의 경우 key 값을 생략해도 되지만 만약 필터가 2개 이상이고 args가 요구된다면 아래와 같이 설정해줘야 한다.
- name: CustomFilter
- name: LoggingFilter
args:
baseMessage: Hi, there.
preLogger: true
postLogger: true
4. Global Filter 설정
Global Filter는 Spring Cloud Gateway에서 모든 라우트에 대해 공통적으로 적용되는 필터이며 Global Filter는 모든 요청과 응답에 대해 적용된다.
기본적으로 순서는 default Filter(request) -> Custom Filter(request) -> Custom Filter(response) -> default Filter(response)이지만 이런걸 무시하고 순서를 결정할 수도 있다.
1. Global Filter 등록
filter 등록 방식은 customfiler 와 동일하다.
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;
@Slf4j
@Component
public class GlobalFilter extends AbstractGatewayFilterFactory<GlobalFilter.Config> {
public GlobalFilter() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
// Custom 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());
}
// Custom Post Filter
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;
}
}
순서 지정(0번) - OrderedGatewayFilter
@Override
public GatewayFilter apply(Config config) {
return new OrderedGatewayFilter((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 : request code = {}", response.getStatusCode());
}
}));
}, 0);
}
2. 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:
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
- id: second-service
uri: http://localhost:8082/
predicates:
- Path=/second-service/**
filters:
- CustomFilter
- default-filters: 모든 라우트에 기본적으로 적용할 필터 설정
- name : Global Filter로 사용할 클래스 명
- args
- key: value 형식으로 작성하며 여기서 작성된 key값과 AbstractGatewayFilterFactory의 제네릭 타입 파라미터 객체의 속성과 일치한다면 해당 value로 초기화해 준다.
- 만약 key가 없다면 무시된다.