본문 바로가기
Programming/Java, Spring

[Spring Cloud] API Gateway Service / routing, filter

by Renechoi 2023. 5. 31.

API Gateway Service의 역할 : 

- 클라이언트 대신 엔드포인트를 요청하고 요청을 받으면 다시 클라이언트에게 전달해주는 프락시 역할을 해준다. 

- 시스템 내부 구조는 숨기고 요청에 대해 적절히 가공해서 응답할 수 있다는 장점 

 

=> 클라이언트가 직접적으로 microservice를 호출하지 않고 게이트웨이만 상대하도록 한다. 

 

- 인증 및 권한 부여

- 서비스 검색 통합

- 응답 캐싱

- 정책, 회로 차단기

- 속도 제한

- 부하 분산
- 로깅, 추적, 상관관계

- 헤더, 쿼리 문자열 및 청구 변환

- ip 허용 목록에 추가 

 

 

 

Netflix Ribbon

 

Spring cloud 에서의 MSA 간 통신 

 

1) RestTemplate

 - 자주 사용 : 인스턴스 만들고 get/post 등의 요청으로 보낼 수 있다. 

RestTemplate restTemplate = new RestTemplate();
restTemplate.getForObject("http://localhost:8080/", User.class, 200);

2) Feign Client 

- Spring cloud에서는 Feign Client라는 API를 이용할 수 있다.

@FeignClient("stores")
public interface StoreClient {@RequestMapping(method=ReqeustMethod.GET, value="/stores")
   List<Store> getStores();
   }

인터페이스 생성

-> 애노테이션으로 호출하고 싶은 microservice 등록 

-> 따로 주소를 다 쓰지 않고, 마치 자신의 서비스에서 쓰듯이 접속할 수 있다. 

 

 

Ribbon: Client side Load Balancer 

- 서비스 이름으로 호출

- 중간에 있는 API Gateway를 클라이언트 사이드로 옮겨온 방식 

-> 비동기 방식으로 호출이 잘 안돼서 최근 사용 드물 

-> Spring boot 2.4 -> maintenance 

 

 

Netflix Zuul 

- API Gateway 역할 

 

 

ribbon과 zuul에 대한 대안 : 

Spring Boot 2.4부터는 Spring Cloud Gateway가 공식적으로 권장되는 API 게이트웨이로 소개되었다. 따라서 Spring Boot 2.7.12 버전을 사용하는 경우, Spring Cloud Gateway를 대안으로 사용하는 것이 좋다. Spring Cloud Gateway는 Netflix Zuul과 비슷한 역할을 수행하면서도 더욱 유연하고 성능에 최적화되어 있다.

 

 

 


 

API Gataway 작동 방식을 확인하기 위해 버전을 낮춰서 zuul을 사용해보자. 

 

 

먼저 서비스를 사용할 서버 두개를 생성한다. 

 

first-service 

second-service 

 

 

lombok만 받고 컨트롤러만 구현해준다.

 

@RestController
@RequestMapping("/")
public class FirstServiceController {
   @GetMapping("/welcome")
   public String welcome(){
      return "welcome to the First service";
   }
}

yml 파일에 각각 포트를 8081과 8082로 설정하였다. 

 

 

 

 

 

zuul 서버를 생성한다.

현재 2.4 보다 미만 버전을 spring.io에서 제공하지 않기 때문에 직접 버전을 낮추고 해당하는 version에 맞는 의존성을 추가해주어야 했다. 

 

plugins {
   id 'java'
   id 'org.springframework.boot' version '2.3.8.RELEASE'
   id 'io.spring.dependency-management' version '1.0.10.RELEASE'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
   mavenCentral()
}

dependencies {
   implementation 'org.springframework.boot:spring-boot-starter-web'
   implementation 'org.springframework.boot:spring-boot-starter'
   testImplementation 'org.springframework.boot:spring-boot-starter-test'
   // https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-netflix-zuul
   implementation group: 'org.springframework.cloud', name: 'spring-cloud-netflix-zuul', version: '2.2.8.RELEASE'
}

tasks.named('test') {
   useJUnitPlatform()
}

 

yml

 

server:
  port: 8000

spring:
  application:
    name: my-zuul-service

