[MSA] API Gateway Service-3

hanana·2023년 10월 27일
0
post-thumbnail

본 포스팅은 인프런 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

Srping Cloud Gateway

Spring Cloud Gateway를 사용하는 이유

  • Zuul과 마찬가지로 Routing을 지원해주는 라이브러리
  • Zuul과 달리 최신의 SpringBoot 버전에서도 사용 가능
  • 비동기처리가 가능하며 다른 라이브러리와 충돌이 적음
  • 기존의 Zuul 서비스를 대체

[SpringCloudGate] 프로젝트 생성

기존의 zuul과 달리 최신버전의 스프링에서도 잘 작동하므로
SpringBoot 2.7.17, JAVA17 환경에서 프로젝트를 구성하였다.

사용한 의존성

  • Gateway
  • Eureka Discovery Client
  • Lombok

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 정보를 지정
  • spring.cloud.gateway.routes
    라우팅 관련된 설정
    - id : id를 부여
    - uri : 포워딩할 uri 지정
    - predicates : 조건문. -Path = '...' 에 해당하는 요청시 지정된 uri로 포워딩

설정을 마치고
요청을 날려보면 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";
    }
}

* 정상적으로 호출이 되는 모습


[SpringCloudGate] - Filter 적용 - 1

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에 특정값을 추가하였다.

마찬가지로 실행 후 요청시
헤더정보가 정상적으로 들어갔음을 확인할 수 있다.


[SpringCloudGate] - Filter 적용 - 2

위 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
  • spring.cloud.gateway.routes.filter
    속성에 의해 요청/응답시 특정한 header를 추가해줄 수 있다.

[SpringCloudGate] - CustomFilter 적용 - 1

사용자 정의필터로
위의 예제처럼 단순히 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
    }
}
  • NoArgsConstructor
    객체의 생성을 위해 작성한다.
  • apply(Config config)
    필터가 동작하여 어떠한 작업을 수행할지를 정의한다.
  • Config
    필요한 설정정보를 이곳에 추가한다.

요청시 성공적으로 로그가 찍히는 모습을 확인할 수 있다.

[주의점]

AbstractGatewayFilterFactory를 상속하고 있는 클래스명과 application.yml의 filter 하위에 등록하는 필터명이 일치해야 한다.
그렇지 않으면 빌드시에 에러가 발생함!


[SpringCloudGate] - Global 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 앞 뒤로 적용되고 있는 모습을 파악할 수 있다.


[SpringCloudGate] - LoggingFilter 적용

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;
    }
}
  • 다른 필터와 다르게 GateFilter를 직접 구현해서 return 해주었는데
    두번째 인자값인 Ordered.HIGHEST_PRECEDENCE 설정을 통해 다른 모든 필터보다
    PRE 필터는 우선적으로 동작하고 POST필터는 가장늦게 동작하도록 설정하였다.

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
            # 추가 끝

실행결과
의도한대로 로깅필터 -> 글로벌필터 -> 커스텀필터 순으로 동작함을 확인할 수 있다.

profile
성숙해지려고 노력하지 않으면 성숙하기까지 매우 많은 시간이 걸린다.

0개의 댓글