클라이언트/백엔드 서비스 간의 통신을 관리하고 제어하는 역할을 하는 컴포넌트
→ 이를 통해 보안, 성능, 확장성 등 다양한 이점을 얻을 수 있음.
→ 서비스 관리 및 모니터링
서비스 감지, 로드 밸런싱, 회로 차단 등을 통해 서비스 운영이 용이
→ 효율적인 필터링 및 가공
요청과 응답을 가로채 필터링하고 수정하여 보안, 로깅 적용 가능
→ 단일 진입점 ( 어딘가에 진입할 때 최초로 입장하는 문 )
클라이언트는 API 게이트웨이 하나만 알면 됨.
백엔드에서 발생한 오류를 처리하고 클라이언트에게 적절한 메세지 전달
→ 오류 전파 = 단일 서버가 아니기 때문에 오류 추적이 어렵기 때문에 나온 방안
Lombok, Spring Cloud Routing → Gateway 선택해 프로젝트 생성

properties → yml으로 바꾸기

server:
port: 8000
spring:
application:
name: apigateway-service
cloud:
gateway:
routes: # 개별 route 마이크로서비스 정보를 routes에 기입함
- id: first-service
uri: http://localhost:8001/
predicates:
- Path=/first-service/**
- id: second-service
uri: http://localhost:8002/
predicates:
- Path=/second-service/**
# predicates에서 설정한 주소는 정확하게는
# Path=http://localhost:8000/first-service/**를 요청하면
# first-service로 보내주겠다는 뜻
실제로 http://localhost:8000/first-service/hello로 접속하면
first-service의 uri + predicates에 요청된 uri가
-> 8001포트에 요청된다
-> http://localhost:8001/first-service/hello
=> port번호만 변경하고 뒤의 값은 그대로 전송한다.
id : 추후 eureka 서버 등에 등록할 서비스 명 미리 식별하도록 부여
uri : 게이트웨이 서버가 아래에 등록할 predicates의 패턴을 감지했을 때 어떤 서버로 포워딩할지
predicates : 어떤 조건을 만족할 떄 연동된 uri로 보내줄지 조건 작성
(경로 기준, 시간 기준, 쿠키 기준 등 조건식의 기준은 다양하다.)

실행해보면, 평소 쓰던 서버인 Tomcat이 아니라 Netty에 의해 실행되는 것을 확인 가능
앞에 지정한 MS(first-service , second-service)를 일반적인 세팅으로 생성 (Lombok, Spring web 의존)
설정파일(yml)에 ApiGateway에서 지정한 port번호를 각 MS에 설정
server:
port: 8001
테스트를 위한 Controller 생성
@RestController
@RequestMapping(value = "/first-service")
// predicates의 식별 주소를 조건문으로 지정했다면
// 지정한 주소와 매핑되도록 값을 지정할 것
public class FirstserviceController {
@RequestMapping(value = "hello", method = RequestMethod.GET)
public String hello(){
return "Hello, First-service!";
}
}

ApiGateway는 Netty를 쓰지만 MS 자체는 tomcat을 쓰는 것을 볼 수 있다.
라우팅 정보 yml → 자바 Config클래스로 옮기는 법
( yml파일은 설정이 복잡하니까 )
server:
port: 8000
spring:
application:
name: apigateway-service
cloud:
gateway:
routes:
- id: first-service
uri: http://localhost:8001/
predicates:
- Path=/first-service/**
filters:
- AddRequestHeader=f-req,f-req-v
- AddResponseHeader=f-res,f-res-v
- id: second-service
uri: http://localhost:8002/
predicates:
- Path=/second-service/**
filters:
- AddRequestHeader=s-req,s-req-v
- AddResponseHeader=s-res,s-res-v
yml 설정을 옮겨 담을 클래스 생성
@Configuration
public class FilterConfig {
@Bean
public RouteLocator gatewayRoutes(RouteLocatorBuilder routeLocatorBuilder){
return routeLocatorBuilder.routes()
// 설정 내용
// first-service 필터세팅
.route( r -> r.path("/first-service/**")
.filters( f -> f.addRequestHeader("f-req", "f-req-v")
.addResponseHeader("f-res", "f-res-v"))
.uri("http://localhost:8001/")
)
// second-service 필터세팅
.route( r -> r.path("/second-service/**")
.filters( f -> f.addRequestHeader("s-req", "s-req-v")
.addResponseHeader("s-res", "s-res-v"))
.uri("http://localhost:8002/"))
.build(); // 빌더패턴 종료
}
}
→ 필터를 게이트웨이에 걸었으므로
MS에 직접 요청하면 필터가 동작하지 않음 (헤더에 값이 등록되지 않음)
요청헤더에 든 값 컨트롤러로 감지하기

pre-filter : request 처리하기 전에 실행되는 필터
post-filter : response를 처리하기 전에 실행되는 필터
@RequestMapping(value = "/header-check", method = RequestMethod.GET)
public String headerCheck(@RequestHeader("fsreqh") String header){
return header;
}
// MS단에서 header를 확인할 수 있다 (Pre필터 단계에서 헤더가 추가되므로)
// @RequestHeader 어노테이션으로 감지받아 사용
커스텀 필터는 AbstractGatewayFilterFactory를 상속한 필터 클래스를 @Component 어노테이션을 이용해 등록하여 동작
생성자에서 super()을 이용해서 필터 기본 세팅에 필요한 정보를 넘겨주고
apply라는 메서드 하나를 구현해줘서 동작
@Component
public class CustomFilter extends AbstractGatewayFilterFactory<CustomFilter.Config> {
// Generic 내부에 설정에 필요한 자료형을 넣어줘야하는데
// CustomFilter의 설정 클래스는 CustomFilter 내부의 Config로 사용할 것
// Config를 이너클래스로 생성한 이유? -> 응집도를 높여주기 위해
public CustomFilter(){
super(Config.class);
}
public static class Config{
}
@Override
public GatewayFilter apply(Config config) {
// import org.springframework.http.server.reactive.ServerHttpRequest;
// import org.springframework.http.server.reactive.ServerHttpResponse;
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
// pre-filter는 그냥 작성하면 작동
System.out.println("Custom pre filter : " + request.getId());
// post-filter는 return 구문 속에 코드를 작성해서 만듦
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
// Mono -> 비동기방식 서버에서 단일값 전달할 때 사용하는 양식
System.out.println("Custom post filter : " + response.getStatusCode());
}));
};
}
}
- netty에서는 tomcat과 달리 ServerHttpRequest와 ServerHttpResponse를 사용 (비동기 서버에서 사용)
server:
port: 8000
spring:
application:
name: apigateway-service
cloud:
gateway:
routes:
- id: first-service
uri: http://localhost:8001/
predicates:
- Path=/first-service/**
filters:
- CustomFilter #커스텀 필터 적용
- id: second-service
uri: http://localhost:8002/
predicates:
- Path=/second-service/**
filters:
- AddRequestHeader=s-req,s-req-v
- AddResponseHeader=s-res,s-res-v
- 등록된 필터는 yml/configuration에 필터 이름 기입
@Component
public class CustomFilter2 extends AbstractGatewayFilterFactory<CustomFilter2.Config> {
public CustomFilter2() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
GatewayFilter gatewayFilter = new OrderedGatewayFilter((exchange, chain) -> {
// 필터 내용
}, Ordered.HIGHEST_PRECEDENCE);
// 구성 순위 조정이 가능하다.
return gatewayFilter;
}
public static class Config{
}
}@Component
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();
// pre filter
System.out.println("Global Filter default message : " + config.getMessage());
// pre에서 사용할지 말지 Boolean값으로 판단
if(config.isPre()){
System.out.println("Global Pre Filter : " + request.getId());
}
// post filter
return chain.filter(exchange).then(Mono.fromRunnable(()->{
// post에서 사용할지 말지 Boolean값으로 판단
if(config.isPost()){
System.out.println("Global Post Filter : " + response.getStatusCode());
}
}));
};
}
@Getter
@Setter
public static class Config{
private String message;
private boolean pre;
private boolean post;
}
}
- 등록된 필터는 yml/configuration에 필터 이름 기입등록을 게이트웨이 서버측 자체에 해줌
→ yml/config에서의 등록을 gateway측에 직접 걸어줌
server:
port: 8000
spring:
application:
name: apigateway-service
cloud:
gateway:
default-filters: # 전역필터 세팅
- name: GlobalFilter
args:
message: Global Filter Default Message Test
pre: ture
post: true
routes:
- id: first-service
uri: http://localhost:8001/
predicates:
- Path=/first-service/**
filters:
- CustomFilter #커스텀 필터 적용
- id: second-service
uri: http://localhost:8002/
predicates:
- Path=/second-service/**
filters:
- AddRequestHeader=s-req,s-req-v
- AddResponseHeader=s-res,s-res-v
- 모든 MS(first-service, second-service)에서 실행되는 모습을 볼 수 있다.
호출 우선 순위
@Slf4j
@Component
public class LogFilter extends AbstractGatewayFilterFactory<LogFilter.Config> {
@Override
public GatewayFilter apply(Config config) {
return ((exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
// pre-filter
log.info("log filter : {}",request.getId());
// post- filter
return chain.filter(exchange).then(Mono.fromRunnable(()->{
log.info("log post filter : {}", response.getStatusCode());
}));
});
}
public LogFilter() {
super(Config.class);
}
public static class Config{
}
}
server:
port: 8000
spring:
application:
name: apigateway-service
cloud:
gateway:
default-filters: # 전역필터 세팅
- name: GlobalFilter
args:
message: Global Filter Default Message Test
pre: true
post: true
routes:
- id: first-service
uri: http://localhost:8001/
predicates:
- Path=/first-service/**
filters:
- CustomFilter
- LogFilter # 로깅필터 추가
- id: second-service
uri: http://localhost:8002/
predicates:
- Path=/second-service/**
filters:
- AddRequestHeader=s-req,s-req-v
- AddResponseHeader=s-res,s-res-v