0. 들어가며
마이크로서비스 아키텍처에서는 하나의 애플리케이션이 여러 개의 독립적인 서비스로 분할된다. 사용자 관리, 상품 카탈로그, 주문, 결제 등 각 서비스는 자신만의 API를 노출한다. 만약 클라이언트(웹, 모바일 앱 등)가 각 서비스에 직접 요청을 보내게 되면 어떤 문제가 발생할까?
- 첫째, 클라이언트가 여러 서비스의 엔드포인트를 모두 알고 있어야 하므로 복잡성이 증가한다.
- 둘째, 인증, 로깅, CORS 처리 등 모든 서비스가 공통적으로 처리해야 하는 기능을 각각 구현해야 하는 비효율이 발생한다.
- 셋째, 서비스 구조가 변경될 때 클라이언트도 함께 변경되어야 하는 강한 결합이 생긴다.
이러한 문제를 해결하기 위해 등장한 개념이 바로 API 게이트웨이(API Gateway)다. API 게이트웨이는 모든 클라이언트 요청의 단일 진입점 역할을 하며, 내부 마이크로서비스 구조를 캡슐화하고 공통 기능을 중앙에서 처리한다. 이번 글에서는 Spring Cloud Gateway를 활용한 API 게이트웨이 구축 방법을 상세히 살펴보겠다.
1. API Gateway란?
1.1. API Gateway의 필요성
마이크로서비스 아키텍처에서 API 게이트웨이가 필요한 이유를 구체적으로 살펴보자.
- 클라이언트 단순화: 모바일 앱, 웹 애플리케이션 등 다양한 클라이언트는 단일 엔드포인트만 알면 된다. 내부적으로 수많은 서비스가 어떻게 구성되어 있는지 알 필요가 없다.
- 횡단 관심사 분리: 인증/인가, 로깅, 모니터링, 요청 제한(Rate Limiting) 등의 공통 기능을 모든 서비스에 분산 구현하지 않고 게이트웨이에서 중앙 처리할 수 있다.
- 프로토콜 변환: 클라이언트가 사용하는 프로토콜(HTTP, WebSocket 등)과 내부 서비스가 사용하는 프로토콜 간의 변환을 담당할 수 있다.
- 라우팅과 로드 밸런싱: 요청 경로에 따라 적절한 서비스로 라우팅하고, 여러 인스턴스가 있을 경우 로드 밸런싱을 수행한다.
- 응답 변환과 집계: 여러 서비스의 응답을 조합하여 클라이언트에 맞는 형태로 가공하거나, 불필요한 데이터를 필터링할 수 있다.
1.2. API Gateway 패턴의 장단점
장점:
- 클라이언트와 백엔드 서비스 간 결합도 감소
- 공통 기능의 중앙 집중식 관리로 개발 및 운영 효율성 향상
- 백엔드 서비스의 세부 구현 변경이 클라이언트에 미치는 영향 최소화
- 보안 정책을 일관되게 적용 가능
단점:
- 네트워크 홉이 추가되어 약간의 지연 시간 증가
- 게이트웨이가 단일 장애점(SPOF)이 될 수 있어 고가용성 구성 필수
- 복잡한 라우팅 규칙으로 인해 게이트웨이 자체가 복잡해질 수 있음
2. Spring Cloud Gateway의 소개
2.1. Spring Cloud Gateway란?
Spring Cloud Gateway는 Spring 생태계에서 제공하는 API 게이트웨이 솔루션이다. Project Reactor를 기반으로 한 논블로킹(Non-blocking) 아키텍처를 채택하여 높은 성능과 적은 리소스 사용을 제공한다. 기존에는 Netflix Zuul 1.x가 많이 사용되었으나, Zuul 1.x는 서블릿 기반의 블로킹 아키텍처로 성능에 한계가 있었다. Spring Cloud Gateway는 리액티브 프로그래밍 모델을 채택하여 더 나은 성능과 유연성을 제공한다.
2.2. 핵심 개념
Spring Cloud Gateway의 동작 방식을 이해하기 위한 핵심 개념은 다음과 같다.
- Route(라우트): 게이트웨이의 기본 구성 요소로, 목적지 URI, 조건(Predicate), 필터(Filter)의 조합으로 정의된다. 요청이 조건과 일치하면 지정된 URI로 라우팅된다.
- Predicate(프레디케이트): 요청이 어떤 조건을 만족하는지 판단하는 조건절이다. HTTP 메서드, 경로, 헤더, 쿼리 파라미터, 시간 등 다양한 조건을 설정할 수 있다.
- Filter(필터): 요청이 라우팅되기 전후에 실행되는 로직이다. 요청이나 응답을 수정하거나, 추가 처리를 수행할 수 있다.
2.3. 동작 흐름
클라이언트 요청이 Spring Cloud Gateway를 통과하는 기본 흐름은 다음과 같다.
- 클라이언트가 게이트웨이로 요청을 전송한다.
- Gateway Handler Mapping이 요청과 일치하는 라우트를 찾는다.
- 해당 라우트에 정의된 프레디케이트 조건을 검증한다.
- 조건이 일치하면, 해당 라우트에 연결된 필터 체인을 실행한다.
- 모든 전처리 필터(Pre-filter)가 실행된 후, 요청이 목적지 서비스로 전달된다.
- 목적지 서비스로부터 응답을 받으면 후처리 필터(Post-filter)가 실행된다.
- 최종 응답이 클라이언트에게 반환된다.
3. First Service, Second Service 추가
API 게이트웨이를 테스트하기 위해 두 개의 간단한 마이크로서비스를 먼저 생성하자.
3.1. First Service 프로젝트 생성
build.gradle (first-service)
plugins {
id 'org.springframework.boot' version '3.2.0'
id 'io.spring.dependency-management' version '1.1.4'
id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
application.yml (first-service)
server:
port: 8081
spring:
application:
name: first-service
FirstServiceController.java
package com.example.firstservice.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/first-service")
public class FirstServiceController {
@GetMapping("/welcome")
public String welcome() {
return "Welcome to First Service";
}
@GetMapping("/message")
public String message(@RequestHeader("first-request") String header) {
log.info("Received header: {}", header);
return "Hello from First Service";
}
@GetMapping("/check")
public Map<String, String> check() {
Map<String, String> response = new HashMap<>();
response.put("service", "first-service");
response.put("status", "OK");
return response;
}
}
3.2. Second Service 프로젝트 생성
build.gradle (second-service) - first-service와 동일한 의존성
application.yml (second-service)
server:
port: 8082
spring:
application:
name: second-service
SecondServiceController.java
package com.example.secondservice.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/second-service")
public class SecondServiceController {
@GetMapping("/welcome")
public String welcome() {
return "Welcome to Second Service";
}
@GetMapping("/message")
public String message(@RequestHeader("second-request") String header) {
log.info("Received header: {}", header);
return "Hello from Second Service";
}
@GetMapping("/check")
public Map<String, String> check() {
Map<String, String> response = new HashMap<>();
response.put("service", "second-service");
response.put("status", "OK");
return response;
}
}
두 서비스를 각각 실행하여 http://localhost:8081/first-service/welcome와 http://localhost:8082/second-service/welcome가 정상 응답하는지 확인한다.
4. Webmvc를 위한 Spring Cloud Gateway
4.1. Spring Cloud Gateway 프로젝트 생성
이제 API 게이트웨이 역할을 할 Spring Cloud Gateway 프로젝트를 생성한다.
build.gradle (apigateway-service)
plugins {
id 'org.springframework.boot' version '3.2.0'
id 'io.spring.dependency-management' version '1.1.4'
id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
repositories {
mavenCentral()
}
ext {
set('springCloudVersion', "2023.0.0")
}
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
중요: Spring Cloud Gateway는 WebFlux 기반으로 동작하므로, spring-boot-starter-web 의존성을 추가하면 충돌이 발생한다. 반드시 spring-cloud-starter-gateway만 포함해야 한다.
4.2. 기본 라우팅 설정 (YAML 방식)
application.yml
server:
port: 8000
spring:
application:
name: apigateway-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/**
logging:
level:
org.springframework.cloud.gateway: DEBUG
이 설정은 다음과 같이 동작한다:
- /first-service/** 경로로 들어오는 요청은 http://localhost:8081로 라우팅된다.
- /second-service/** 경로로 들어오는 요청은 http://localhost:8082로 라우팅된다.
게이트웨이를 실행하고 http://localhost:8000/first-service/welcome에 접속하면 First Service의 응답이, http://localhost:8000/second-service/welcome에 접속하면 Second Service의 응답이 반환되는 것을 확인할 수 있다.
4.3. Java 코드를 이용한 라우팅 설정
YAML 대신 Java Config를 사용하여 라우팅을 설정할 수도 있다.
GatewayConfig.java
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 GatewayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("first-service", r -> r.path("/first-service/**")
.uri("<http://localhost:8081>"))
.route("second-service", r -> r.path("/second-service/**")
.uri("<http://localhost:8082>"))
.build();
}
}
이 경우 application.yml의 spring.cloud.gateway.routes 설정은 제거하거나 주석 처리해야 한다.
5. Webflux를 위한 Spring Cloud Gateway
Spring Cloud Gateway는 내부적으로 WebFlux와 Netty를 사용하여 논블로킹 방식으로 동작한다. 이는 많은 동시 연결을 효율적으로 처리할 수 있게 해준다.
5.1. 리액티브 프로그래밍 모델
Spring Cloud Gateway는 전통적인 서블릿 기반의 블로킹 모델 대신 리액티브 스트림(Reactive Streams)을 기반으로 동작한다. 이는 다음과 같은 이점을 제공한다.
높은 확장성: 적은 수의 스레드로 많은 요청을 처리할 수 있다.
효율적인 리소스 사용: 스레드 블로킹이 최소화되어 CPU와 메모리를 효율적으로 사용한다.
백프레셔(Backpressure) 지원: 데이터 스트림 처리 시 소비자가 처리할 수 있는 만큼만 데이터를 전송한다.
5.2. WebClient를 통한 논블로킹 통신
Spring Cloud Gateway는 서비스 간 통신 시 WebClient를 사용하여 논블로킹 방식으로 요청을 처리한다. 이는 게이트웨이 자체의 성능을 저하시키지 않으면서 백엔드 서비스와 통신할 수 있게 해준다.
// WebClient를 사용한 논블로킹 호출 예시
@Slf4j
@Component
public class CustomGlobalFilter implements GlobalFilter, Ordered {
@Autowired
private WebClient.Builder webClientBuilder;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// WebClient를 사용한 비동기 호출
return webClientBuilder.build()
.get()
.uri("<http://some-service/api/check>")
.retrieve()
.bodyToMono(String.class)
.doOnNext(response -> log.info("Service response: {}", response))
.then(chain.filter(exchange));
}
}
6. Spring Cloud Gateway - Filter 적용
필터는 요청이 라우팅되기 전후에 실행되는 로직이다. Spring Cloud Gateway는 다양한 내장 필터를 제공하며, 사용자 정의 필터도 생성할 수 있다.
6.1. 프리 필터(Pre-filter)와 포스트 필터(Post-filter)
필터는 실행 시점에 따라 두 가지로 구분된다.
- 프리 필터(Pre-filter): 요청이 실제 서비스로 전달되기 전에 실행된다. 요청 헤더 추가, 인증 검사, 요청 로깅 등의 작업을 수행한다.
- 포스트 필터(Post-filter): 서비스로부터 응답을 받은 후 클라이언트에게 반환하기 전에 실행된다. 응답 헤더 추가, 응답 로깅, 에러 처리 등의 작업을 수행한다.
6.2. 내장 필터 적용 예시
application.yml (필터 추가)
spring:
cloud:
gateway:
routes:
- id: first-service
uri: <http://localhost:8081>
predicates:
- Path=/first-service/**
filters:
- AddRequestHeader=first-request, first-request-header-value
- AddResponseHeader=first-response, first-response-header-value
- id: second-service
uri: <http://localhost:8082>
predicates:
- Path=/second-service/**
filters:
- AddRequestHeader=second-request, second-request-header-value
- AddResponseHeader=second-response, second-response-header-value
이 설정은:
- First Service로 가는 요청에 first-request 헤더를 추가하고, 응답에 first-response 헤더를 추가한다.
- Second Service로 가는 요청에 second-request 헤더를 추가하고, 응답에 second-response 헤더를 추가한다.
First Service의 /first-service/message 엔드포인트는 first-request 헤더를 받아 로그로 출력하도록 구현되어 있으므로, 게이트웨이를 통해 요청하면 헤더가 정상적으로 전달되는 것을 확인할 수 있다.
6.3. 자주 사용되는 내장 필터
요청/응답 조작 필터:
- AddRequestHeader: 요청에 헤더 추가
- AddRequestParameter: 요청에 쿼리 파라미터 추가
- AddResponseHeader: 응답에 헤더 추가
- RemoveRequestHeader: 요청에서 헤더 제거
- RemoveResponseHeader: 응답에서 헤더 제거
- SetRequestHeader: 요청 헤더 값 설정
- SetResponseHeader: 응답 헤더 값 설정
- RewritePath: 요청 경로 재작성
라우팅 관련 필터:
- RedirectTo: 지정된 URL로 리다이렉트
- SetPath: 요청 경로 설정
- Retry: 실패한 요청 재시도
- CircuitBreaker: 서킷 브레이커 패턴 적용
7. Spring Cloud Gateway - Custom Filter 적용
내장 필터만으로는 부족한 경우 직접 커스텀 필터를 구현할 수 있다.
7.1. 커스텀 필터 구현 (AbstractGatewayFilterFactory 상속)
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 CustomFilter extends AbstractGatewayFilterFactory<CustomFilter.Config> {
public CustomFilter() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
log.info("Custom PRE filter: request uri -> {}", request.getURI());
// 커스텀 프리 필터 로직
if (config.isPreLogger()) {
log.info("Custom Filter Start: {}", config.getBaseMessage());
}
// 체인의 다음 필터로 요청 전달 (비동기)
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
// 커스텀 포스트 필터 로직
if (config.isPostLogger()) {
log.info("Custom Filter End: {}", config.getBaseMessage());
}
log.info("Custom POST filter: response code -> {}", response.getStatusCode());
}));
};
}
@Data
public static class Config {
private String baseMessage;
private boolean preLogger;
private boolean postLogger;
}
}
7.2. 커스텀 필터 적용
application.yml
spring:
cloud:
gateway:
routes:
- id: first-service
uri: <http://localhost:8081>
predicates:
- Path=/first-service/**
filters:
- name: CustomFilter
args:
baseMessage: First Service Custom Filter
preLogger: true
postLogger: true
- AddRequestHeader=first-request, first-request-header-value
- AddResponseHeader=first-response, first-response-header-value
- id: second-service
uri: <http://localhost:8082>
predicates:
- Path=/second-service/**
filters:
- name: CustomFilter
args:
baseMessage: Second Service Custom Filter
preLogger: true
postLogger: true
- AddRequestHeader=second-request, second-request-header-value
- AddResponseHeader=second-response, second-response-header-value
8. Spring Cloud Gateway - Global Filter 적용
Global Filter는 모든 라우트에 공통으로 적용되는 필터다.
8.1. Global Filter 구현
package com.example.apigatewayservice.filter;
import lombok.extern.slf4j.Slf4j;
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.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Slf4j
@Component
public class GlobalCustomFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
log.info("Global Filter Start: request id -> {}", request.getId());
log.info("Global Filter: request uri -> {}", request.getURI());
log.info("Global Filter: request method -> {}", request.getMethod());
// 요청 처리 시간 측정을 위한 시작 시간 기록
long startTime = System.currentTimeMillis();
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
long endTime = System.currentTimeMillis();
log.info("Global Filter End: response status -> {}", response.getStatusCode());
log.info("Global Filter: 처리 시간 -> {} ms", (endTime - startTime));
}));
}
@Override
public int getOrder() {
// 필터 실행 순서 정의 (낮을수록 먼저 실행)
return -1;
}
}
8.2. Global Filter 실행 순서 제어
Ordered 인터페이스를 구현하거나 @Order 어노테이션을 사용하여 여러 글로벌 필터의 실행 순서를 제어할 수 있다.
@Component
@Order(0)
public class FirstGlobalFilter implements GlobalFilter {
// 가장 먼저 실행됨
}
@Component
@Order(1)
public class SecondGlobalFilter implements GlobalFilter {
// 두 번째로 실행됨
}
9. Spring Cloud Gateway - Logging Filter 적용
로깅은 API 게이트웨이에서 중요한 기능 중 하나다. 요청과 응답의 상세 정보를 로깅하는 필터를 구현해보자.
9.1. Logging 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.OrderedGatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
@Slf4j
@Component
public class LoggingFilter extends AbstractGatewayFilterFactory<LoggingFilter.Config> {
public LoggingFilter() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
// OrderedGatewayFilter를 사용하여 필터 순서 지정
return new OrderedGatewayFilter((exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
// 요청 로깅
if (config.isRequestLogging()) {
log.info("===== Request Logging =====");
log.info("Method: {}", request.getMethod());
log.info("Path: {}", request.getURI().getPath());
log.info("Headers: {}", request.getHeaders());
// 요청 바디 로깅 (리액티브 방식)
if (config.isIncludeRequestBody()) {
return DataBufferUtils.join(exchange.getRequest().getBody())
.flatMap(dataBuffer -> {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
DataBufferUtils.release(dataBuffer);
String body = new String(bytes, StandardCharsets.UTF_8);
log.info("Request Body: {}", body);
// 바디를 다시 exchange에 설정
ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(request) {
@Override
public Flux<DataBuffer> getBody() {
return Flux.just(exchange.getResponse().bufferFactory()
.wrap(bytes));
}
};
return chain.filter(exchange.mutate()
.request(mutatedRequest)
.build());
});
}
}
// 응답 로깅 (체인 필터 실행 후)
if (config.isResponseLogging()) {
// 응답 로깅을 위한 데코레이터 생성
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(response) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (config.isIncludeResponseBody()) {
return DataBufferUtils.join(body)
.flatMap(dataBuffer -> {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
DataBufferUtils.release(dataBuffer);
String responseBody = new String(bytes, StandardCharsets.UTF_8);
log.info("Response Status: {}", getStatusCode());
log.info("Response Body: {}", responseBody);
return getDelegate().writeWith(
Mono.just(getDelegate().bufferFactory().wrap(bytes)));
});
}
return super.writeWith(body);
}
};
return chain.filter(exchange.mutate()
.response(decoratedResponse)
.build());
}
return chain.filter(exchange);
}, config.getOrder());
}
@Data
public static class Config {
private boolean requestLogging = true;
private boolean responseLogging = true;
private boolean includeRequestBody = false;
private boolean includeResponseBody = false;
private int order = 0;
}
}
9.2. Logging Filter 적용
application.yml
spring:
cloud:
gateway:
routes:
- id: first-service
uri: <http://localhost:8081>
predicates:
- Path=/first-service/**
filters:
- name: LoggingFilter
args:
requestLogging: true
responseLogging: true
includeRequestBody: true
includeResponseBody: true
order: -1
- AddRequestHeader=first-request, first-request-header-value
10. Spring Cloud Gateway + Eureka 연동
지금까지는 게이트웨이에서 서비스의 물리적 주소(localhost:8081, localhost:8082)를 직접 지정했다. 하지만 앞서 배운 서비스 디스커버리(Eureka)와 연동하면, 서비스의 논리적 이름만으로 라우팅할 수 있다.
10.1. 의존성 추가
build.gradle (apigateway-service)
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}
10.2. Eureka Client 설정
application.yml
server:
port: 8000
spring:
application:
name: apigateway-service
cloud:
gateway:
routes:
- id: first-service
uri: lb://FIRST-SERVICE # lb: 로드 밸런싱 프로토콜, FIRST-SERVICE는 Eureka에 등록된 서비스 이름
predicates:
- Path=/first-service/**
filters:
- AddRequestHeader=first-request, first-request-header-value
- AddResponseHeader=first-response, first-response-header-value
- id: second-service
uri: lb://SECOND-SERVICE
predicates:
- Path=/second-service/**
filters:
- AddRequestHeader=second-request, second-request-header-value
- AddResponseHeader=second-response, second-response-header-value
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: <http://localhost:8761/eureka/>
instance:
instance-id: ${spring.application.name}:${spring.application.instance_id:${random.value}}
중요: uri에 lb:// 접두사를 사용하면 Spring Cloud Gateway가 Eureka에서 해당 서비스 이름의 인스턴스 목록을 조회하고, 로드 밸런싱을 적용하여 요청을 전달한다.
10.3. 메인 애플리케이션 클래스
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);
}
}
10.4. 서비스 디스커버리 연동 테스트
- Eureka Server 실행
- First Service 실행 (Eureka Client 설정 추가 필요)
- Second Service 실행 (Eureka Client 설정 추가 필요)
- API Gateway 실행
First Service와 Second Service에 Eureka Client 설정을 추가해야 한다.
first-service의 application.yml
server:
port: 8081
spring:
application:
name: first-service
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: <http://localhost:8761/eureka/>
instance:
instance-id: ${spring.application.name}:${spring.application.instance_id:${random.value}}
first-service의 메인 클래스
@SpringBootApplication
@EnableDiscoveryClient
public class FirstServiceApplication {
public static void main(String[] args) {
SpringApplication.run(FirstServiceApplication.class, args);
}
}
10.5. 로드 밸런싱 확인
First Service를 여러 개의 인스턴스로 실행하고(예: 8081, 8083 포트), 게이트웨이를 통해 여러 번 요청을 보내면 요청이 각 인스턴스에 분산되는 것을 확인할 수 있다.
# 여러 번 요청 보내기
for i in {1..10}; do curl <http://localhost:8000/first-service/check>; echo; done
각 요청이 서로 다른 포트의 인스턴스로 전달되는 것을 로그를 통해 확인할 수 있다.
11. 실무 적용 시 고려사항
11.1. 성능 최적화
타임아웃 설정: 백엔드 서비스의 응답 지연에 대비해 적절한 타임아웃을 설정해야 한다.
spring:
cloud:
gateway:
httpclient:
connect-timeout: 5000
response-timeout: 10s
요청 크기 제한: 대용량 요청을 제한하여 서버 리소스를 보호한다.
spring:
codec:
max-in-memory-size: 10MB
11.2. 보안 고려사항
CORS 설정: 크로스 오리진 요청을 허용해야 하는 경우 CORS 설정을 추가한다.
@Bean
public CorsConfiguration corsConfiguration() {
CorsConfiguration corsConfig = new CorsConfiguration();
corsConfig.addAllowedOrigin("<http://localhost:3000>");
corsConfig.addAllowedMethod("*");
corsConfig.addAllowedHeader("*");
corsConfig.setAllowCredentials(true);
corsConfig.setMaxAge(3600L);
return corsConfig;
}
Rate Limiting: 특정 클라이언트의 과도한 요청을 제한한다.
spring:
cloud:
gateway:
routes:
- id: first-service
uri: lb://FIRST-SERVICE
predicates:
- Path=/first-service/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
redis-rate-limiter.requestedTokens: 1
11.3. 고가용성 구성
API 게이트웨이는 시스템의 단일 진입점이므로 고가용성 구성이 필수적이다.
- 여러 게이트웨이 인스턴스를 실행하고 앞단에 로드 밸런서(예: AWS ALB, Nginx)를 배치한다.
- 무상태(Stateless)로 설계하여 어떤 인스턴스로도 요청을 처리할 수 있게 한다.
- 세션 정보가 필요한 경우 Redis 등 외부 저장소를 활용한다.
11.4. 모니터링과 로깅
Actuator를 활성화하여 게이트웨이의 상태를 모니터링한다.
management:
endpoints:
web:
exposure:
include: health,info,metrics,gateway
endpoint:
gateway:
enabled: true
주요 모니터링 지표:
- 라우트별 요청 수
- 응답 시간
- 에러율
- 활성 연결 수
12. 결론
Spring Cloud Gateway는 마이크로서비스 아키텍처에서 필수적인 API 게이트웨이 역할을 수행하는 강력한 도구다. 논블로킹 아키텍처를 기반으로 높은 성능을 제공하며, 다양한 프레디케이트와 필터를 통해 유연한 라우팅 규칙을 구성할 수 있다.
특히 Eureka와의 연동을 통해 동적인 서비스 디스커버리와 로드 밸런싱을 구현할 수 있어, 클라우드 네이티브 환경에 적합하다. 또한 커스텀 필터를 통해 인증, 로깅, 모니터링 등 횡단 관심사를 중앙에서 처리함으로써 각 마이크로서비스의 복잡도를 낮출 수 있다.
다음 글에서는 실제 이커머스 애플리케이션을 구축하면서 지금까지 배운 내용을 종합적으로 적용해볼 예정이다.
'MSA > MSA 기본' 카테고리의 다른 글
| [BASIC #6] Configuration Service와 중앙 설정 관리 (0) | 2025.09.20 |
|---|---|
| [BASIC #5] 인증 처리와 JWT 기반 보안 (0) | 2025.09.20 |
| [BASIC #4][실습] E-commerce MSA 프로젝트 구조 설계 (0) | 2025.09.20 |
| [BASIC #2] Service Discovery (0) | 2025.09.20 |
| [BASIC #1] Microservice와 Spring Cloud의 소개 (0) | 2025.09.20 |
