
이전 포스팅에서 서비스 인스턴스의 정보를 관리하는 서비스 디스커버리에 대해서 정리해봤다.
이번에는 이 정보를 사용하여 여러가지 작업을 수행하는 API Gateway에 대해서 정리해보려고 한다 👀
API Gateway는 MSA 환경에서 클라이언트와 마이크로서비스 사이에 위치하여 모든 API 요청의 단일 진입점 역할을 하는 컴포넌트이다.
간단하게 구조도를 그려보면 다음과 같다.

즉, Client는 서비스 인스턴스의 실제 주소를 바라보는 것이 아니라 오직 API Gateway만 바라보게 된다.
이후, API Gateway는 Service Discovery(Eureka Server)로부터 서비스의 위치를 파악하고 API 요청을 적절하게 라우팅해주는 역할을 수행한다.
그렇다면 Gateway는 어떻게 사용할 수 있을까?

우선 사진과 같이 Gateway 의존성을 추가하여 프로젝트를 설정하면서 시작하자
Gateway도 마찬가지로 설정 파일에서 대부분의 작업이 수행된다.
설정 파일에서는 라우팅 정보를 관리하며, 필요시 필터 정보도 추가할 수 있다.
우선 설정 파일을 살펴보자
Sample Code
server: port: 8000 spring: application: name: api-gateway cloud: gateway: routes: - id: user-service uri: lb://USER-SERVICE predicates: - Path=/user-service/login - Method=POST filters: - RemoveRequestHeader=Cookie - RewritePath=/user-service/(?<segment>.*), /$\{segment} - id: user-service uri: lb://USER-SERVICE predicates: - Path=/user-service/users - Method=POST filters: - RemoveRequestHeader=Cookie - RewritePath=/user-service/(?<segment>.*), /$\{segment} - id: user-service uri: lb://USER-SERVICE predicates: - Path=/user-service/actuator/** - Method=GET,POST filters: - RemoveRequestHeader=Cookie - RewritePath=/user-service/(?<segment>.*), /$\{segment} - id: user-service uri: lb://USER-SERVICE predicates: - Path=/user-service/** - Method=GET filters: - RemoveRequestHeader=Cookie - RewritePath=/user-service/(?<segment>.*), /$\{segment} - AuthorizationHeaderFilter - id: order-service uri: lb://ORDER-SERVICE predicates: - Path=/order-service/** filters: - RewritePath=/order-service/(?<segment>.*), /$\{segment} default-filters: - name: GlobalFilter args: baseMessage: Spring Cloud Gateway Global Filter preLogger: true postLogger: true eureka: client: register-with-eureka: true fetch-registry: true service-url: defaultZone: http://localhost:8761/eureka
샘플 코드가 조금 폭력적(?)일수도 있으나 천천히 살펴보자
우선, API Gateway 서버도 서비스 디스커버리에 정보를 추가하도록 eureka 관련 설정을 진행하고 있다.
다만, 여기서 eureka.client.fetch-registry 설정을 true로 해야만 라우팅이 가능하다는 것을 기억하자
그리고 이어서 중요한 설정 정보를 정리하면 다음과 같다.
server.port
- API Gateway가 사용할 포트 번호
spring.application.name
- 서비스 이름 (eureka에 등록될 때 사용)
spring.cloud.gateway.routes
- 실제 라우팅 규칙 정의 부분이다.
id
- 라우트의 고유 식별자이다.
- 동일한 식별자를 여러번 사용해도 실제로는 상관없다.
uri
- 요청을 전달할 대상 서비스를 의미한다.
- 여기서 lb://는 로드 밸런싱을 의미한다.
- 만약, 정적 주소 (ex.
http://localhost:8080/~~)를 사용한다면 여러개의 서비스를 등록하더라도 해당 주소로만 요청이 넘어가는 문제가 발생한다.- 따라서, 특별한 경우가 아니면 lb:// 를 prefix로 사용하자
predicates
- 이 라우트에 적용할 조건을 정의하는 부분이다.
- 필터, 경로, HTTP 메소드 등이 여기서 정의된다.
filters
- 요청/응답 변환 필터를 의미한다.
RemoveRequestHeader
- 특정 헤더를 제거 (예: 쿠키)
RewritePath
- URL 경로를 재작성하는 부분이다.
- 정규표현식을 사용한다.
AuthorizationHeaderFilter
- 커스텀 인증 필터를 정의하는 부분이다.
spring.cloud.gateway.default-filters
- 모든 라우트에 적용되는 필터이다.
이렇게 정리해보니 아마 독자가 읽기 어려울 것 같다는 생각이 든다.
그래서 하나의 라우트 정보를 사진으로 정리하면 다음과 같다.

들어온 요청을 적절하게 라우팅을 해주는 것이 API Gateway의 목표이다.
이전에 모놀리식 서비스를 만들어봤다면, token filter를 만들어서 적절한 요청인지 권한 확인을 진행하는 Spring Security를 적용해본 경험이 있을 것이다.
모든 요청은 API Gateway로 들어오기 때문에, 각 서비스마다 필터를 적용해주는 것보다는 API Gateway에 필터를 적용해서 보안관련 작업을 수행하는 것이 더 효율적이다.
따라서, 위 설정 파일에서도 살펴봤다시피 filters 부분에 여러가지 커스텀 필터를 등록할 수 있다.
Client가 요청을 보내면 그 요청에 대해서 우선 pre-filter가 동작한다.
이후, 서비스에서 요청을 모두 처리하고 response를 받게되면 API Gateway에서 설정해준 post-filter가 동작하게 된다.
순서를 작성해보면 다음과 같다.
Filter 동작 과정 - (1)
request->pre-filter->서비스 인스턴스에서 요청 처리->post-filter->response
만약, GlobalFilter와 해당 라우트에 별도의 커스텀 필터도 적용된다면 다음과 같이 동작하게 된다.
Filter 동작 과정 - (2)
request->global-pre-filter->custom-pre-filter->서비스 인스턴스에서 요청 처리->custom-post-filter->global-post-filter
필터, 인터셉터, AOP를 동시에 사용하는 경우 동작 과정과 비슷하게 진행된다는 것을 기억하자!
권한 확인과 같은 SecurityFilter를 적용하기 위해서는 커스텀 필터를 직접 구현하고 등록해야 한다.
커스텀 필터의 기본적인 구조는 다음과 같다.
Sample Code
@Component @Slf4j public class CustomFilter extends AbstractGatewayFilterFactory<CustomFilter.Config> { public CustomFilter() { 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("Custom PRE filter: request id -> {}", request.getId()); // Custom Post Filter return chain.filter(exchange).then(Mono.fromRunnable(() -> { log.info("Custom POST filter: response code -> {}", response.getStatusCode()); })); }; } public static class Config { // Put the configuration properties } }
여기서 스프링부트로 Spring Security를 사용해서 JWT 필터를 만들어봤다면 한가지 차이점을 발견했을수도 있다.
바로 OncePerRequestFilter 가 아닌 AbstractGatewayFilterFactory 를 상속한다는 것이다.
여기서 우리는 Spring MVC와 Spring WebFlux에 대해서 이해해야 한다.
Spring MVC는 이전에 많이 봐서 이해되지만, Spring WebFlux는 어떤 내용일까?
위에서 상속하는 필터의 내용이 다른 이유는 기반 기술 스택의 차이에서 비롯된다.
Spring MVC (서블릿 기반)
- 내장 서버: Tomcat (서블릿 컨테이너)
- 처리 방식: 동기식, 블로킹 I/O
- 필터 타입: OncePerRequestFilter, HandlerInterceptor
- 요청/응답 객체: HttpServletRequest, HttpServletResponse
Spring WebFlux (리액티브 기반)
- 내장 서버: Netty (비동기 이벤트 기반)
- 처리 방식: 비동기식, 논블로킹 I/O
- 필터 타입: WebFilter, AbstractGatewayFilterFactory
- 요청/응답 객체: ServerHttpRequest, ServerHttpResponse
Spring Cloud Gateway는 Spring WebFlux 기반으로 구축되어 있기 때문에, 모든 필터 로직이 리액티브 스타일로 작성되어야 한다.
이는 위 코드에서 apply 메서드를 살펴보면 HttpServlet이 아닌, ServerHttp를 사용하고 있는 모습을 볼 수 있다.
마찬가지로 chain.filter(exchange).then(...) 형식의 비동기 처리 방식으로 나타난다.
여기서 필터 체인이 Mono<Void> 타입을 반환하고 있는데, 이를 통해 비동기 처리를 가능하게 한다.
(JavaScript의 Promise와 비슷한 개념이다)
그렇다면 왜 Spring Gateway는 WebFlux 기반으로 구현되었을까?
성능 이점
- 높은 동시성 처리: 적은 수의 스레드로 많은 요청을 처리할 수 있다.
- 자원 효율성: 스레드 블로킹이 적어 CPU와 메모리 사용이 효율적이다.
특히 API Gateway는 모든 요청의 진입점으로 높은 부하를 받기 때문에, 이러한 비동기 모델이 적합하다.
이러한 이유로 Spring Cloud Gateway에서는 OncePerRequestFilter 대신 AbstractGatewayFilterFactory를 상속받아 필터를 구현하며, 모든 필터 로직이 리액티브 방식으로 작성되어야 한다는 것을 기억하자 👊
위 내용을 기반으로 Jwt를 검증하는 코드의 일부는 다음과 같다.
Sample Code
@Override public GatewayFilter apply(Config config) { return (exchange, chain) -> { ServerHttpRequest request = exchange.getRequest(); if (!request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) { return onError(exchange, "No authorization header", HttpStatus.UNAUTHORIZED); } String authorizationHeader = request.getHeaders().get(HttpHeaders.AUTHORIZATION).get(0); String jwt = authorizationHeader.replace("Bearer ", ""); if (!isJwtValid(jwt)) { return onError(exchange, "JWT token is not valid", HttpStatus.UNAUTHORIZED); } return chain.filter(exchange); }; } private Mono<Void> onError(ServerWebExchange exchange, String err, HttpStatus httpStatus) { ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(httpStatus); log.error(err); byte[] bytes = "The requested token is invalid.".getBytes(StandardCharsets.UTF_8); DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes); return response.writeWith(Flux.just(buffer)); }
return chain.filter반환 타입
JWT 검증 로직을 apply의 pre-filter 부분에 작성해주면 된다.
여기서 우리가 눈여겨 봐야할 것은 에러를 반환하기 위해 작성된 onError 메서드의 반환 타입이 Mono<Void> 라는 것이다.
또한, post-filter는 별도로 사용하지 않으며 마찬가지로 반환타입이 비동기 처리에 사용되는 Mono 타입이라는 것을 기억해두자!
GlobalFilter는 사진으로 한번 정리해보자
위에서 커스텀 필터 관련 내용을 정리했기 때문에 크게 이해하는데 문제는 없을 것이다.

한가지 추가된 내용이라면 application.yml에 작성한 내용을 매개변수로 전달받아서 로그찍는데 사용했다는 것이다.
우리가 이전에 API Gateway를 설정하는 과정에서 lb:// 를 사용하여 라우트의 uri를 정의했던 것을 기억할 것이다.
여기서 lb를 사용하게 되면 기본적으로 라운드 로빈 방식으로 로드밸런싱을 진행한다.
여러개의 인스턴스를 띄우고 한번 테스트해보자

우선 MY-FIRST-SERVICE 를 3개 띄워서 Eureka에 등록한 상황이다.

다음으로 localhost:8000/first-service/check 로 접근하면 위와 같은 화면이 나온다.
여기서 8000은 Gateway Port Number이다.
서비스에 직접 접근하는게 아니라 Gateway에 접근하면 알아서 라우팅을 진행해줄 것이다.
또한, API Gateway에 first-service관련 설정은 다음과 같다.
- id: first-service
uri: lb://MY-FIRST-SERVICE
predicates:
- Path=/first-service/**
filters:
- AddRequestHeader=first-request, first-request-header-by-yaml
- AddResponseHeader=first-response, first-response-header-from-yaml
- CustomFilter
따라서, first-service로 접근하면 API Gateway가 라우팅을 진행해줄 것이다.
위 사진과 같이 path로 들어갔더니 각자 다른 3개의 포트를 사용하고 있는 모습을 볼 수 있다.
실제로 하나의 페이지에서 계속해서 새로고침을 해보면 라운드 로빈 방식으로 로드밸런싱이 진행되기 때문에 포트번호가 번갈아가면서 바뀌는 모습을 볼 수 있다.

사진과 같이 인스턴스를 종료시키고 새로고침을 누르다보면 에러페이지가 뜨는 모습을 볼 수 있다.
즉, 다른 사람은 정상적인 페이지를 보고 있으나 특정 클라이언트는 에러 페이지를 보고 있는 모습을 상상할 수 있을 것이다.
이러한 문제가 발생하는 이유는 Service Discovery(Eureka)가 서비스가 종료되었음을 바로 확인하지 못해서 그런것이다.
즉, 풀링 작업을 30초마다 주기적으로 수행하지만, 서비스가 내려가고 풀링이 진행되지 않은 시간동안은 오류 페이지를 보여주는 것이다.

실제로 계속 새로고침을 하다보면 위 사진과 같이 다시 정상적으로 페이지 접속이 가능하다.
이번 포스팅에서는 API Gateway의 개념을 살펴보고 설정과 커스텀 필터 구현에 대해서 정리해봤다.
API Gateway는 MSA 환경에서 클라이언트와 서비스 인스턴스 사이의 중간 계층으로, 모든 API 요청의 단일 진입점 역할을 수행한다.
여기서 우리가 중요하게 기억해야 할 내용들을 다시 정리해보면
API Gateway는 lb:// 형식의 URI를 사용해 동적으로 라우팅이 가능하며, 기본적으로 라운드 로빈 방식의 로드 밸런싱을 제공한다는 것
Spring Cloud Gateway는 WebFlux 기반으로 동작하므로, 필터의 반환 타입은 Mono여야 하며, 비동기적인 처리가 가능하다는 것,
커스텀 필터를 구현할 때는 OncePerRequestFilter가 아닌 AbstractGatewayFilterFactory를 상속해야 한다는 것
정도가 있을 것 같다.
이번 포스팅을 통해 Gateway의 기본 개념과 설정 방법을 이해할 수 있으면 좋겠다 👊
안녕하세요! 캠프 2기에 지원하게되었는데 블로그를 보게되어서 질문 드립니다..! 처음 3개월 교육을 대학교 수업과 병행하며 할 수 있을까요? 이틀은 학교에 가야되는 상황인데 9시부터 6시 강의라서 강의를 듣기만 하면되는지, 복습을 얼마나 해야하는지, 얼마나 시간을 쏟아야하는지 가늠이 안가서 여쭤봅니다..!