하나의 거대한 프로젝트(Monolithic)로 시작했던 쇼핑몰이 대박을 터뜨렸습니다. 개발자가 100명이 넘어가자, 코드가 꼬이고 배포 한 번 하려면 30분이 걸립니다. 결국 넷플릭스(Netflix)나 우아한형제들처럼 시스템을 '회원(Auth)', '상품(Product)', '주문(Order)' 서버로 잘게 쪼개는 마이크로서비스 아키텍처(MSA, Microservice Architecture)로 전환했습니다. 그런데 문제가 생겼습니다. 프론트엔드 앱이 회원가입을 하려면 auth.api.com을 찔러야 하고, 상품을 조회하려면 product.api.com을 찔러야 합니다. 클라이언트가 수십 개의 분산된 서버 주소를 전부 외우고 다녀야 하는 끔찍한 상황입니다. 게다가 JWT 토큰 검증은 30개의 서버마다 각각 중복해서 코드를 짜야 할까요? "클라이언트는 딱 하나의 주소(api.com)만 호출해! 내가 주소를 보고 알맞은 서버로 토스해 줄게. 그리고 토큰 검사 같은 공통 작업도 내가 여기서 한 번에 다 처리해 주마!" 이 위대한 단일 진입점(Single Point of Entry)의 역할을 수행하는 것이 바로 Spring Cloud Gateway(SCG)입니다.
이번 [Spring Boot 실무 완벽 가이드] 시리즈에서는 "MSA의 필수 관문!" SCG의 라우팅(Routing)과 글로벌 필터(Filter) 아키텍처에 대하여 다뤄보겠습니다.

🔀 1. 라우팅(Routing): 트래픽의 교통경찰
Spring Cloud Gateway 프로젝트를 새로 하나 만듭니다. (이 서버에는 WebFlux가 내장되어 있어, 수만 건의 트래픽을 멈춤(Blocking) 없이 비동기로 아주 가볍게 중계할 수 있습니다.) 이 서버의 역할은 비즈니스 로직이 아니라 오직 '길 안내'입니다. application.yml에 교통 규칙만 적어주면 됩니다.

spring:
cloud:
gateway:
routes:
# 1. 주문 서비스 라우팅
- id: order-service
uri: http://localhost:8082 # 클라이언트가 여기로 가길 원하면, 진짜 목적지는 8082(주문 서버)다!
predicates:
- Path=/order/** # 만약 주소가 "/order/"로 시작한다면 일로 보내라 (조건)
# 2. 회원 서비스 라우팅
- id: auth-service
uri: http://localhost:8081
predicates:
- Path=/auth/**
이제 클라이언트는 http://gateway.com/order/1 하나만 찌르면, Gateway가 알아서 8082 포트에 떠 있는 주문 서버로 요청을 토스해 줍니다.
🛡️ 2. 글로벌 필터(Global Filter): 중복 코드의 구원자
MSA의 가장 큰 골칫거리는 '인증(Authentication)'입니다. 사용자가 물건을 사려면 JWT 토큰이 유효한지 검사해야 합니다. 30개의 서버 각각에 Spring Security를 달고 토큰 검증 코드를 복붙해야 할까요? 아닙니다. 클라이언트의 요청이 무조건 통과하는 유일한 관문, Gateway에 딱 한 번만 필터(Filter)를 달아서 검사하면 됩니다.
// Gateway에서 동작하는 커스텀 인가 필터 (WebFlux 기반이라 HttpServletRequest 대신 ServerWebExchange 사용)
@Component
@Slf4j
public class JwtAuthorizationFilter extends AbstractGatewayFilterFactory<JwtAuthorizationFilter.Config> {
public JwtAuthorizationFilter() { super(Config.class); }
public static class Config { } // 필터 설정값 넣는 용도
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
// 1. HTTP 헤더에서 Authorization 토큰 꺼내기
if (!request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) {
return onError(exchange, "토큰이 없습니다!", HttpStatus.UNAUTHORIZED);
}
String token = request.getHeaders().get(HttpHeaders.AUTHORIZATION).get(0);
// 2. 토큰 위조/만료 검증 (예: JwtUtil.validateToken(token))
if (!isValid(token)) {
return onError(exchange, "가짜 토큰입니다!", HttpStatus.UNAUTHORIZED);
}
// 3. 통과! 이제 뒷단의 진짜 서버(주문, 상품 등)로 요청을 넘겨준다.
return chain.filter(exchange);
};
}
}
위 필터를 만들고 yml 설정에 filters: - JwtAuthorizationFilter 한 줄만 추가하면, 뒤에 숨은 30개의 마이크로서비스는 "내게 오는 요청은 이미 Gateway가 검사 끝낸 안전한 요청이야!"라고 믿고 오직 비즈니스 로직에만 집중할 수 있습니다.
🎯 3. 마무리 및 다음 단계
지금까지 수십 개로 찢어진 마이크로서비스 환경에서 클라이언트에게는 단일 API 주소의 편리함을 제공하고, 서버 측에는 인가(Auth), 로깅, Rate Limit(트래픽 제한) 같은 공통 관심사를 한 방에 해결해 주는 MSA의 절대 군주, Spring Cloud Gateway의 원리에 대해 아주 치밀하게 다루어 보았습니다. 이제 거대한 아키텍처의 교통정리가 완료되었습니다.
그런데 엄청난 문제가 하나 남아있습니다. Gateway의 yml 설정 파일에 uri: http://192.168.0.10:8082 처럼 뒷단 서버의 IP를 하드코딩해 두었습니다. 클라우드(AWS, K8s) 시대에는 주문 서버에 트래픽이 몰리면 자동으로 주문 서버가 2대, 3대, 10대로 늘어납니다(오토스케일링). 각 서버는 켜질 때마다 IP 주소가 랜덤으로 바뀌는데, Gateway는 어떻게 새로 켜진 서버의 IP를 알아내서 트래픽을 분산(로드밸런싱)해 줄까요? 매번 yml 파일을 고치고 Gateway를 재시작해야 할까요? 이어지는 38단계 포스팅에서는 "MSA 서버들의 실시간 전화번호부!" IP가 수시로 바뀌는 클라우드 환경에서 살아남기 위한 Eureka(유레카) Service Discovery 아키텍처 완벽 가이드에 대해 아주 뼈 때리게 파헤쳐 보겠습니다!
'Framework > Spring Boot' 카테고리의 다른 글
| [Spring Boot] Resilience4j 서킷 브레이커: 장애의 도미노를 끊어라 (0) | 2026.08.05 |
|---|---|
| [Spring Boot] Eureka 서비스 디스커버리 (0) | 2026.08.05 |
| [Spring Boot] ELK 스택 (Elasticsearch, Logstash, Kibana): 중앙 집중형 로그 파이프라인 (0) | 2026.08.05 |
| [Spring Boot] Elasticsearch 연동: RDB의 LIKE 검색 한계를 부수다 (0) | 2026.08.05 |
| [Spring Boot] Spring Batch Chunk 지향 처리: 1,000만 건 메모리 최적화의 꽃 (0) | 2026.08.05 |