Spring Cloud + MSA 애플리케이션 개발 3(API Gateway Service)

지원·2024년 2월 12일
0

MSA개발

목록 보기
3/15

API Gateway Service란?

  • 사용자가 설정한 라우팅 설정에 따라서 각각 엔드포인트로 클라이언트 대신에 요청하고 응답을 받으면 클라이언트에 전달하는 프록시 역할

API Gateway Service 기능

  • 인증 및 권한 부여
  • 서비스 검색 통합
  • 응답 캐싱
  • 속도 제한 , 부하 분산
  • 로깅 , 추적 , 상관관계
  • IP 허용 목록에 추가

Spring Cloud 에서 MSA 간 통신 방법

  1. RestTemplate
  2. Feign Client

client 가 직접 Microservice 에 접근하는건 좋지 않다고 했다.

  • 그렇기 때문에 API Gateway 를 통해야 하는데, 그 작업을 별도에 서비스에 구축하는 것이 아닌 클라이언트 내부에 Ribon 을 넣으면 된다.
  • 즉 Ribon 을 통해서 Microservice 로 갈 수 있다.
  • Gateway 를 중간에 놓았지만 Client 안에다가 넣는다고 생각하면 된다.
  • 이렇게 하면 ip:port 번호로 접근하는 것이 아닌 Microservice 이름을 통해서 접근할 수 있는 장점이 있다.
  • 하지만 Ribon 은 deprecated 됐다.

Netflix Zuul

  • Netflix Zuul 는 API gateway 라고 생각하면 된다.
  • 하지만 deprecated 됐기 떄문에 Spring Cloud Gateway 를 학습해야 한다.

Spring Cloud Gateway 란?

  • Zuul 은 최신 트렌드에 맞지 않으며 호환성 문제가 있어 deprecated 됐다.
  • Spring 에서 직접 만든 API Gateway 가 바로 Spring Cloud Gateway 이며, 비동기 처리가 가능하고 최신 트렌드에 맞다.
  • Zuul 을 대체하는 것이라고 생각하면 된다.

실제로 yml 파일에 gateway:routes 설정을 하면서 Spring Cloud Gateway 를 사용해보자.

