
API 게이트웨이는 클라이언트의 요청을 받아 백엔드 서비스로 라우팅하고, 다양한 부가 기능을 제공하는 중간 서버입니다. 클라이언트와 서비스 간의 단일 진입점 역할을 하며, 보안, 로깅, 모니터링, 요청 필터링 등을 처리합니다.
API Gateway를 사용하면 여러 MSA 서비스를 클라이언트가 각각 호출할 필요 없이, 단일 호스트로 통합하여 요청을 보낼 수 있습니다.
Gateway는 로그인 및 인증(헤더 토큰, JWT), 권한(롤체크) 확인 기능을 지원하여, 사용자의 요청에 대해 로그인 여부나 권한 조건을 처리할 수 있습니다.
주문 건수 등 특정 데이터에 대한 로깅과 비로그인 사용자의 요청 통계 수집 역시 Gateway에서 가능하며, 별도의 로깅 애플리케이션과 연계해 서비스 운영에 필요한 정보를 기록할 수 있습니다
Spring Cloud Gateway를 사용하려면 Spring Boot 애플리케이션에 의존성을 추가해야 합니다.
아래는 build.gradle 예시입니다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
}
application.yml 파일에서 라우팅 설정을 정의할 수 있습니다.
spring:
cloud:
gateway:
discovery:
locator:
enabled: true # 서비스 디스커버리를 통해 동적으로 라우트를 생성하도록 설정
routes:
- id: users-service # 라우트 식별자
uri: lb://users-service # 'users-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
predicates:
- Path=/users/** # /users/** 경로로 들어오는 요청을 이 라우트로 처리
- id: orders-service # 라우트 식별자
uri: lb://orders-service # 'orders-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
predicates:
- Path=/orders/** #/orders/** 경로로 들어오는 요청을 이 라우트로 처리
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
필터의 종류는 2가지가 있습니다.
필터를 구현하려면 GlobalFilter 또는 GatewayFilter 인터페이스를 구현하고, filter 메서드를 오버라이드해야 합니다. 예시는 실습을 진행하면서 보여드리겠습니다.
Mono는 리액티브 프로그래밍에서 0 또는 1개의 데이터를 비동기적으로 처리합니다.Mono<Void>는 아무 데이터도 반환하지 않음을 의미합니다.ServerWebExchange는 HTTP 요청과 응답을 캡슐화한 객체입니다.exchange.getRequest()로 HTTP 요청을 가져옵니다.exchange.getResponse()로 HTTP 응답을 가져옵니다.GatewayFilterChain은 여러 필터를 체인처럼 연결합니다.chain.filter(exchange)는 다음 필터로 요청을 전달합니다.Pre Filter
@Component
public class PreFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 요청 로깅
System.out.println("Request: " + exchange.getRequest().getPath());
return chain.filter(exchange);
}
@Override
public int getOrder() { // 필터의 순서를 지정합니다.
return -1; // 필터 순서를 가장 높은 우선 순위로 설정합니다.
}
}
Post Filter
@Component
public class PostFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
// 응답 로깅
System.out.println("Response Status: " + exchange.getResponse().getStatusCode());
}));
}
@Override
public int getOrder() {
return -1;
}
}
회사에서 이미 만들어진 시스템은 스프링 부트 2를 사용하고 있을 가능성이 있습니다.
코드 보다는 기능에 집중해서 Zuul을 알고 가면 좋을 것 같습니다. 코드는 다르지만 Spring Cloud Gateway와 같은 기능을 제공합니다.
Spring Boot 2에서는 Zuul을 사용하여 API 게이트웨이를 설정할 수 있습니다.
아래는 build.gradle 예시입니다.
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-zuul'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
}
아래는 Spring Boot 애플리케이션 입니다.
@SpringBootApplication
@EnableZuulProxy
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
application.yml 파일에서 라우팅 설정을 정의할 수 있습니다.
zuul:
routes:
users-service:
path: /users/**
serviceId: users-service
orders-service:
path: /orders/**
serviceId: orders-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
Zuul 필터를 사용하여 요청 전후에 다양한 작업을 수행할 수 있습니다.
@Component
public class PreFilter extends ZuulFilter {
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
// 요청 로깅
System.out.println(String.format("%s request to %s", request.getMethod(), request.getRequestURL().toString()));
return null;
}
}
💡 클라우드 게이트웨이 + 유레카 + Order 인스턴스(1개) + Product 인스턴스(2개) 로 진행해봅니다.
이번 실습은 저번에 로드밸런싱의 실습 파일에서 진행하겠습니다. 제 글의 MSA 시리즈의 로드밸런싱을 참고해주세요.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/order")
public class OrderController {
@GetMapping
public String getOrder() {
return "Order details";
}
}
server:
port: 19092
spring:
application:
name: order-service
eureka:
client:
service-url:
defaultZone: http://localhost:19090/eureka
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ProductController {
@Value("${server.port}") // 애플리케이션이 실행 중인 포트를 주입받습니다.
private String serverPort;
@GetMapping("/product")
public String getProduct() {
return "Product info!!!!! From port : " + serverPort;
}
}
server:
port: 19093
spring:
application:
name: product-service
eureka:
client:
service-url:
defaultZone: http://localhost:19090/eureka
상품 어플리케이션은 로드 밸런싱 확인을 위해 2개의 포트(19093.19094)에 실행하겠습니다. 19092는 삭제해주세요. “클라이언트 사이드 로드 밸런싱” 실습을 참고해서 진행하세요.
추가적으로 혹시 이미 사용중인 포트라면 해당 포트를 정지시켜주세요
윈도우의 경우 CMD 창을 열고 아래의 명령어를 입력합니다. 뒤에 숫자는 정지시키고자 하는 포트입니다.
start.spring.io 에서 아래와 같은 디펜던시를 추가 후 프로젝트를 생성합니다.

이제 filter를 작성합니다. 먼저 prefilter입니다.
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.logging.Logger;
@Component
public class CustomPreFilter implements GlobalFilter, Ordered {
private static final Logger logger = Logger.getLogger(CustomPreFilter.class.getName());
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest response = exchange.getRequest();
logger.info("Pre Filter: Request URI is " + response.getURI());
// Add any custom logic here
return chain.filter(exchange);
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}
다음은 PostFilter입니다.
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.logging.Logger;
@Component
public class CustomPostFilter implements GlobalFilter, Ordered {
private static final Logger logger = Logger.getLogger(CustomPostFilter.class.getName());
@Override
public Mono<Void> filter(ServerWebExchange exchange, org.springframework.cloud.gateway.filter.GatewayFilterChain chain) {
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
logger.info("Post Filter: Response status code is " + response.getStatusCode());
// Add any custom logic here
}));
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}
마지막으로 application.yml입니다.
server:
port: 19091 # 게이트웨이 서비스가 실행될 포트 번호
spring:
main:
web-application-type: reactive # Spring 애플리케이션이 리액티브 웹 애플리케이션으로 설정됨
application:
name: gateway-service # 애플리케이션 이름을 'gateway-service'로 설정
cloud:
gateway:
routes: # Spring Cloud Gateway의 라우팅 설정
- id: order-service # 라우트 식별자
uri: lb://order-service # 'order-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
predicates:
- Path=/order/** # /order/** 경로로 들어오는 요청을 이 라우트로 처리
- id: product-service # 라우트 식별자
uri: lb://product-service # 'product-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
predicates:
- Path=/product/** # /product/** 경로로 들어오는 요청을 이 라우트로 처리
discovery:
locator:
enabled: true # 서비스 디스커버리를 통해 동적으로 라우트를 생성하도록 설정
eureka:
client:
service-url:
defaultZone: http://localhost:19090/eureka/ # Eureka 서버의 URL을 지정
유레카 서버 ⇒ 게이트웨이 ⇒ 주문 ⇒ 상품 순으로 어플리케이션을 실행합니다.



