API 게이트웨이는 프론트와 백엔드 간의 연결 전에, 백엔드 서버를 Production에 올려 테스트할 때 필요하기에 만들게 되었다.
1. 인증 로직 번거로움 관점
백엔드 기능의 서브 도메인 단위 통합테스트를 하고 싶었는데, 현재 서브 도메인 내에서 자체적으로 인증 기능을 수행해고 있었다.
그래서 curl로 간단하게 테스트하기가 어려운 문제가 존재하고 있었다.
인증 기능과 같은 공통 로직 관점 기능이 마이크로서비스에 침투해 있으면, 우리가 구현한 기능만 간단하게 테스트하기가 까다로워지는 문제가 발생한다.
2. 프론트가 API 버전을 몰랐으면 좋겠다.
- 프론트가 버전을 몰라도, request Body 기반으로 라우팅을 수행할 수 있으면 좋겠다.
그런데, request body 파싱->객체 변환->API 재호출은 게이트웨이의 비용이 커진다.
- 웹 서버를 직접 구현해보면 알겠지만, TCP는 스트림으로 들어온다.
- 즉, 한번 받으면 다시 읽지 못한다.
- 내가 생각한 동작은
- 게이트웨이에서 바디 한번 먼저 읽고
- 그 요청이 어디로 라우팅되어야 하는지 확인 후
- 그 요청 그대로 해당 서비스로 라우팅한다.
- 그런데 실제로는
- 게이트웨이에서 바디를 한번 먼저 읽는 순간, 다시 읽지 못한다.
- 헤더와 달리 바디는 json으로 되어 있어, 똑같은 구조를 그대로 다시 만드는데 꽤나 큰 오버헤드가 소모된다.
- 게이트웨이에서 바디를 읽는 순간, 다시 읽은 바디를 그대로 출력할 준비를 해야 한다.
- gRPC로 HTTP/2.0을 이용해 메시지 자체를 압축해서 보내면 그나마 좀 나을려나?
그런데 아직 스프링 클라우드 게이트웨이의 동작을 잘 모르는 상황이다.
따라서 gRPC까지 동원해서 요청 "전달" 이 아닌, 요청 "읽기"/새 요청 "보내기"로 보내기에는 사이드 이펙트가 두렵다.
곰곰히 생각해 봤을 때, 2번은 차치하더라도 1번으로 얻는 메리트가 API 게이트웨이가 포함됐을 때의 단점을 상쇄한다고 생각했다.
급한 상황에 curl/Postman으로 간편하게 테스트 할 수 있음은 마음의 안정+굉장히 중요하다고 생각한다.
그래서 API 게이트웨이를 도입한다.
1.SCG 라우팅 정의하기
서비스 디스커버리 사용
보통 서비스 디스커버리로 유레카를 사용하기도 하지만, 현재 우리 Pinit 서비스에는 k3s가 적용되어 있다.
k3s 클러스터 내에선 Service DNS와 kube-proxy/iptables 기반 로드밸런싱이 기본 제공된다.
따라서 Eureka를 제거하고도 라우팅이 가능하다.
spring:
cloud:
gateway:
server:
webflux:
routes:
- id: notification-service
uri: http://notification-service
predicates:
- Host=notification.pinit.go-gradually.me
- id: auth-service
uri: http://auth-service
predicates:
- Host=auth.pinit.go-gradually.me
- id: task-service
uri: http://task-service
predicates:
- Host=api.pinit.go-gradually.me
2. SCG에 인증 필터 넣기
- X-Member-Id 헤더 추가 및 전달
- 쿼리 파라미터로 넘길 경우, 사용자 ID 정보가 프록시/로그에 남을 수 있다.
- 보통 로그 찍을 때 URI를 저장하기 때문
- 따라서 X-Member-Id 헤더에 memberId 값을 넘기고, 이를 전달하는 형태로 사용
- 인증을 제외하는 경로 지정
- 어디를 인증을 제외해야 하는가? - 필터 예외 넣기
GatewayFilterFactory
1) 핵심 개념: GatewayFilterFactory vs GatewayFilter
GatewayFilterFactory- “설정값(Config) →
GatewayFilter생성” 역할 - YAML의
filters:항목은 내부적으로 “어떤 Factory를 쓸지 + 어떤 Config로 적용할지”를 의미한다.
- “설정값(Config) →
GatewayFilter- 실제 실행 로직(필터 체인에서 동작)
- 시그니처는 대략
(exchange, chain) -> Mono<Void>형태(리액티브)
SCG는 여러 built-in GatewayFilter Factory를 제공하며, route filters는 요청/응답을 수정할 수 있다.
2) SCG가 YAML의 filters:를 처리하는 흐름
SCG는 filters를 대략 아래 순서로 처리한다.
- 애플리케이션 부팅 시, SCG가 스프링 컨테이너에 등록된
GatewayFilterFactory빈들을 수집 - 라우트 정의(
spring.cloud.gateway.routes[*])를 읽으면서 각filters:항목을 파싱 - 필터 이름으로 해당 Factory 빈을 찾음 - 이름 규칙이 중요하다.
args또는 shortcut 문법으로 Config를 바인딩factory.apply(config)호출 →GatewayFilter생성- 요청이 route에 매칭되면, 생성된 filter chain에서 순서대로 실행
3) “이름” 규칙이 중요한 이유 (매칭 규칙)
커스텀 팩토리를 만들 때 보통 클래스명을 …GatewayFilterFactory로 끝내고, YAML에서는 그 접미어를 뺀 이름으로 쓰게 된다.
- 클래스:
JwtSubToMemberIdHeaderGatewayFilterFactory - YAML:
JwtSubToMemberIdHeader
이유는 필터 이름으로 해당 Factroy 빈을 찾을 때, GatewayFilterFactory 로 끝나는 빈 만을 탐색하기 때문이다.
관련 문서: 스프링 클라우드 게이트웨이 docs
Developer Guide :: Spring Cloud Gateway
To write a GatewayFilter, you must implement GatewayFilterFactory as a bean. You can extend an abstract class called AbstractGatewayFilterFactory. The following examples show how to do so: Example 1. PreGatewayFilterFactory.java @Component public class Pre
docs.spring.io
4) 커스텀GatewayFilterFactory 작성 패턴
가장 일반적인 방식은 AbstractGatewayFilterFactory<Config>를 상속받는 것이다.
@Component
public class MyFilterGatewayFilterFactory
extends AbstractGatewayFilterFactory<MyFilterGatewayFilterFactory.Config> {
public MyFilterGatewayFilterFactory() {
super(Config.class);
}
public static class Config {
private String headerName;
private String headerValue;
// getter/setter
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
var request = exchange.getRequest().mutate()
.header(config.getHeaderName(), config.getHeaderValue())
.build();
return chain.filter(exchange.mutate().request(request).build());
};
}
}
위 Config에서 shortcut 구문(MyFilter=a,b)으로 값을 넣고 싶다면, Config 필드 매핑 순서를 정의해야 한다.
@Override
public List<String> shortcutFieldOrder() {
return List.of("headerName", "headerValue");
}
그럼 YAML에서 다음처럼 사용할 수 있다.
filters:
- MyFilter=X-Member-Id, 123
자세한 소스코드는 다음 링크에서 확인 가능하다.
pinit-gateway/src/main/java/me/pinitgateway/filter/JwtSubToMemberIdHeaderGatewayFilterFactory.java at master · Pinit-Scheduler/
Public 일정 관리/실행 서비스 Pinit의 API Gateway. Contribute to Pinit-Scheduler/pinit-gateway development by creating an account on GitHub.
github.com
3. CORS 처리하기
게이트웨이에서 CORS 설정하는 방법을 알아보자.
spring:
cloud:
gateway:
server:
webflux:
globalcors:
cors-configurations:
"[/**]":
allowedOrigins:
- "https://app.example.com"
allowedMethods:
- GET
- POST
- PUT
- PATCH
- DELETE
- OPTIONS
allowedHeaders: "*"
allowCredentials: true
maxAge: 3600
기본적으로 위 설정을 넣어주게 되면 scg가 지원하는 cors 설정을 사용할 수 있는데,
현재 우리 api-gateway는 스프링 시큐리티를 사용중이다.
- cors 검증
- jwt 검증
- jwt sub 추출
이때 발생하는 에러를 어디서 처리하느냐를 두 가지 방법으로 구분할 수 있다.
- gateway filter로 처리하기
- 스프링 시큐리티 필터로 처리하기
기본적으로 두 필터의 우선순위는 스프링 시큐리티 필터체인이 먼저 처리된 뒤, gateway filter & route가 진행된다.
- cors/jwt 검증 부분은 스프링 시큐리티가 기본 지원하는 인증/인가 관련 예외 처리를 쓰고 싶으므로, gateway filter가 아닌 인증 필터로 처리하였다.
- jwt sub 추출은 스프링 시큐리티 지원 기능보단 직접 게이트웨이 필터를 다루고 싶으므로, gateway filter 로 처리하였다.
이후, 각 서비스의 cors를 제거하면 정상 동작하게 된다.
각 서비스에 등록되어 있는 CORS를 제거하지 않으면, CORS 헤더가 중복 적용되어 CORS 처리가 정상적으로 이루어지지 않는다.
'Article - 깊게 탐구하기 > 시간 기록, 관리 서비스 Pinit' 카테고리의 다른 글
| [Pinit] 핀잇 백엔드 마이크로서비스를 배포할 k8s 클러스터 구축하기 (0) | 2025.12.23 |
|---|---|
| [Pinit] 최종 일관성 구현 - 회원가입 플로우, 이대로 괜찮은가? (0) | 2025.12.15 |
| [Pinit] 도메인 이벤트로 소통하기 - gRPC (0) | 2025.12.14 |
| [Pinit] 푸시 알림 발송하기 (0) | 2025.12.14 |
| [Pinit] 푸시 알림 Exactly-Once 구현하기 (0) | 2025.12.12 |