Spring Cloud Gateway - 프로젝트 생성

  • spring cloud gateway , lombok , eureka client 의존성을 추가한다.
  cloud:
    gateway:
      routes:
        - id: first-service
          uri: http://localhost:8081/
          predicates:
            - Path=/first-service/**

        - id: second-service
          uri: http://localhost:8082/
          predicates:
            - Path=/second-service/**
  • 위와 같이 yml 파일을 작성
  • api gateway 의 port 는 8000 으로 설정
  • predicates 는 조건문이라고 생각하면 되는데, 요청 경로가 first-service 면 port 가 8081 인 서버로 가고 second-service 면 port 가 8082 인 서버로 간다.
  • 그래서 spring web , eureka client 의존성을 추가한 2개의 프로젝트를 만들고 Controller 를 만들어서 @RequestMapping("/first-service") , @RequestMapping("/second-service") 를 각각 달아줬다.

해당 서버들을 모두 실행하고 각 uri 로 접근한 결과 Controller 가 잘 동작하는 것을 확인했다.

Spring Cloud Gateway - Filter 적용

현재까지의 프로세스를 정리

  • Client 가 First Service , Second Service 에 접근할 때는 Spring Cloud gateway 를 통해 접근하도록 한다.
  • 이때 Spring Cloud gateway 에서는 Gateway Handler Mapping 을 통해 요청 정보를 받고 Predicate 라는 조건을 거치고 Pre Filter 라는 필터에 들어간다.
  • Pre Filter 의 응답이 Post Filter 에 들어가고 그 이후 다시 Gateway Handler Mapping 에 들어가면서 응답이 나간다.
  • Filter 를 적용할 때는 Property(yml파일) 과 Java Code 로 작성할 수 있는데 모두 해볼 예정이다.

FilterConfig

  • FilterConfig.class 를 만들고 여기에 RouteLocator 를 Bean 으로 등록한다.
  • 등록할 때 .route(r.path().filters(f.addRequestHeader().addResponseHeader())) 와 같은 api 를 통해서 해당 path 에 요청이 들어오면 filter 를 통하는데 여기서 RequestHeader 를 추가하거나, ResponseHeader 를 추가할 수 있다.
  • 이렇게 추가된 정보를 Controller 에서 가져다가 사용할 수 있다.
  • 앞서 말했던 것 처럼 Java Code 로 직접 작성할 수도 있고 yml 파일로도 할 수 있다.
  • 직접 실습을 통해 확인해보자.

Java Code 로 Filter 추가

@Configuration
public class FilterConfig {
    @Bean
    public RouteLocator gatewayRoutes(RouteLocatorBuilder builder) {
        // yml 파일에서 했던 것을 Java Code 로 작성
        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://localhost:8081"))
                .route(r -> r.path("/second-service/**")
                        .filters(f -> f.addRequestHeader("second-request","second-request-header")
                                .addResponseHeader("second-response","second-response-header"))
                        .uri("http://localhost:8082"))
                .build();
    }
}
  • 위와 같이 설정하고 각 first-service , second-service 에서 @GetMapping("/message") 를 만들고 파라미터에 @RequestHeader("위에서 설정한 헤더") String header 를 추가하면 된다.
  • 8000/first-service/message 로 접근하면 RequestHeader 에는 우리가 추가한 first-request-header 가 추가되고 개발자 도구로 들어가서 response 값을 확인하면 first-response-header 가 추가된 것도 확인할 수 있다.
  • 즉 gateway(8000번 포트) 에서 filter 를 거치는데 해당 path 에 따라서 RequestHeader , ResponseHeader 를 추가하고 해당 uri 로 전달할 수 있다.

yml 파일로 Filter 추가

  cloud:
    gateway:
      routes:
        - id: first-service
          uri: http://localhost:8081/
          predicates:
            - Path=/first-service/**
          filters:
            - AddRequestHeader=first-request, first-request-header2
            - AddResponseHeader=first-response, first-response-header2

        - id: second-service
          uri: http://localhost:8082/
          predicates:
            - Path=/second-service/**
          filters:
            - AddRequestHeader=second-request, second-request-header2
            - AddResponseHeader=second-response, second-response-header2
  • 위와 같이 yml 파일에 작성하면 Java Code 와 작성한 것 처럼 동작한다.

Spring Cloud Gateway - Custom Filter

  • Custom Filter 에서 로그 출력 , 인증 , locale 변경 등등 가능
  • CustomFilter 는 반드시 AbstractGatewayFilterFactory 를 상속 후 apply 메서드 구현 (exchange 로 request , response 를 가져올 수 있다)
  • Reactive Gateway 를 가져오면 기본적으로 netty 를 사용한다.
  • 원래는 tomcat 을 사용하지만 gateway 를 사용하면 비동기를 지원하는 netty 를 사용
  • tomcat 을 사용하면 ServletRequest 객체를 사용하지만 netty 에서는 ServerHttpRequest 객체를 지원한다.
  • 실제로 Custom Filter 를 만들어보자.
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(()->{ // 비동기 방식 서버에서 단일값을 전달할 때 Mono 타입으로 전달한다.
                log.info("Custom POST filter : response id -> {}" , response.getStatusCode());
            }));
        };
    }

    public static class Config {
        // configuration 정보를 여기에 넣으면 된다.

    }
}

// application.yml
  cloud:
    gateway:
      routes:
        - id: first-service
          uri: http://localhost:8081/
          predicates:
            - Path=/first-service/**
          filters:
            - CustomFilter
  • 위 코드 처럼 apply 함수 에서 Pre Filter , Post Filter 를 만들면 된다.
  • 그런후 application.yml 에다가 해달 filter 를 등록해줘야 하기 때문에 filters: 에다가 현재 만든 Class 를 넣어주면 된다.
  • 이렇게 하고 first-service , second-service 를 gateway 를 통해 요청하면 Pre Filter , Post Filter 가 각각 동작하여 로그가 남겨지는 것을 확인할 수 있다.

Spring Cloud Gateway - Global Filter

Custom Filter 는 yml 파일에서 routes: 으로 각각 라우팅 정보 마다 지정을 해줬어야 했는데 Global Filter 는 공통된 부분은 하나의 필터로 사용할 수 있다.

  • 코드는 거의 유사하다.
    public GatewayFilter apply(Config config) {
        // Custom Pre Filter
        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 : request id -> {}",request.getId());
            }

            // Custom Post Filter
            return chain.filter(exchange).then(Mono.fromRunnable(()->{ // 비동기 방식 서버에서 단일값을 전달할 때 Mono 타입으로 전달한다.
                if (config.isPostLogger()) {
                    log.info("Global Filter End : response code -> {}",response.getStatusCode());
                }
            }));
        };
    }
    
    @Data
    public static class Config {
        private String baseMessage;
        private boolean preLogger;
        private boolean postLogger;

    }

// yml 파일
spring:
  application:
    name: apigateway-service
  cloud:
    gateway:
      default-filters:
        - name: GlobalFilter
          args:
            baseMessage: Spring Cloud Gateway Global Filter
            preLogger: true
            postLogger: true
  • yml 파일에 args: 에다가 기본값을 정해서 전달할 수 있다.
  • preLogger , PostLogger 값에 따라서 로그 출력 같은 로직을 만들 수 있다.

Spring Cloud Gateway - Logging Filter

  • Global Filter 를 응용해서 로깅 역할을 하는 필터를 만들어보자.

순서

  • Global -> Custom -> Logging(pre) -> Logging(post) -> Custom -> Global

코드는 Global 과 거의 유사하다.

  cloud:
    gateway:
    	...
      routes:
      		...
          filters:
             # 추가적인 전달할 파라미터가 있다면 name 옵션을 써야한다.
            - name: CustomFilter
            - name: LoggingFilter
              args:
                baseMessage: Hi , there.
                preLogger: true
                postLogger: true
  • 위와 같이 yml 파일을 작성하면 되고 CustomFilter 는 파라미터 넘기는 값이 없지만 LoggingFilter 는 넘기는 값이 있다.
  • 그럴때는 둘다 name 옵션을 사용해야 하고 파라미터 넘기는 값은 args: 에다가 작성하면 된다.

Spring Cloud Gateway - Load Balancer 1

Eureka 연동

  • 즉 Spring Gateway , first-service , second-service 모두 유레카에 등록한다.
  • 이제 흐름을 생각해보면 8000/first-service/welcome 으로 요청하면 api gateway 에게 먼저 전달되고 그 이후 유레카에게 전달되며 어디에 해당 서비스가 있는지에 대한 정보를 받고 그 정보를 가지고 service 에 접근한다.
  • Eureka Client 추가할 때 yml 파일이 많이 달라진다.
  • lb(Load Banlancer) , Discovery Sever 에 등록된 이름으로 접근한다.
  • Path 에 따라서 Discovery 에 등록된 이름을 따라서 간다.
  • 유레카 서버를 통해서 클라이언트 요청 정보를 전달한다는 것이다.(8081 , 8082 이렇게 접근하는게 X)
  • 뒤에서 yml 파일을 살펴보자.
      routes:
        - id: first-service
          uri: lb://MY-FIRST-SERVICE
          predicates:
            - Path=/first-service/**
          filters:
            - CustomFilter
        - id: second-service
          uri: lb://MY-SECOND-SERVICE
          predicates:
            - Path=/second-service/**
  • 그전에 유레카에 등록하려는 spring gateway , first-service , second-service 모두 yml 파일에서 eureka:client: 에 대한 설정을 true 로 바꿔줘야한다.
  • 위에 yml 파일 처럼 uri 에 lb://인스턴스이름 이런식으로 등록하면 된다.
  • 원래는 8081/first-service 이런식으로 했지만 이제는 유레카 서버에 등록했기 때문에 요청이 유레카 서버에 들어가기 때문에 lb(Load Balancer) 으로 유레카 서버에 등록된 이름을 통해 해당 서비스에 접근할 수 있다.

실제로 모든 코드를 수정한 후 실행해보면 유레카 대시보드에 3개가 등록되어 있었고 POSTMAN 으로 각 요청들을 호출해보면 정상적으로 동작한다.

Spring cloud Gateway - Load Balancer 2

first-service 를 랜덤포트를 사용

  • yml 파일에서 port = 0 으로 지정
  instance:
    instance-id: ${spring.application.name}:${spring.application.instance_id:${random.value}}
  • 형식이 같으면 여러개를 띄워도 한 개로만 나오기 때문에 instance-id 형식을 지정 (전에 했던 내용)
  • 이렇게 한 후 하나는 인텔리J 에서 기동하고 하나는 터미널에서 mvn spring-boot:run 명령어로 기동

이렇게 한 service 에 인스턴스가 2개가 있을 때 어느 인스턴스로 갔는지 알 수 있는 방법은?

log.info("Server port = {}",request.getServerPort());
env.getProperty("local.server.port"));
  • 첫 번째 방법은 메서드에서 HttpServletRequest 객체를 받아온다.
  • 두 번째 방법은 Environment 를 생성자 주입을 통해 가져온다.
  • 이렇게 하고 실행하면 로그로 어떤 포트 번호로 어느 인스턴스로 갔는지 알 수 있다.
  • 실제로 실행해보면 번갈아가면서 호출이 된다.
  • 즉 gateway 안에는 라우팅 기능 및 Load Balancer 기능이 포함되어 있다.

참고자료

profile
덕업일치

0개의 댓글