1. MSA의 관문: API 게이트웨이와 서비스 통신
마이크로서비스 아키텍처(MSA)에서는 시스템이 수많은 독립적인 서비스로 분리된다. 이때 클라이언트(웹, 모바일 앱 등)가 각 서비스의 주소를 모두 알고 직접 통신해야 한다면, 인증, 로깅, 라우팅 등 공통적인 기능들이 중복 구현되고 보안에 매우 취약한 구조가 된다.
API 게이트웨이(API Gateway)는 이러한 문제들을 해결하기 위해 모든 클라이언트의 요청을 받아 처리하는 단일 진입점(Single Point of Entry) 역할을 수행하는 서버다.
2. API 게이트웨이

2.1. 개요
API 게이트웨이는 시스템의 외부에 위치하여, 마치 '회사의 안내 데스크'와 같은 역할을 한다. 외부 방문객은 특정 부서나 직원을 직접 찾아가는 대신, 안내 데스크를 통해 신원을 확인하고 방문 목적에 맞는 부서로 안내받는다. 이처럼 API 게이트웨이는 모든 외부 요청을 대신 받아 인증, 로깅 등 사전 공통 작업을 처리한 뒤, 요청에 맞는 내부 마이크로서비스로 전달(라우팅)하는 역할을 담당한다.
2.2. 핵심 기능
API 게이트웨이는 단순한 요청 중개를 넘어, 시스템 전체의 안정성과 보안을 책임지는 다양한 핵심 기능을 수행한다.
- 라우팅 (Routing): 클라이언트의 요청 경로(Path)나 헤더(Header) 등을 분석하여 적절한 마이크로서비스로 요청을 전달한다.
- 인증 및 인가 (Authentication & Authorization): 모든 요청에 대해 API 키, JWT(JSON Web Token) 등을 검증하여 시스템의 보안을 중앙에서 관리한다.
- 로드 밸런싱 (Load Balancing): 동일한 서비스의 여러 인스턴스로 트래픽을 분산하여 안정성을 확보한다.
- 장애 허용 (Fault Tolerance): 서킷 브레이커, 재시도(Retry), 타임아웃(Timeout) 등의 패턴을 적용하여 특정 서비스의 장애가 시스템 전체로 전파되는 것을 방지한다.
- 로깅 및 모니터링 (Logging & Monitoring): 모든 요청과 응답을 중앙에서 기록하고 추적하여 시스템의 상태를 모니터링한다.
- 기타: 응답 캐싱, 요청/응답 변환, IP 기반 접근 제어 등 다양한 공통 기능을 제공한다.
3. 서비스 간 통신 방식: RestTemplate vs. OpenFeign
API 게이트웨이가 외부와 내부를 연결하는 관문이라면, 내부 마이크로서비스들끼리는 어떻게 통신할까? Spring Cloud 환경에서는 주로 다음 두 가지 기술이 사용된다.
3.1. RestTemplate (전통 방식)
Spring 프레임워크가 제공하는 기본적인 HTTP 클라이언트다. HTTP 요청을 직접 생성하고 응답을 받는 직관적인 구조를 가지고 있다.
// 직접 URL을 생성하여 다른 서비스를 호출
RestTemplate restTemplate = new RestTemplate();
restTEmplate.getForObject("http://localhost:8080/", User.class, 200);
하지만 RestTemplate은 코드가 다소 장황해지고, 서비스 디스커버리와 연동한 동적인 로드 밸런싱을 위해서는 추가적인 설정이 필요하다는 단점이 있다. 현재는 유지보수(Maintenance) 모드로 전환되어 새로운 기능 개발은 중단된 상태다.
3.2. Feign Client (현대 방식)
OpenFeign은 선언적(Declarative) REST 클라이언트다. 개발자는 실제 구현 없이 인터페이스와 어노테이션만으로 다른 서비스를 호출하는 코드를 작성할 수 있다.
// "user-service"라는 이름의 서비스를 찾아 호출하도록 선언
@FeignClient("user-service")
public interface UserServiceClient {
@GetMapping("/users/{userId}")
UserDto getUserById(@PathVariable String userId);
}
OpenFeign은 Eureka와 같은 서비스 디스커버리 클라이언트와 완벽하게 통합된다. 위 코드처럼 서비스의 실제 주소(http://...) 대신 논리적인 서비스 이름(user-service)만 지정하면, Spring Cloud LoadBalancer(Deprecated된 Netflix Ribbon을 대체)가 알아서 해당 서비스의 활성 인스턴스 목록을 찾아 요청을 분산 처리해준다. 이는 RestTemplate에 비해 훨씬 간결하고 유연하며 강력한 서비스 간 통신 방식을 제공한다.
Netflix Ribbon이라는 MSA간 통신 기술도 있었는데, 현재는 Deprecated된 상태이고, 이런 Ribbon을 기반으로 하는 Netflix Zuul 또한 Ribbon이 Deprecated된 시점부터는 동일하게 Deprecated된 상태이다.
4. WebFlux 기반 게이트웨이 vs. Spring MVC 기반 게이트웨이
API 게이트웨이를 구축할 때 Spring 프레임워크 내에서는 크게 두 가지 기술 스택을 선택할 수 있다
- 전통적인 Spring MVC(서블릿 기반)
- 현대적인 Spring WebFlux(리액티브 스택)
두 방식의 가장 근본적인 차이는 '요청을 처리하는 방식'에 있다. 이는 게이트웨이의 성능과 자원 효율성에 결정적인 영향을 미친다.
4.1. Spring MVC 기반 게이트웨이 (동기, 블로킹 방식)
Spring MVC는 '요청 당 스레드 하나(Thread-per-Request)' 모델을 기반으로 동작하는 동기(Synchronous), 블로킹(Blocking) 방식이다.
- 동작 원리:
- 클라이언트로부터 요청이 들어오면, 스레드 풀(Thread Pool)에서 스레드 하나를 할당받는다. 이 스레드는 요청 처리의 전 과정(다른 마이크로서비스를 호출하고 응답을 기다리는 시간(I/O 대기)까지 포함)이 끝날 때까지 다른 일을 하지 않고 대기(Block)한다. 처리가 완료되어야만 스레드는 풀에 반납되고 다음 요청을 받을 수 있다.
- 비유:
- '직원 한 명이 주문 하나를 끝까지 전담하는 카페'와 같다. 직원은 손님의 주문을 받고, 음료가 완성될 때까지 카운터 앞에서 계속 기다린다. 음료를 전달해야만 다음 손님의 주문을 받을 수 있다. 손님이 몰리면 직원(스레드)을 계속 늘려야 하고, 이는 곧 한계에 부딪힌다.
- 특징:
- 장점: 코드가 순차적으로 실행되어 이해하기 쉽고 디버깅이 직관적이다.
- 단점: 요청이 많아질수록 스레드 수가 급증하여 자원 소모가 크고, 컨텍스트 스위칭 비용으로 인해 성능이 저하된다. 특히 I/O 대기가 잦은 API 게이트웨이 역할에는 비효율적이다. (과거 Netflix Zuul 1이 이 방식을 사용했다.)
4.2. WebFlux 기반 게이트웨이 (비동기, 논블로킹 방식)
Spring WebFlux는 '이벤트 루프(Event Loop)' 모델을 기반으로 하는 비동기(Asynchronous), 논블로킹(Non-Blocking) 방식이다. Spring Cloud Gateway가 바로 이 WebFlux를 기반으로 만들어졌다.
- 동작 원리:
- 최소한의 스레드(보통 CPU 코어 수만큼)를 가진 이벤트 루프가 모든 요청을 받는다. 만약 I/O 작업(예: 다른 서비스 호출)이 필요하면, 스레드는 그 작업이 끝날 때까지 기다리지 않는다. 대신, 작업 완료 시 실행될 콜백(Callback) 함수를 등록하고 즉시 다른 요청을 처리하러 간다. 나중에 I/O 작업이 완료되면, 이벤트 루프는 등록된 콜백 함수를 실행하여 응답을 마무리한다.
- 비유:
- '여러 직원이 역할을 분담하는 효율적인 카페'와 같다. 한 직원은 주문만 계속 받고, 주문서를 주방에 전달한 뒤 바로 다음 손님의 주문을 받는다. 주방에서는 음료를 만들고, 음료가 완성되면 다른 직원이 손님에게 전달한다. 어떤 직원도 다음 단계가 끝날 때까지 멍하니 서서 기다리지 않으므로, 적은 수의 직원(스레드)으로 훨씬 많은 손님(요청)을 처리할 수 있다.
- 특징:
- 장점: 적은 스레드로 매우 높은 동시 처리량을 감당할 수 있어 자원 효율성이 극대화된다. I/O 작업이 많은 API 게이트웨이에 가장 이상적인 모델이다.
- 단점: 비동기 콜백 기반이므로 코드의 흐름이 순차적이지 않아 상대적으로 복잡하고 디버깅이 까다로울 수 있다. (리액티브 프로그래밍에 대한 이해 필요)
4.3. 결론: 어떤 것을 선택해야 하는가?
| 구분 | Spring MVC (Servlet) |
Spring WebFlux (Reactive)
|
| 동작 모델 | 동기, 블로킹 | 비동기, 논블로킹 |
| 스레드 모델 | 요청 당 스레드 1:1 할당 |
이벤트 루프 (적은 수의 스레드)
|
| 자원 효율성 | 낮음 (높은 스레드 비용) | 매우 높음 |
| 성능 | 동시 요청 수에 한계 명확 |
높은 동시성 처리량에 최적화
|
| 주요 구현체 | (구) Netflix Zuul 1 |
Spring Cloud Gateway
|
5. Gateway 실습(1): Webmvc Gateway
GitHub - joneconsulting/new-toy-msa
Contribute to joneconsulting/new-toy-msa development by creating an account on GitHub.
github.com
5.1. 모듈 생성 (apigateway-service)



5.2. application.yml 설정
server:
port: 8000
spring:
application:
name: apigateway-service
cloud:
gateway:
server:
webmvc:
routes:
- id: first-service
uri: http://localhost:8081/
predicates:
- Path=/first-service/**
- id: second-service
uri: http://localhost:8082/
predicates:
- Path=/second-service/**
5.3. 실행
아래와 같은 순서로 실행해야 한다.
- Service Discovery (Eureka Server)
- APIGateway Service
- First Service
- Second Service
5.4. 웹 브라우저 결과 확인
1) first-service 확인 및 second-service 확인 (apigateway service 없을 때 부터 잘 됐었음)


2) apigateway service 사용 ★
url을 보면 8081이나 8082가 아닌, 8000으로 요청을 보내고 있지만, Path에 정해진 규칙에 따라 first-service, second-service로 요청이 보내지고 있음을 확인할 수 있다.


6. Gateway 실습(2): Webflux Gateway
6.1. dependencies 추가
dependencies {
...
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
}
6.2. application.yml 설정 (단, 이번에는 Eureka 연동까지 포함)
server:
port: 8000
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8761/eureka
spring:
application:
name: apigateway-service
cloud:
gateway:
server:
webflux:
routes:
- id: first-service
uri: http://localhost:8081/
predicates:
- Path=/first-service/**
- id: second-service
uri: http://localhost:8082/
predicates:
- Path=/second-service/**
6.3. 실행
아래와 같은 순서로 실행해야 한다.
- Service Discovery (Eureka Server)
- APIGateway Service
- First Service
- Second Service

6.4. 웹 브라우저 결과 확인



참고 사항)
동기(WebMVC) 기반의 Gateway 설정에서는 Eureka를 연동하지 않았어도 라우팅이 제대로 되는것을 확인했다. 반면 비동기(Webflux) 기반의 Gateway 설정에서 Eureka를 연동했지만, 단순히 유레카 대시보드에 등록되는것 말고는 딱히 추가적인 기능이 없어보인다.
따라서 이때, "아니 그러면 굳이 Eureka 연동해야돼? 라는 의문이 생길 수 있다. 지금은 그냥 "아, Eureka 연동을 하든 안하든 gateway를 통해서 각 서비스로 라우팅이 잘 되는구나. 대신에 Eureka랑 연동하면 Eureka 대시보드에 Gateway가 뜨는구나" 정도로만 이해하고 넘어가자
Gateway와 Eureka를 연동하는 이유를 지금 설명하기에는 적절치 않다. 따라서 Gateway에서 왜 Eureka를 연동하는지는 아래에 12를 참고하자.
7. (Webflux) Gateway Filter
7.1. Gateway Filter란?
API Gateway는 이전에 다루었던 클라이언트의 요청을 적절한 서비스로 중계하는 라우팅(Routing) 기능 외에도, 요청과 응답을 동적으로 제어하는 강력한 필터(Filter) 기능을 제공한다.
필터는 '공항의 보안 검색대'에 비유할 수 있다. 마이크로서비스에 요청이 도달하기 전후에 공통적인 추가 작업을 처리하는 역할을 수행하는데, 이는 마치 비행기 탑승 전 승객의 신원을 확인하거나 수하물에 특정 정보를 추가하는 과정과 유사하다.
7.2. 요청 처리 흐름과 필터의 위치

클라이언트의 요청이 API Gateway를 통과하는 과정에서 필터는 다음과 같이 동작한다.
- 요청 접수: 클라이언트가 API Gateway로 요청을 보낸다.
- 조건 검사 (Predicate): 게이트웨이는 요청 경로(Path) 등의 조건이 어떤 라우팅 규칙과 일치하는지 판단한다.
- 사전 필터 (Pre-filter): 일치하는 서비스로 요청을 보내기 전, 필터가 동작한다. (예: 인증 토큰 검사, 요청 헤더 추가)
- 라우팅 (Routing): 필터를 통과한 요청이 실제 마이크로서비스(first-service 등)로 전달된다.
- 사후 필터 (Post-filter): 마이크로서비스로부터 응답을 받은 후, 클라이언트에게 최종 전달하기 전 필터가 다시 동작한다. (예: 응답 헤더 추가, 응답 시간 로깅)
- 최종 응답: 모든 과정을 거친 응답이 클라이언트에게 전달된다.
7.3. Pre-filter와 Post-filter
필터는 실행 시점에 따라 사전 필터(Pre-filter)와 사후 필터(Post-filter)로 나뉜다.
- 사전 필터 (Pre-filter): 마이크로서비스로 요청이 라우팅되기 전에 실행된다. 주로 다음과 같은 '사전 처리' 용도로 사용된다.
- 인증 및 인가: 요청 헤더에 있는 JWT 같은 인증 토큰의 유효성을 검사한다.
- 로깅: 시스템에 들어오는 모든 요청 정보를 기록하여 추적한다.
- 요청 변형: 모든 요청에 특정 헤더를 추가하거나 일부 내용을 수정한다.
- 사후 필터 (Post-filter): 마이크로서비스에서 응답을 받은 후 클라이언트에게 전달되기 전에 실행된다. 다음과 같은 '사후 처리' 에 유용하다.
- 응답 변형: 모든 응답에 공통 헤더(예: X-Response-Time)를 추가한다.
- 로깅: 응답 상태 코드나 총 처리 시간을 기록한다.
- 에러 형식 통일: 서비스마다 다른 형식의 오류 응답을 일관된 형태로 가공하여 클라이언트에게 반환한다.
8. Gateway Filter 구현 실습 수행(1): @Configuration
이 실습의 핵심은 API Gateway가 어떻게 필터(Filter)를 사용해 각 마이크로서비스로 들어가는 요청(Request)과 나가는 응답(Response)에 추가적인 정보를 삽입하는지, 그리고 마이크로서비스는 그 정보를 어떻게 활용하는지를 이해하는 것이다.
Filter 구현 방법에는 @Configuration을 통한 필터링과 application.yml과 같은 설정 파일을 통한 필터링 방식이 있다. 이번에는 @Configuration을 통한 필터링에 대해 알아보자.
1) FilterConfig.java
API Gateway에 라우팅 규칙과 각 규칙에 적용될 필터를 Java 코드로 직접 정의하는 설정 파일이다.
@Configuration
public class FilterConfig {
// application.yml의 환경 변수를 가져오기 위해 주입 (이 코드에서는 직접 사용되진 않음)
Environment env;
public FilterConfig(Environment env) {
this.env = env;
}
@Bean
public RouteLocator getRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
// 1. 첫 번째 라우팅 규칙 정의
.route(r -> r.path("/first-service/**") // "r"은 RoutePredicateSpec 객체
.filters(f -> f.addRequestHeader("f-request", "1st-request-header-by-java") // "f"는 GatewayFilterSpec 객체
.addResponseHeader("f-response", "1st-response-header-from-java"))
.uri("http://localhost:8081"))
// 2. 두 번째 라우팅 규칙 정의
.route(r -> r.path("/second-service/**")
.filters(f -> f.addRequestHeader("s-request", "2nd-request-header-by-java")
.addResponseHeader("s-response", "2nd-response-header-from-java"))
.uri("http://localhost:8082"))
.build();
}
}
- @Configuration: 이 클래스가 Spring의 설정 파일임을 나타낸다. Spring 컨테이너는 이 클래스를 스캔하여 @Bean으로 정의된 객체들을 관리한다.
- @Bean과 RouteLocator: RouteLocator는 Spring Cloud Gateway의 라우팅 정보를 담는 핵심 객체이다. 이 메서드는 RouteLocator 객체를 생성하여 Spring Bean으로 등록하며, 여기에 정의된 규칙대로 게이트웨이가 동작하게 된다.
- 람다(Lambda) 표현식 r -> ...: route() 메서드는 라우팅 규칙 하나를 정의한다. 여기서 r -> r.path(...)는 람다 표현식으로, 코드를 간결하게 표현하기 위해 사용된다. r은 라우팅의 조건을 설정하는 객체(RoutePredicateSpec)이다.
- .path("/first-service/**"): (조건) 클라이언트의 요청 경로가 /first-service/로 시작하면 이 규칙을 적용하라는 의미이다.
- 필터 설정 f -> ...: .filters() 메서드는 해당 경로에 적용할 필터들을 정의한다. 여기서 f는 필터를 설정하는 객체(GatewayFilterSpec)이다.
- .addRequestHeader(...): (사전 필터 - Pre-filter) 이 필터는 클라이언트의 요청을 /first-service로 보내기 전에 요청 헤더를 추가한다.
- 사용 이유: 모든 요청에 공통적인 정보(예: 요청 추적 ID, 인증 정보 등)를 삽입하여, 각 마이크로서비스가 일관된 데이터를 받도록 하기 위함이다. 여기서는 "f-request"라는 이름의 헤더를 추가했다.
- .addResponseHeader(...): (사후 필터 - Post-filter) 이 필터는 /first-service로부터 응답을 받은 후 최종 클라이언트에게 응답을 보내기 전에 응답 헤더를 추가한다.
- 사용 이유: 응답에 대한 공통 정보(예: 게이트웨이 버전, 처리 시간 등)를 추가하여 클라이언트나 다른 시스템이 활용할 수 있도록 하기 위함이다.
- .addRequestHeader(...): (사전 필터 - Pre-filter) 이 필터는 클라이언트의 요청을 /first-service로 보내기 전에 요청 헤더를 추가한다.
2) FirstServiceController.java
first-service의 API 엔드포인트를 정의하는 컨트롤러이다. 게이트웨이가 추가한 헤더를 직접 받아 처리하는 로직이 포함되어 있다.
@RestController
@RequestMapping("/first-service")
@Slf4j
public class FirstServiceController {
// ... 생성자 생략 ...
// ... welcome() 메서드 생략 ...
@GetMapping("/message")
public String message(@RequestHeader("f-request") String header) {
log.info(header); // 게이트웨이가 추가한 헤더 값을 로그로 출력
return "Hello World in First Service.";
}
// ... check() 메서드 생략 ...
}
@RequestHeader("f-request") String header: 이 부분이 핵심이다.
- @RequestHeader 어노테이션: Spring MVC가 HTTP 요청의 헤더 값을 파라미터로 직접 매핑해주는 기능이다.
- 사용 이유: API Gateway의 FilterConfig에서 addRequestHeader("f-request", ...)를 통해 추가한 헤더 값을 이 컨트롤러에서 직접 받기 위해 사용된다. 만약 클라이언트가 게이트웨이를 거치지 않고 이 서비스로 직접 요청하면 "f--request" 헤더가 없으므로 에러가 발생한다. 즉, 이 코드는 **"이 요청은 반드시 게이트웨이를 통해 들어왔을 것이며, 게이트웨이가 붙여준 'f-request' 헤더 값을 사용하겠다"**는 의미를 내포한다.
- log.info(header); 라인을 통해, 게이트웨이가 설정한 "1st-request-header-by-java"라는 값이 성공적으로 출력되는 것을 확인할 수 있다.
3) SecondServiceController.java
second-service의 컨트롤러로, first-service와 동일한 구조를 가진다.
@RestController
@RequestMapping("/second-service")
@Slf4j
public class SecondServiceController {
// ... welcome(), check() 메서드 생략 ...
@GetMapping("/message")
public String message(@RequestHeader("s-request") String header) {
log.info(header);
return "Hello World in Second Service.";
}
}
FirstServiceController와 동일한 원리로 동작한다. 게이트웨이의 두 번째 라우팅 규칙에 의해 추가된 "s-request" 헤더 값을 @RequestHeader를 통해 받아 로그로 출력한다. 이를 통해 각 라우팅 규칙별로 서로 다른 필터가 독립적으로 적용됨을 확인할 수 있다.
8.5. Gateway Filter 구현 실습 결과
1) 8081, 8082 직접 접근 (=Header 정보 없음)

first-service

second-service
📌 만약 8081, 8082 직접 접근을 가능하게 하려면 POSTMAN에서 Header 값으로 f-request 혹은 s-request를 직접 넣어서 요청하면 된다.
2) 8000 게이트웨이 접근 (=Header 정보 있음)


9. Gateway Filter 구현 실습 수행(2): application.yml
1) application.yml
server:
port: 8000
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8761/eureka
spring:
application:
name: apigateway-service
cloud:
gateway:
server:
webflux:
routes:
- id: first-service
uri: http://localhost:8081/
predicates:
- Path=/first-service/**
filters:
- AddRequestHeader=f-request, 1st-request-header-by-yaml
- AddResponseHeader=f-response, 1st-response-header-from-yaml
- id: second-service
uri: http://localhost:8082/
predicates:
- Path=/second-service/**
filters:
- AddRequestHeader=s-request, 2nd-request-header-by-yaml
- AddResponseHeader=s-response, 2nd-response-header-from-yaml
2) Java 코드 수정
FilterConfig.java에서 @Configuration과 @Bean 주석처리 필요
3) 실행
실행해보면 7.Gateway Filter 구현 실습 수행(1): @Configuration의 결과값과 동일하다.
10. CustomFilter
- 흐름은 요청 -> PreFilter -> 화면 출력 -> PostFilter
- 지금 react gateway(비동기)라서 servlet 사용 못함 > HttpRequest, HttpResponse 사용 못함 > exchange를 통해 reactive.ServerHttpRequest, Response로 받아서 처리하고 있음
10.1. Java Code
1) CustomFilter.java
@Component
@Slf4j
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();
// Custom Pre Filter
log.info("Custom PRE Filter: request id -> {}", request.getId());
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
// Custom Post Filter
log.info("Custom POST Filter: response code -> {}", response.getStatusCode());
}));
};
}
public static class Config {
}
}
2) application.yml
server:
port: 8000
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8761/eureka
spring:
application:
name: apigateway-service
cloud:
gateway:
server:
webflux:
default-filters:
- name: GlobalFilter
args:
baseMessage: Spring Cloud Gateway WebFlux Global Filter
preLogger: true
postLogger: true
routes:
- id: first-service
uri: lb://MY-FIRST-SERVICE
predicates:
- Path=/first-service/**
filters:
# - AddRequestHeader=f-request, 1st-request-header-by-yaml
# - AddResponseHeader=f-response, 1st-response-header-from-yaml
- CustomFilter
- id: second-service
uri: lb://MY-SECOND-SERVICE
predicates:
- Path=/second-service/**
filters:
# - AddRequestHeader=s-request, 2nd-request-header-by-yaml
# - AddResponseHeader=s-response, 2nd-response-header-from-yaml
- name: CustomFilter
- name: LoggingFilter
args:
baseMessage: Hi, there.
preLogger: true
postLogger: true
10.2. 웹 브라우저 및 로그 결과 확인
`http://127.0.0.1:8000/first-service/check` 주소로 접속한 다음에, ApigatewayServiceApplication의 로그 기록을 확인해보면 아래와 같이 Prefilter와 Postfilter의 Log가 잘 출력되는것을 볼 수 있다.


11. Global Filter
11.1. Java Code
1) GlobalFilter.java
@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(), request.getRemoteAddress());
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;
}
}
2) application.yml
server:
port: 8000
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8761/eureka
spring:
application:
name: apigateway-service
cloud:
gateway:
server:
webflux:
default-filters: # ✅ Global Filter
- name: GlobalFilter
args:
baseMessage: Spring Cloud Gateway WebFlux Global Filter
preLogger: true
postLogger: true
routes:
- id: first-service
uri: lb://MY-FIRST-SERVICE
predicates:
- Path=/first-service/**
filters:
# - AddRequestHeader=f-request, 1st-request-header-by-yaml
# - AddResponseHeader=f-response, 1st-response-header-from-yaml
- CustomFilter
- id: second-service
uri: lb://MY-SECOND-SERVICE
predicates:
- Path=/second-service/**
filters:
# - AddRequestHeader=s-request, 2nd-request-header-by-yaml
# - AddResponseHeader=s-response, 2nd-response-header-from-yaml
- name: CustomFilter
- name: LoggingFilter
args:
baseMessage: Hi, there.
preLogger: true
postLogger: true
12. Logging Filter
12.1. Java Code
1) LoggingFilter.java
@Component
@Slf4j
public class LoggingFilter extends AbstractGatewayFilterFactory<LoggingFilter.Config> {
public LoggingFilter() { super(Config.class); }
// @Override
// public GatewayFilter apply(Config config) {
// return (exchange, chain) -> {
// ServerHttpRequest request = exchange.getRequest();
// ServerHttpResponse response = exchange.getResponse();
//
// log.info("Logging Filter baseMessage: {}, {}", config.getBaseMessage(), request.getRemoteAddress());
//
// if (config.isPreLogger()) {
// log.info("Logging Filter Start: request uri -> {}", request.getURI().toString());
// }
//
// return chain.filter(exchange).then(Mono.fromRunnable(() -> {
// if (config.isPostLogger()) {
// log.info("Logging Filter End: response code -> {}", response.getStatusCode());
// }
// }));
// };
// }
/* 우선 순위를 갖는 Logging Filter 적용 */
@Override
public GatewayFilter apply(Config config) {
GatewayFilter filter = new OrderedGatewayFilter((exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
log.info("Logging Filter baseMessage: {}, {}", config.getBaseMessage(), request.getRemoteAddress());
if (config.isPreLogger()) {
log.info("Logging Filter Start: request uri -> {}", request.getURI().toString());
}
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
if (config.isPostLogger()) {
log.info("Logging Filter End: response code -> {}", response.getStatusCode());
}
}));
}, OrderedGatewayFilter.HIGHEST_PRECEDENCE); // ✅ 우선순위 설정
return filter;
}
@Data
public static class Config {
private String baseMessage;
private boolean preLogger;
private boolean postLogger;
}
}
2) application.yml
server:
port: 8000
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8761/eureka
spring:
application:
name: apigateway-service
cloud:
gateway:
server:
webflux:
default-filters:
- name: GlobalFilter
args:
baseMessage: Spring Cloud Gateway WebFlux Global Filter
preLogger: true
postLogger: true
routes:
- id: first-service
uri: lb://MY-FIRST-SERVICE
predicates:
- Path=/first-service/**
filters:
# - AddRequestHeader=f-request, 1st-request-header-by-yaml
# - AddResponseHeader=f-response, 1st-response-header-from-yaml
- CustomFilter
- id: second-service
uri: lb://MY-SECOND-SERVICE
predicates:
- Path=/second-service/**
filters:
# - AddRequestHeader=s-request, 2nd-request-header-by-yaml
# - AddResponseHeader=s-response, 2nd-response-header-from-yaml
- name: CustomFilter
- name: LoggingFilter # ✅ LoggingFilter
args:
baseMessage: Hi, there.
preLogger: true
postLogger: true
13. API Gateway와 Eureka 연동: 동적 라우팅 이론 ★
13.1. 기존 방식의 문제점
우리의 API Gateway는 http://localhost:8081과 같이 하드코딩된 주소로 서비스를 찾아가는 정적 라우팅(Static Routing) 방식을 사용했다. 이 방식은 클라이언트가 게이트웨이의 주소(예: 8000번 포트)만 알면 된다는 장점이 있지만, 서비스의 IP나 포트가 변경되거나 인스턴스가 추가될 때마다 게이트웨이 설정을 수동으로 변경하고 재시작해야 하는 한계가 명확하다.
이 문제를 해결하기 위해 Eureka와 연동하여 서비스의 실제 주소가 아닌 논리적인 서비스 이름(Service ID)을 기반으로 요청을 전달하는 동적 라우팅(Dynamic Routing)을 구현한다.
13.2. 게이트웨이의 Eureka 정보 활용 방식
게이트웨이가 클라이언트의 모든 요청마다 Eureka 서버에 주소를 묻는 것은 비효율적이다. 실제로는 게이트웨이 또한 하나의 Eureka 클라이언트로서, 주기적으로 Eureka 서버로부터 전체 서비스의 등록 정보(Registry)를 가져와 자신의 내부에 캐싱(caching)한다. 클라이언트 요청이 들어오면, 게이트웨이는 이 캐시된 정보를 조회하여 매우 빠르게 실제 서비스의 주소를 찾아 라우팅을 수행한다.
14. API Gateway와 Eureka 연동: 동적 라우팅 구현 ★
14.1. 모든 서비스를 Eureka Client로 등록
동적 라우팅을 위해서는 요청을 보내는 API Gateway와 요청을 받는 first-service, second-service 모두가 Eureka 서버에 자신의 정보를 등록하는 클라이언트여야 한다. 각 서비스의 pom.xml에 Eureka 클라이언트 의존성을 추가하고, application.yml에 Eureka 서버의 위치 정보를 설정한다.
1) pom.xml 의존성 추가
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2) application.yml 클라이언트 설정
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8761/eureka
14.2. Gateway의 라우팅 방식 변경 (uri 수정)
게이트웨이가 Eureka에 등록된 서비스 이름을 기반으로 라우팅하도록 apigateway-service의 application.yml 파일을 수정한다.
기존의 uri: http://... 설정을 uri: lb://SERVICE-NAME으로 변경한다.
- uri: 라우팅할 최종 목적지를 지정한다.
- lb://: Load Balancer의 약자로, Spring Cloud Gateway가 Eureka와 같은 서비스 디스커버리 클라이언트와 통합하여 동작하도록 하는 프로토콜이다. 게이트웨이는 lb:// 뒤에 오는 서비스 이름(MY-FIRST-SERVICE)을 Eureka에서 찾아 해당 서비스의 실제 IP와 포트 목록으로 변환하고, 그중 하나로 요청을 라우팅(및 로드 밸런싱)한다.
spring:
cloud:
gateway:
routes:
- id: first-service
uri: lb://MY-FIRST-SERVICE # ✅ 'my-first-service'라는 이름으로 등록된 서비스를 찾음
predicates:
- Path=/first-service/**
- id: second-service
uri: lb://MY-SECOND-SERVICE # ✅ 'my-second-service'라는 이름으로 등록된 서비스를 찾음
predicates:
- Path=/second-service/**
14.3. 웹 브라우저 결과 확인
이제 first-service나 second-service가 임의의 포트(예: 56303)로 실행되더라도, 게이트웨이는 Eureka를 통해 해당 서비스의 동적인 주소를 정확히 찾아내어 정상적으로 요청을 라우팅할 수 있게 된다. 이를 통해 서비스의 주소 변경이나 수평 확장(Scale-out)에 유연하게 대처할 수 있는 MSA 아키텍처가 완성된다.



'MSA > MSA 기본' 카테고리의 다른 글
| [11] MSA: Service Discovery vs. API Gateway (0) | 2025.09.21 |
|---|---|
| [9] MSA: Service Discovery (0) | 2025.09.20 |
| [8] MSA: Spring Cloud (0) | 2025.09.20 |
| [7] MSA: MSA 😎 수정 필요 (0) | 2025.09.20 |
| [6] MSA: SOA와 MSA (0) | 2025.09.20 |