zuul:
  routes:
    first-service:
      path: /first-service/**
      url: http://localhost:8081

    second-service:
      path: /second-service/**
      url: http://localhost:8082

 


 

 

api gateway 프로젝트 생성하기 

 

plugins {
   id 'java'
   id 'org.springframework.boot' version '2.7.12'
   id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

configurations {
   compileOnly {
      extendsFrom annotationProcessor
   }
}

repositories {
   mavenCentral()
}

ext {
   set('springCloudVersion', "2021.0.7")
}

dependencies {
   implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
   implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
   compileOnly 'org.projectlombok:lombok'
   annotationProcessor 'org.projectlombok:lombok'
   testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

dependencyManagement {
   imports {
      mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
   }
}

tasks.named('test') {
   useJUnitPlatform()
}

 

 

api gateway는 tomcat이 아닌 Netty 라는 Was 를 사용한다. 특징은 비동기 방식으로 운영된다는 점이다. 

 

 

 

 

 

앞서 구현한 first-service의 컨트롤러 매핑 부분을 바꿔준다. 

 

@RestController
@RequestMapping("/first-service")
public class FirstServiceController {

   @GetMapping("")
   public String home(){
      return "redirect:/welcome";
   }


   @GetMapping("/welcome")
   public String welcome(){
      return "welcome to the First service";
   }
}

 

 

yml 파일 

 

server:
  port: 8000

eureka:
  client:
    register-with-eureka: false
    fetch-registry: false
    service-url:
      defaultZone: http://localhost:8761/eureka

spring:
  application:
    name: apigateway-service
  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/**

 

 

8000 번 포트에서 호출시 다음과 같이 라우팅 되는 것을 확인 할 수 있다. 

 

 

 

 

클라우드 필터는 다음과 같은 흐름으로 진행된다. 

 

 

 

 

 

자바 코드를 통해 filter를 구현하기 

 

RouteLocator를 리턴해주면 yml 파일에 적용한 값을 자바 코드로 구현할 수 있다. 

이때 header에 request와 response 값을 넣어줘보자. 

 

 

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://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();
}

=> 앞서 작성한 yml 파일을 자바 코드로 옮긴 코드 

 

 

 

 

filter 기능을 yml 파일에도 작성할 수 있다. 

 


spring:
  application:
    name: apigateway-service
  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

 

 

 

 

 

Custom Filter 만들기 


@Component
@Slf4j
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
            // 비동기 방식의 단일 값을 전달할 때 사용하는 Mono
            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                log.info("Custom POST filter: response code -> {}", response.getStatusCode());
            }));
        };
    }

    // 반환 값을 만들어주기 위해 필요하다.
    public static class Config {
        // configuration properties를 추가할 수 있다.
    }
}

 

yml 파일에서 설정한 것을 지워주고 customfilter를 추가한다. 

 


spring:
  application:
    name: apigateway-service
  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
            - CustomFilter
        - id: second-service
          uri: http://localhost:8082/
          predicates:
            - Path=/second-service/**
          filters:
#            - AddRequestHeader=second-request, second-request-header2
#            - AddResponseHeader=second-response, second-response-header2
            - CustomFilter

 

 

GlobalFilter 구현하기 

공통적으로 실행될 수 있다. 

 

GlobalFilter는 맨 처음과 마지막에 실행된다. 

 

필터 작동 순서 :

gateway client > gateway handler > global filter > custom filter > logging filter > proxied Service

 

 

 

로깅 필터도 마찬가지 방식으로 구현할 수 있으며 우선순위를 주어 순서를 변경할 수 있다. 

 


@Component
@Slf4j
public class LoggingFilter extends AbstractGatewayFilterFactory<LoggingFilter.Config> {
    public LoggingFilter() {
        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: request id -> {}", request.getId());
//            }
//            return chain.filter(exchange).then(Mono.fromRunnable(()->{
//                if (config.isPostLogger()) {
//                    log.info("Global Filter End: response code -> {}", response.getStatusCode());
//                }
//            }));
//        });
        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: request id -> {}", request.getId());
            }
            return chain.filter(exchange).then(Mono.fromRunnable(()->{
                if (config.isPostLogger()) {
                    log.info("Logging POST Filter: response code -> {}", response.getStatusCode());
                }
            }));
        }, Ordered.LOWEST_PRECEDENCE);

        return filter;
    }

    @Data
    public static class Config {
        private String baseMessage;
        private boolean preLogger;
        private boolean postLogger;
    }
}

 

 

 

요청과 응답 흐름을 다음과 같이 되도록 바꿔보자. 

 

 

클라이언트 요청을 API Gateway가 받아서 유레카 서버로 갔다가 찾아서 다시 gateway로 왔다가 dispatch 해준다. 

 

 

 

디스커버리 서버에 등록된 주소로 포워딩 시켜주도록 

first, second server를 eureka에 등록시켜 주고 

yml에도 localhost 대신 lb: 주소 방식으로 작성해준다. 

 

routes:
  - id: first-service
    uri: lb://MY-FIRST-SERVICE
    predicates:
      - Path=/first-service/**
    filters:
      - CustomFilter
  - id: second-service
    uri: lb://MY-SECOND-SERVICE

 

 

만약 First-service가 멀티로 작동중이면 

lb 옵션에 의해 해당 서버를 찾고 로드 밸런싱을 하여 인스턴스를 분배한다. 

 

 

 

이때 어떤 포트가 실행중인지를 확인해보고자 컨트롤러에 port를 보도록 코드를 간단하게 써보자. 

 

Environment env;

@Autowired
public FirstServiceController(Environment env) {
   this.env = env;
}

 

@GetMapping("/check")
public String check(HttpServletRequest request) {
   log.info("Server port={}", request.getServerPort());

   return String.format("Hi, there. This is a message from First Service on PORT %s"
      , env.getProperty("local.server.port"));
}

 

 

 

 

 

 


 

ref. 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

반응형