본 포스팅은 인프런 Dowon Lee님의
Spring Cloud로 개발하는 마이크로서비스 애플리케이션(MSA) 강의를 토대로 작성되었습니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%EC%84%9C%EB%B9%84%EC%8A%A4
Spring Cloud Gateway를 사용하는 이유
- Zuul과 마찬가지로 Routing을 지원해주는 라이브러리
- Zuul과 달리 최신의 SpringBoot 버전에서도 사용 가능
- 비동기처리가 가능하며 다른 라이브러리와 충돌이 적음
- 기존의 Zuul 서비스를 대체
기존의 zuul과 달리 최신버전의 스프링에서도 잘 작동하므로
SpringBoot 2.7.17, JAVA17 환경에서 프로젝트를 구성하였다.
사용한 의존성
application.yml
server:
port: 8000
eureka:
client:
register-with-eureka: false #eureka설정 일단 사용하지 않음
fetch-registry: false #eureka설정 일단 사용하지 않음
service-url:
defaultZone: http://127.0.0.1:8761/eureka # eurekaClient 등록위치
spring:
application:
name: apigateway-service
cloud:
gateway:
routes:
- id: first-service # id지정
uri: http://127.0.0.1:8081/ #포워딩할 uri
predicates: # 조건절과 같은 의미
- Path=/first-service/** #Path 정보를 지정
- id: second-service # id지정
uri: http://127.0.0.1:8082/ #포워딩할 uri
predicates: # 조건절과 같은 의미
- Path=/second-service/** #Path 정보를 지정
설정을 마치고
요청을 날려보면 404에러가 나온다.
원인 : Path=/first-service/ 조건이 맞아서 포워딩은 되었는데
최종 호출된 요청은 http://127.0.0.1:8081/first-service/** 이 되기 때문에
url 매핑이 안되어서 404에러를 발생시킨다.
MircoService들의 기본 매핑경로에 /first-service를 추가하여 해결한다.
FirstServiceController
@RestController
@RequestMapping("/first-service")
public class FirstServiceController {
@GetMapping("/welcome")
public String welcome() {
return "Welcome to the First Service";
}
}
* 정상적으로 호출이 되는 모습
application.yml
server:
port: 8000
eureka:
client:
register-with-eureka: false #eureka설정 일단 사용하지 않음
fetch-registry: false #eureka설정 일단 사용하지 않음
service-url:
defaultZone: http://127.0.0.1:8761/eureka # eurekaClient 등록위치
spring:
application:
name: apigateway-service
FilterConfig
@Configuration
public class FilterConfig {
@Bean
public RouteLocator gatewayRoutes(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("/first-service/**")
.filters(f -> f.addRequestHeader("first-request","first-request-header")
.addResponseHeader("first-response","first-response-header"))
.uri("http://127.0.0.1:8081"))
.route(r -> r.path("/second-service/**")
.filters(f -> f.addRequestHeader("second-request","second-request-header")
.addResponseHeader("second-response","second-response-header"))
.uri("http://127.0.0.1:8082"))
.build();
}
}
메소드 구성이 직관적이여서 쉽게 파악알 수 있다.
application.yml에서 지운 설정을 자바코드에 옮겨놨고,
요청/응답의 header에 특정값을 추가하였다.
마찬가지로 실행 후 요청시
헤더정보가 정상적으로 들어갔음을 확인할 수 있다.
위 FilterConfig 클래스의 @Bean, @Configuration 에노테이션을 주석처리 한 후
application.yml을 아래와 같이 변경해준다.
server:
port: 8000
eureka:
client:
register-with-eureka: false #eureka설정 일단 사용하지 않음
fetch-registry: false #eureka설정 일단 사용하지 않음
service-url:
defaultZone: http://127.0.0.1:8761/eureka # eurekaClient 등록위치
spring:
application:
name: apigateway-service
cloud:
gateway:
routes:
- id: first-service # id지정
uri: http://127.0.0.1:8081/ #포워딩할 uri
predicates: # 조건절과 같은 의미
- Path=/first-service/** #Path 정보를 지정
#필터정보 추가
filters:
- AddRequestHeader=first-request, first-request-header2
- AddResponseHeader=first-response, first-response-header2
- id: second-service # id지정
uri: http://127.0.0.1:8082/ #포워딩할 uri
predicates: # 조건절과 같은 의미
- Path=/second-service/** #Path 정보를 지정
#필터정보추가
filters:
- AddRequestHeader=second-request, second-request-header2
- AddResponseHeader=second-response, second-response-header2
사용자 정의필터로
위의 예제처럼 단순히 header 정보만 추가하는것이 아닌,
인증/인가정책 적용, 로깅, 요청을 변경하는 역할을 수행할 수 있다.
application.yml
server:
port: 8000
eureka:
client:
register-with-eureka: false #eureka설정 일단 사용하지 않음
fetch-registry: false #eureka설정 일단 사용하지 않음
service-url:
defaultZone: http://127.0.0.1:8761/eureka # eurekaClient 등록위치
spring:
application:
name: apigateway-service
cloud:
gateway:
routes:
- id: first-service # id지정
uri: http://127.0.0.1:8081/ #포워딩할 uri
predicates: # 조건절과 같은 의미
- Path=/first-service/** #Path 정보를 지정
filters:
#커스텀필터 추가
- MyCustomFilter
# - AddRequestHeader=first-request, first-request-header2
# - AddResponseHeader=first-response, first-response-header2
- id: second-service # id지정
uri: http://127.0.0.1:8082/ #포워딩할 uri
predicates: # 조건절과 같은 의미
- Path=/second-service/** #Path 정보를 지정
filters:
#커스텀필터 추가
- MyCustomFilter
# - AddRequestHeader=second-request, second-request-header2
# - AddResponseHeader=second-response, second-response-header2
MyCustomFilter
@Slf4j
@Component
public class MyCustomFilter extends AbstractGatewayFilterFactory<MyCustomFilter.Config> {
public MyCustomFilter(){
super(Config.class);
}
/**
* 실행시킬 내용을 여기에 입력
* @param config
* @return
*/
@Override
public GatewayFilter apply(Config config) {
//Custom Pre Filter. Suppose we can extract JWT and perform Authentication
return ((exchange, chain) -> {
// PRE filter - DO SOMETHING - START
//비동기 방식을 기본적으로 사용하기 때문에
// ServletRequest이 아닌 ServerRequest
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
log.info("Custom PRE filter: request uri -> {}", request.getId());
// PRE filter - DO SOMETHING - END
// Custom Post Filter. Suppose we can call error response handler based on error code.
return chain.filter(exchange).then(Mono.fromRunnable(()-> {
//POST filter - DO SOMETHING - START
log.info("Custom POST filter: response code -> {}", response.getStatusCode());
//POST filter - DO SOMETHING - END
}));
});
}
public static class Config{
//Put the configuration properties
}
}
요청시 성공적으로 로그가 찍히는 모습을 확인할 수 있다.
[주의점]
AbstractGatewayFilterFactory를 상속하고 있는 클래스명과 application.yml의 filter 하위에 등록하는 필터명이 일치해야 한다.
그렇지 않으면 빌드시에 에러가 발생함!
앞서 특정 MicroService마다 요청을 처리하는 필터를 등록했다.
그러나 사실 단순히 로그를 남긴다던가, 인증정보를 확인하는 과정은 애플리케이션 전반에 걸친 공통적인 작업이다.
이를 구현하기 글로벌 필터를 구현해보자.
* 가독성을 위해 위에서 사용된 주석은 공백처리 하였음
application.yml
server:
port: 8000
eureka:
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://127.0.0.1:8761/eureka
spring:
application:
name: apigateway-service
cloud:
gateway:
# 추가 - start
default-filters:
- name: MyGlobalFilter
args: # 필터 Config 애서 사용할 필드
baseMessage: Spring Cloud Gateway Global Filter
preLogger: true
postLogger: true
# 추가 - end
routes:
- id: first-service
uri: http://127.0.0.1:8081/
predicates:
- Path=/first-service/**
filters:
- MyCustomFilter
- id: second-service
uri: http://127.0.0.1:8082/
predicates:
- Path=/second-service/**
filters:
- MyCustomFilter
MyGlobalFilter
@Slf4j
@Component
public class MyGlobalFilter extends AbstractGatewayFilterFactory<MyGlobalFilter.Config> {
public MyGlobalFilter(){
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());
if(config.isPreLogger()) {
log.info("Global Filter Start : requestId -> {}",request.getId());
}
return chain.filter(exchange).then(Mono.fromRunnable(()-> {
if(config.isPostLogger()) {
log.info("Global Filter End : responseCode -> {}",response.getStatusCode());
}
}));
});
}
@Getter
@Setter
public static class Config{
private String baseMessage;
private boolean preLogger;
private boolean postLogger;
}
}
애플리케이션을 재실행 한 후 요청을 보내면
globalFilter가 CustomFilter 앞 뒤로 적용되고 있는 모습을 파악할 수 있다.
LoggingFilter
@Slf4j
@Component
public class LoggingFilter extends AbstractGatewayFilterFactory<LoggingFilter.Config> {
public LoggingFilter(){
super(Config.class);
}
@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());
if(config.isPreLogger()) {
log.info("Logging PRE Filter : requestId -> {}",request.getId());
}
return chain.filter(exchange).then(Mono.fromRunnable(()-> {
if(config.isPostLogger()) {
log.info("Logging POST Filter : responseCode -> {}",response.getStatusCode());
}
}));
}), Ordered.HIGHEST_PRECEDENCE);
return filter;
}
@Getter
@Setter
public static class Config{
private String baseMessage;
private boolean preLogger;
private boolean postLogger;
}
}
application.yml
# 윗부분 생략
spring:
application:
name: apigateway-service
cloud:
gateway:
default-filters:
- name: MyGlobalFilter
args:
baseMessage: Spring Cloud Gateway Global Filter
preLogger: true
postLogger: true
routes:
- id: first-service
uri: http://127.0.0.1:8081
predicates:
- Path=/first-service/**
filters:
- MyCustomFilter
- id: second-service
uri: http://127.0.0.1:8082/
predicates:
- Path=/second-service/**
filters:
- MyCustomFilter
# 추가 시작
- name: LoggingFilter
args:
baseMessage: Hi Logger!
preLogger: true
postLogger: true
# 추가 끝
실행결과
의도한대로 로깅필터 -> 글로벌필터 -> 커스텀필터 순으로 동작함을 확인할 수 있다.