쵼쥬
쵼쥬의 개발공부 TIL
쵼쥬
전체 방문자
오늘
어제
  • 분류 전체보기 (276)
    • 코딩테스트 (192)
      • [알고리즘] 알고리즘 정리 (7)
      • [백준] 코딩테스트 연습 (126)
      • [프로그래머스] 코딩테스트 연습 (59)
    • Spring (71)
      • [인프런] 스프링 핵심 원리- 기본편 (9)
      • [인프런] 스프링 MVC 1 (6)
      • [인프런] 스프링 MVC 2 (4)
      • [인프런] 실전! 스프링 부트와 JPA 활용1 (7)
      • [인프런] 실전! 스프링 부트와 JPA 활용2 (5)
      • [인프런] 실전! 스프링 데이터 JPA (7)
      • [인프런] 실전! Querydsl (7)
      • JWT (5)
      • [인프런] Spring Cloud (17)
      • [인프런] Spring Batch (4)
    • Java (6)
      • [Java8] 모던인자바액션 (4)
      • [부스트코스] 웹 백엔드 (2)
      • [패스트캠퍼스] JAVA STREAM (0)
    • CS (6)
      • 디자인 패턴과 프로그래밍 패터다임 (2)
      • 네트워크 (4)

블로그 메뉴

  • 홈

공지사항

인기 글

태그

  • Spring Data JPA
  • 타임리프
  • 누적합
  • 백준
  • 위클리 챌린지
  • 구현
  • 스프링
  • 프로그래머스
  • jpa
  • querydsl
  • 인프런
  • 알고리즘
  • MVC
  • 자바
  • spring
  • BFS
  • 백분
  • 부스트코스
  • 코딩테스트
  • 비트마스킹

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
쵼쥬

쵼쥬의 개발공부 TIL

API Gateway Service
Spring/[인프런] Spring Cloud

API Gateway Service

2022. 6. 17. 18:48

API Gateway Service

사용자가 설정한 라우팅 설정에 따라서 각각 엔드 포인트로 클라이언트를 대신해서 요청하고 응답을 받으면 다시 클라이언트에게 전달을 하는 프록시 역할을 하게 된다. 시스템의 내부 구조는 숨기고 외부 요청에 대해서 적절한 형태로 가공해서 응답할 수 있는 장점이 있다.

 

마이크로 서비스 간 직접 통신

클라이언트는 오직 게이트 웨이만 상대하게 된다.

사용자 지정 서비스로 구현된 API 게이트 웨이 사용

  • 인증 및 권한 부여
  • 서비스 검색 통합 (마이크로 서비스 검색 통합)
  • 응답 캐싱
  • 정책, 회로 차단기 및 QoS 다시 시도 
  • 속도 제한
  • 부하 분산 (로드 밸런싱)
  • 로깅, 추적, 상관 관계 
  • 헤더, 쿼리 문자열 및 청구 변화 
  • IP 허용 목록에 추가

 

Netflix Ribbon과 Zuul

Netflix Ribbon 

Spring Cloud에서 MSA간 통신

1. RestTemplate

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

2. Feign Client

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

 

Ribbon -> 비동기가 되지 않아서 최근에 잘 사용하지 않음

Client side Load Balancer (클라이언트 측에서 마이크로 서비스 주소를 관리)

  • 서비스 이름으로 호출 (ip, port 알지 않아도 됨)
  • Health Check

 

Spring Cloud Ribbon 은 Spring boot 2.4에서 Maintenance 상태 (모듈에 더 이상 새로운 기능을 추가하지 않음. 지원 X)

 

Netflix Zuul

게이트 웨이 역할 (클라이언트는 Zuul을 상대. Routing, API gateway)

 

Spring Cloud Zuul 은 Spring boot 2.4에서 Maintenance 상태 (모듈에 더 이상 새로운 기능을 추가하지 않음. 지원 X)

 

 

Ribbon 과 Zuul이 어떤 역할을 하는지 Spring Boot 버전을 낮춰서 확인

 

Netflix Zuul 구현

First Service, Second Serivce

  • Spring Boot : 2.3.8
  • Dependencies : Lombok, Spring Web, Eureka Discovery Client
server:
  port: 8081

spring:
  application:
    name: my-first-service

eureka:
  client:
    fetch-registry: false
    register-with-eureka: false
server:
  port: 8082

spring:
  application:
    name: my-second-service

eureka:
  client:
    fetch-registry: false
    register-with-eureka: false

 

Zuul Service

  • Spring Boot : 2.3.8
  • Dependencies : Lombok, Spring Web, Zuul
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
package com.example.zuulservice.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

@Slf4j
@Component
public class ZuulLoggingFilter extends ZuulFilter {

    @Override
    public Object run() throws ZuulException {
        log.info("**************** printing logs: ");

        RequestContext ctx = RequestContext.getCurrentContext();    // Request 정보를 가진 최상위 객체
        HttpServletRequest request = ctx.getRequest();  // Request 정보
        log.info("**************** " + request.getRequestURI());

        return null;
    }

    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 1;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }
}

ZuulFilter

비지니스 서비스의 사전 처리(예: 인증), 사후 처리(예: 로깅)를 해줌

 

run 메서드 - 실제 어떤 동작을 하는지 

filterType 메서드 - pre(사전 필터), post(사후 필터)

 

 

Spring Cloud Gateway

Zuul에서 호환성 문제가 있던 비동기 처리 가능해졌다.

 

apigateway-service

  • Spring Boot : Zuul 사용하지 않기 때문에 최신 버전 사용 가능
  • DevTools, Eureka, Discovery Client, Gateway
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/**
Netty started on port 8000 -> 기존에는 톰캣서버가 작동했는데 gateway에선 비동기 서버 작동

http://localhost:8000/first-service/welcome으로 요청이 들어오면 인스턴스 서버에 넘겨줄때 기존과 다르게 그대로 넘겨주게 되어 404 에러 발생

-> 인스턴스 서버들의 RequestMapping도 first-service, second-service로 설정해줘야 한다.

 

 

Spring Cloud Gateway - Filter

filter 작업 방식 두가지

  • java code
  • property(application.yml)

java code

package com.config;

import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

addRequestHeader - 클라이언트가 header에 넣지 않은 값이 필요하다면 넣을 수 있다.

addResponseHeader - 클라이언트에게 값을 넘겨줄때 헤더값을 넣어서 반환할 수 있다.

 

property

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

 

 

@RequestHeader 로 헤더에 어떤 값이 들어있는지 확인

@GetMapping("/message")
public String message(@RequestHeader("first-request") String header) {
    log.info(header);
    return "Hello World in First Service.";
}
@GetMapping("/message")
public String message(@RequestHeader("second-request") String header) {
    log.info(header);
    return "Hello World in Second Service.";
}

Eureka의 정확한 용도는 Service Registry와 Discovery의 역할이며, Gateway는 Routing을 처리한다고 보는 것이 맞습니다. Load Balancer를 위해서는 외부의 Service Mesh를 사용하시거나, 기존의 Ribbon을 대신하는 Spring Cloud LoadBalancer(https://spring.io/blog/2020/03/25/spring-tips-spring-cloud-loadbalancer)를 사용해 보시면 좋을 것 같습니다. 

 

Custom Filter 

package com.example.apigatewayservice.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

@Component
@Slf4j
public class CustomFilter extends AbstractGatewayFilterFactory<CustomFilter.Config> {
    // CustomFilter는 상속 받아야 함

    public CustomFilter() {
        super(Config.class);
    }

    public static class Config {
        // configuration 정보있다면 내부 클래스 이용해서 넣을 수 있음
    }

    @Override
    public GatewayFilter apply(Config config) {
        // Custom PreFilter

        return (exchange, chain) -> {
            // netty 라는 비동기 방식의 서버 사용 중 (tomcat X)
            // ServerHttp 라는 객체를 사용 (ServletHttp X)
            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(() -> {
                // chain.filter(exchange) 필터 적용
                // 이전에 썼던 ServletRequest, Response 가 아니라 스프링 5에서 지원되는 Web Flux 를 이용해서
                // 서버를 구축할 때 반환값으로 Mono데이터 타입을 사용할 수 있다. (데이터타입을 하나 주겠다는 의미)
                // Mono : 0 ~ 1개 데이터 전달, Flux : 0 ~ N개 데이터 전달
                log.info("Custom Post Filter: response code -> {}", response.getStatusCode());
            }));
        };
    }
}

 chain.filter(exchange) 어떤 필터를 적용할지 선택

 

application.yml 에도 넣을 수 있고 config 파일에도 넣을 수 있는데 여기선 application.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/
          filters:
#            - AddRequestHeader=first-request, first-request-header2
#            - AddResponseHeader=first-response, first-response-header2
            - CustomFilter
          predicates:
            - Path=/first-service/**
        - id: second-service
          uri: http://localhost:8082/
          filters:
#            - AddRequestHeader=second-request, second-request-header2
#            - AddResponseHeader=second-response, second-response-header2
            - CustomFilter
          predicates:
            - Path=/second-service/**

 

Global Filter

일반적인 Filter와 동일한 방법으로 만들지만 모든 route에 공통으로 적용되어야 해서 개별적으로 적용하지 않고 한번에 적용 가능

모든 Filter 중에 가장 먼저 실행되고 가장 마지막에 종료된다.

 

package com.example.apigatewayservice.filter;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

@Component
@Slf4j
public class GlobalFilter extends AbstractGatewayFilterFactory<GlobalFilter.Config> {

    public GlobalFilter() {
        super(Config.class);
    }

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

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

Config 파라미터 preLogger, postLogger 정보는 application.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:
      default-filters:
        - name: GlobalFilter
          args:
            baseMessage: Spring Cloud Gateway Global Filter
            preLogger: true
            postLogger: true
      routes:
        - id: first-service
          uri: http://localhost:8081/
          filters:
#            - AddRequestHeader=first-request, first-request-header2
#            - AddResponseHeader=first-response, first-response-header2
            - CustomFilter
          predicates:
            - Path=/first-service/**
        - id: second-service
          uri: http://localhost:8082/
          filters:
#            - AddRequestHeader=second-request, second-request-header2
#            - AddResponseHeader=second-response, second-response-header2
            - CustomFilter
          predicates:
            - Path=/second-service/**

application.yml 파일에 있는 정보는 내장되어 있는 데이터라서 데이터가 변경되면 새롭게 갱신해줘야 함

but 외부에 있는 데이터를 반영할 수 도 있다. 추후에 할 예정

 

Logging Filter

package com.example.apigatewayservice.filter;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.OrderedGatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

@Component
@Slf4j
public class LoggingFilter extends AbstractGatewayFilterFactory<LoggingFilter.Config> {

    public LoggingFilter() {
        super(Config.class);
    }

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

    @Override
    public GatewayFilter apply(Config config) {

        //        return (exchange, chain) -> {
//            ServerHttpRequest request = exchange.getRequest();
//            ServerHttpResponse response = exchange.getResponse();
//
//            log.info("Logging Filter baseMessage: {}", config.getBaseMessage());
//
//            if (config.isPreLogger()) {
//                log.info("Logging Filter Start: request id -> {}", request.getId());
//            }
//
//            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
//                if (config.isPostLogger()) {
//                    log.info("Logging Filter End: response code -> {}", response.getStatusCode());
//                }
//            }));
//        };

        /*
        람다식이 어떻게 구현되어 있는지 확인
        GatewayFilter를 반환해야 함
        GatewayFilter는 인스턴스라서 new OrderedGatewayFilter()로 생성해야함

        spirng web flex는 servlet을 지원하지 않아서 serverRequest, serverResponse를 사용해야함
         */

        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 Start: request id -> {}", request.getId());
            }

            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                if (config.isPostLogger()) {
                    log.info("Logging Post Filter End: response code -> {}", response.getStatusCode());
                }
            }));
        }, Ordered.HIGHEST_PRECEDENCE);

        return filter;
    }
}
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:
      default-filters:
        - name: GlobalFilter
          args:
            baseMessage: Spring Cloud Gateway Global Filter
            preLogger: true
            postLogger: true
      routes:
        - id: first-service
          uri: http://localhost:8081/
          filters:
#            - AddRequestHeader=first-request, first-request-header2
#            - AddResponseHeader=first-response, first-response-header2
            - CustomFilter
          predicates:
            - Path=/first-service/**
        - id: second-service
          uri: http://localhost:8082/
          filters:
#            - AddRequestHeader=second-request, second-request-header2
#            - AddResponseHeader=second-response, second-response-header2
            - name: CustomFilter
            - name: LoggingFilter # 변수가 있으면 name으로 설정해줘야 한다.
              args:
                baseMessage: Hi, there.
                preLogger: true
                postLogger: true
          predicates:
            - Path=/second-service/**

원래는 Global -> Custom -> Logging 순으로 진행되어야 하지만 Ordered.HIGHEST_PRECEDENCE로 우선순위를 잡아서 위와 같은 실행 순서로 실행된다.

Ordered.HIGHEST_PRECEDENCE (우선순위 가장 높음) < - > Ordered.LOWEST_PRECEDENCE (우선순위 가장 낮음)

 

Spring Cloud Gateway - Eureka 연동

Eureka라는 naming service에 지금까지 등록한 service 등록

 

Cloud Gateway 어플리케이션을 Eureka Server에 등록하는 이유??

spring cloud gateway에서 사용자의 요청 정보를 해당 서비스로 직접 이동 (http://~) 하는 경우라면, spring cloud gateway와 registry service(유레카 등)는 서로의 연결되지 않아도 됩니다. 대신, gateway에서 서비스의 이동을 직접하는 방식이 아니라, registry service에 전달해서 해야 할 경우 (라우팅 정보 등록 시 LB라고 등록합니다)에는 spring cloud gateway와 registry service가 서로 연결되어 있어야 합니다. 즉, 말씀하신 내용처럼 fetch-registry 설정으로 유레카로부터 주기적으로 서비스 인스턴스들의 정보를 갱신해 줘야 합니다. 라우팅 정보에 LB를 사용하게 되면, 유레카에 등록된 서비스가 여러 개의 서비스로 구성되어 있는 경우, Load Balancing (부하 분산) 처리를 지원합니다. 

 

pom.xml(apigateway-service, first-service, second-service 모두)

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

application.yml(apigateway-service, first-service, second-service 모두)

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

application.yml (apigateway-service)

uri 설정

http 프로토콜을 이용하는 것이 아니라 Eureka Server로 가서 요청하게 한다.

lb -> DiscoverySerivce으로 가서 해당하는 이름을 찾겠다.

uri: lb://MY-FIRST-SERVICE
uri: lb://MY-SECOND-SERVICE

 

Eureka Server 기동 후 실행 (discoveryservice)

 

Spring Cloud Gateway - Load Balancer

First Service, Second Service를 각각 2개씩 기동해서 로드밸런스 동작

방법

1. VM Options -> -Dserver.port=[다른 포트] (인텔리제이 내부에서)

2. $ mvn spring-boot:run -Dspring-boot.run.jvmArguments='-Dserver.port=9003' (메이븐 실행)

3. $mvn clean compile package  (메이븐 컴파일 해서 jar 파일 실행)

    $ java -jar -Dserver.port=9004 ./target/user-service-0.0.1-SNAPSHOT.jar

 

 

 

포트번호를 0으로 바꿔서 random port 사용 -> Eureka Server에서 인스턴스 하나밖에 확인되지 않음

인스턴스 Id 추가해서 해결 

server:
  port: 0

spring:
  application:
    name: my-first-service

eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://localhost:8761/eureka
  instance:
    instance-id: ${spring.cloud.client.ip-address}:${spring.application.instance_id:${random.value}}
    prefer-ip-address: true

 

둘 중에 어떤 인스턴스가 실행될까??

Environment(환경 변수) 로 확인

package com.example.firstservice;

import lombok.extern.slf4j.Slf4j;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.util.Collections;
import java.util.Enumeration;


@RestController
@RequestMapping("/first-service")
@Slf4j
public class FirstServiceController {
    Environment env;

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

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

    @GetMapping("/message")
    public String message(@RequestHeader("first-request") String header) {
        log.info(header);
        return "Hello World in First Service.";
    }

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

        log.info("spring.cloud.client.hostname={}", env.getProperty("spring.cloud.client.hostname"));
        log.info("spring.cloud.client.ip-address={}", env.getProperty("spring.cloud.client.ip-address"));

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

호출할 때마다 다른 포트번호로 실행되는거 확인할 수 있다.

 

gateway 를 사용하면 라우팅, 로드밸런서 기능을 사용할 수 있다.

'Spring > [인프런] Spring Cloud' 카테고리의 다른 글

Catalogs, Orders Microservice  (0) 2022.06.19
Users Microservice  (0) 2022.06.18
E-commerce 어플리케이션  (0) 2022.06.18
Service Discovery  (0) 2022.05.29
Microservice와 Spring Cloud 소개  (0) 2022.05.28
    'Spring/[인프런] Spring Cloud' 카테고리의 다른 글
    • Users Microservice
    • E-commerce 어플리케이션
    • Service Discovery
    • Microservice와 Spring Cloud 소개
    쵼쥬
    쵼쥬

    티스토리툴바