728x90
반응형
SMALL
반응형
SMALL

이전 Part 6 까지는 API Gateway Service는 Eureka에 등록하지 않았다. 이제 이 Gateway도 등록을 해야한다.

등록해서 Load Balancer를 사용해보자.

 

 

API Gateway Service의 application.yml

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

spring:
  application:
    name: api-gateway-service
  cloud:
    gateway:
      default-filters:
        - name: GlobalFilter
          args:
            baseMessage: Spring Global Filter
            preLogger: true
            postLogger: true
      routes:
        - id: user-service
          uri: lb://USER-SERVICE
          predicates:
            - Path=/user-service/**
          filters:
            - name: CustomFilter

기존에 GatewayService의 application.yml 파일은 eureka.client.register-with-eureka가 false, eureka.client.fetch-registry: false였는데 둘 다 true로 변경해주었다. 그래서 eureka에 이 서비스를 등록하고 eureka에 등록되어 있는 서비스들을 실시간으로 데이터 조회하게 된다.

 

그 다음, 중요한 부분인 spring.cloud.gateway.routes에 기록되는 각각의 서비스들의 uri다.

보면 user-service의 uri가 lb://USER-SERVICE로 변경되었는데 lb는 Load Balancer의 약자로 eureka에 등록된 Named Service 기반으로 서비스를 찾겠다는 뜻이고 그 서비스의 이름이 USER-SERVICE가 된다는 뜻이다. 

 

주의

서비스의 이름에 "_"가 들어가면 안된다. 나도 이거 때문에 한참 고생했는데 만약 있다면 저렇게 USER-SERVICE로 바꾸자.

 

기존에 eureka 서버를 띄우면 application name으로 등록된 서비스들의 이름이 보였다. (아래 사진을 참고)

이렇게 eureka에 등록된 서비스들의 이름을 가지고 Gateway가 실제 서비스의 uri를 찾아간다.

 

 

UserService의 application.yml 

server:
  port: 0
spring:
  application:
    name: user-service

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

server.port를 우선 0으로 지정해서 임의의 포트를 할당받게 했다. 실행할 때마다 번거롭게 포트를 새롭게 지정해 줄 필요 없이 원하는 만큼 서비스의 인스턴스를 실행할 수 있다. 실제로 서비스의 인스턴스를 두 개 정도 띄워보자. IntelliJ에서 인스턴스를 여러개 띄우는 방법은 이미 전 포스팅에서 설명했던 부분이니 넘어간다.

두 개를 띄우고 eureka 서버에 들어가보면 위 사진처럼 두개의 인스턴스가 기동되어 있음을 확인할 수 있다. 그리고 각 인스턴스의 포트를 신경쓰지 않고 Named 기반의 서비스를 Gateway에서 등록했으니 UserService로의 요청을 Gateway는 잘 전달해줄 수 있을 것이다.

 

 

Load Balancing 확인

실제로 유저 서비스는 인스턴스가 두개가 등록됐는데 외부 요청이 들어올 때마다 Gateway가 어떤 인스턴스에 요청을 보내는지 확인해보자. 그리고 어떤 방식으로 유저 서비스에 대한 요청을 Load Balancing해주는지도 확인해보자.

 

Postman으로 테스트해 볼 예정이고 그 전에 UserService의 코드를 좀 수정해야한다.

package com.example.tistoryuserservice.controller;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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;

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/user-service")
public class StatusController {
    private final Environment environment;

    @GetMapping("/welcome")
    public String welcomePage() {
        return "Welcome ! This is User Service.";
    }

    @GetMapping("/message")
    public String message(@RequestHeader("user-request") String header) {
        log.info("header: {}", header);
        return "header check";
    }

    @GetMapping("/check")
    public String check() {
        log.info("current port: {}", environment.getProperty("local.server.port"));
        return "Hi, check method !" + environment.getProperty("local.server.port");
    }
}

Environment 클래스를 주입한다. 이 클래스는 환경 변수에 접근할 수 있게 해주는 스프링에서 제공해주는 클래스이다. 이 클래스를 주입받고 check() method에 이 요청이 들어왔을 때 처리해주는 현재 서버의 포트를 찍어주게 변경했다. 

 

그리고 인스턴스를 두 개 띄웠으니까 각 인스턴스의 잡혀있는 포트를 확인해보자.

유레카 서버에서 보면 인스턴스 두 개에 각각 링크가 걸려있다. 각 링크를 클릭해보면 포트번호를 확인할 수 있는데 나의 경우 인스턴스 한 개는 56482, 56537로 잡혀있다.

 

이 두 개의 포트번호를 확인한 후 Postman을 열어 실제로 요청을 보내보자.

최초 요청을 보내면 응답은 포트번호 56482로 나온다. 즉, 56482로 띄워진 인스턴스가 이 요청을 처리했다는 의미인데 바로 한번 더 요청을 보내보자.

그 다음 요청은 56537로 잡힌 인스턴스가 이 요청을 처리했다. 이후에 계속 실행을 해보면 한번은 56482가 한번은 56537 인스턴스가 각 요청을 처리한다 이런 방식을 라운드로밍 방식이라고 하고 Load Balancer가 각 요청에 대해 띄워져 있는 인스턴스에게 한 번씩 요청을 전달해 처리하도록 수행해준다. 이게 Load Balancing이고 이를 Spring Cloud Gateway가 해준다.

 

 

728x90
반응형
LIST
728x90
반응형
SMALL
반응형
SMALL

외부의 요청이 Gateway를 통해 들어올 때 요청을 가로채서 추가적인 작업을 수행할수도 있다. 이 때 사용되는 방법이 Filter이다.

 

 

Filter 설정하기

config 파일 하나를 만들기 위해 config라는 패키지 하나를 만들고 그 안에 FilterConfig.java 파일을 만들었다.

package com.example.tistorygateway.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("/user-service/**")
                        .filters(f -> f.addRequestHeader("user-request", "user-request-header")
                                        .addResponseHeader("user-response", "user-response-header"))
                        .uri("http://localhost:8081"))
                .build();
    }
}

@Configuration 어노테이션을 추가하면 스프링 부트가 자동으로 이 클래스를 설정 처리해준다. 

그리고 그 클래스 내부에 @Bean으로 등록한 메소드를 하나 만들고 RouteLocatorBuilder 인스턴스를 build()한다.

이 RouteLocatorBuilder로 라우트 별 설정을 할 수 있다. 이 gateway로 들어오는 요청의 path가 /user-service로 시작하는 모든 요청에 대해 RequestHeader와 ResponseHeader를 추가한다. Header를 추가할 때 key/value쌍으로 추가하면 되는데 이렇게 추가를 할 수 있고 그 요청에 대한 URL을 http://localhost:8081로 보낸다는 의미에 uri()가 있다. 

 

이렇게 Config파일을 하나 만들고 서버를 재실행한 후 UserService에 새로운 Request Controller를 만들어보자.

 

 

UserService

package com.example.tistoryuserservice.controller;

import lombok.extern.slf4j.Slf4j;
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;

@Slf4j
@RestController
@RequestMapping("/user-service")
public class StatusController {

    @GetMapping("/welcome")
    public String welcomePage() {
        return "Welcome ! This is User Service.";
    }
	
    // 새롭게 추가한 부분
    @GetMapping("/message")
    public String message(@RequestHeader("user-request") String header) {
        log.info("header: {}", header);
        return "header check";
    }
}

새로운 GetMapping 메소드를 추가했고 Parameter로 RequestHeader를 받는 header 하나를 넣었다. 이렇게 파라미터에 요청 헤더를 받아올 수 있는데 이 헤더값을 로그로 찍은 후 "header check" 이라는 문자열을 응답 메세지로 반환한다.

 

이제 /user-service/message로 요청을 해서 filter가 동작하는지 확인해보자. 

 

 

 

Filter 동작 확인하기

위 사진처럼 /user-service/message로 요청을 보냈을 때 Response Headers에 User-Response라는 key가 담겨있는것을 확인할 수 있다. key에 대한 value는 'user-response-header'라고 명시되어 있음을 확인할 수 있다. 

 

여기서 한가지 더 확인할 수 있는건 Request Header에는 추가한 "user-request"가 들어가 있지 않는것을 볼 수 있는데 이는 filter를 거치기 전 request header에 정보이기 때문이다. 완벽하게 filter가 정상적으로 동작하고 있는것이다. 

그 filter를 거친 request header의 값은 어디서 확인하냐면 UserService의 Controller에서 파라미터에 넣었던 @RequestHeader를 통해 확인할 수 있다.

그러니까 흐름은 외부 요청 -> Gateway -> Filter -> UserService 이렇게 진행되고 위 브라우저에서는 외부 요청단계에 머물러 있는것이고 로그로 찍힌 상태에서는 UserService에 도달한 상태. 이렇게 외부 요청을 중간에 가로채서 추가적인 작업을 Filter를 통해서 수행할 수 있다.

 

 

이렇게 Filter를 설정할 수도 있고 application.yml 파일로도 Filter를 추가할 수 있다. 그것도 한 번 해 볼 생각이다.

 

 

application.yml 파일로 filter 추가하기

우선 위에서 등록해봤던 @Configuration을 주석처리하자. filter를 application.yml 파일로 설정할 생각이니까.

package com.example.tistorygateway.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("/user-service/**")
                        .filters(f -> f.addRequestHeader("user-request", "user-request-header")
                                        .addResponseHeader("user-response", "user-response-header"))
                        .uri("http://localhost:8081"))
                .build();
    }
}

 

주석 처리했으면 이제 application.yml 파일에 다음과 같이 filter를 추가한다.

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

spring:
  application:
    name: api-gateway-service
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: http://localhost:8081/user-service/**
          predicates:
            - Path=/user-service/**
          # application.yml 파일에 filter 추가
          filters:
            - AddRequestHeader=user-request, user-request-header2
            - AddResponseHeader=user-response, user-response-header2

추가한 필터가 하는 동작은 위에서 했던 내용과 정확히 일치한다.

Request Header에 key/value를 (user-request, user-request-header2).

Response Header에 key/value를 (user-response, user-response-header2). 

물론, value값만 차이를 구분하기 위해 뒤에 숫자 '2'를 추가했다. 

 

이제 application.yml 파일로 추가한 filter가 정상 동작하는지 또 한번 확인해보자.

 

 

application.yml에 추가한 filter 확인하기

확인해보면 역시 정상적으로 filter가 동작하고 있음을 확인할 수 있고 RequestHeader도 확인해보기 위해 로그를 확인한다.

 

 

 

CustomFilter 추가하기

사용자 정의 필터도 만들 수 있다. 이런 필터로는 사용자 인증 같은 인증된 유저만 접근가능하게 하는 경우가 제일 많이 사용되는데 이런 필터는 어떻게 만드는지 직접 구현해 볼것이다.

 

우선 filter라는 패키지를 하나 추가하고 CustomFilter.java 파일을 만든다.

package com.example.tistorygateway.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;

@Slf4j
@Component
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("[CustomFilter] Start: request id {}", request.getId());

            // Custom Post Filter
            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
               log.info("[CustomFilter] End: response code: {}", response.getStatusCode());
            }));
        };
    }

    public static class Config {
        // Put the configuration properties
    }
}

@Component 어노테이션을 사용해서 이 클래스를 스프링에 빈으로 등록하고, AbstractGatewayFilterFactory를 상속받는다.

apply() 함수 안에 실제 필터로 무언가 할 내용을 입력하면 된다. 위 예시에서는 필터가 시작하는 시점과 끝나는 시점의 로그를 출력해 필터의 적용 및 시작과 끝을 좀 더 보기 수월하게 했다.

 

이렇게 설정을 했으면 이 필터를 적용해야한다. 커스텀 필터는 글로벌 필터가 아니기 때문에 필요한 서비스마다 필터 설정을 걸어주면 된다.

application.yml 파일에서 필터를 설정해주자.

 

 

Custom Filter 적용하기

application.yml 파일에서 방금 만든 CustomFilter를 추가한다.

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

spring:
  application:
    name: api-gateway-service
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: http://localhost:8081/user-service/**
          predicates:
            - Path=/user-service/**
          filters:
#            - AddRequestHeader=user-request, user-request-header2
#            - AddResponseHeader=user-response, user-response-header2
            - CustomFilter

spring.cloud.gateway.routes.filters에 CustomFilter를 추가했고 그 전에 입력한 AddRequestHeader와 AddResponseHeader는 주석처리했다. 이렇게 설정한 후 재실행 시켜서 다시 UserService에 요청을 보내보자.

 

UserService가 응답할 수 있는 어떤 요청도 상관없이 요청을 보내보면 gateway service에서 확인할 수 있는 로그가 있다.

필터가 적용된 UserService에 대한 요청이 들어왔을 때 찍힌 로그가 보인다. 이렇듯 사용자 정의 필터를 원하는 서비스마다 적용시킬 수 있다.

 

 

 

GlobalFilter 추가하기

GlobalFilter는 gateway service로부터 들어오는 모든 요청에 대해 필터를 적용하는 것이다.

이 또한 CustomFilter를 만드는것과 비슷하게 만들 수 있다.

 

filter 패키지 안에 GlobalFilter.java 파일을 만들고 다음과 같이 작성했다.

package com.example.tistorygateway.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;

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

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

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            ServerHttpResponse response = exchange.getResponse();
            log.info("[GlobalFilter] baseMessage: {}", config.getBaseMessage());

            if (config.isPreLogger()) {
                log.info("[GlobalFilter] Start: request id {}", request.getId());
            }

            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                if (config.isPostLogger()) {
                    log.info("[GlobalFilter] End: response code: {}", response.getStatusCode());
                }
            }));
        };
    }

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

GlobalFilter는 CustomFilter 만들 때와 거의 유사하다. 똑같다고 봐도 되는데 코드에서 달라지는 부분은 Config 클래스에 properties가 추가됐다. baseMessage, preLogger, postLogger 세 개의 필드를 추가했고 이 값은 application.yml 파일에서 지정해 줄 것이다.

 

 

GlobalFilter 적용하기

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

spring:
  application:
    name: api-gateway-service
  cloud:
    gateway:
      default-filters:
        - name: GlobalFilter
          args:
            baseMessage: Spring Global Filter
            preLogger: true
            postLogger: true
      routes:
        - id: user-service
          uri: http://localhost:8081/user-service/**
          predicates:
            - Path=/user-service/**
          filters:
#            - AddRequestHeader=user-request, user-request-header2
#            - AddResponseHeader=user-response, user-response-header2
            - CustomFilter

application.yml 파일에서 spring.cloud.gateway.default-filters에 GlobalFilter가 추가됐음을 확인할 수 있다. 이렇게 default-filters로 추가하면 어떤 라우트가 됐던간 이 gateway를 통과하는 모든 요청은 저 필터를 거친다. 

그리고, args로 Config 클래스에서 만든 세 가지 필드 baseMessage, preLogger, postLogger 값을 설정했다. 

 

이렇게 작성하고 gateway-service를 재실행해서 UserService에 요청을 날려보자. 그럼 gateway service에서 이런 로그를 확인할 수 있다. 

보면 GlobalFilter가 가장 먼저 시작하고 GlobalFilter가 끝나기 전 CustomFilter가 동작해서 끝나고 난 후 GlobalFilter가 마지막으로 끝난다. 모든 필터는 이렇게 동작한다. GlobalFilter로 설정한 필터가 제일 먼저 시작해서 제일 나중에 끝난다.

 

 

 

LoggingFilter 추가하기

필터를 하나 더 추가해서 적용했을 때 필터의 우선순위에 따라 필터가 적용되는 순서가 달라짐을 확인해보고 적절하게 사용할 수 있도록 해보자. CustomFilter, GlobalFilter를 만든 패키지에 LoggingFilter.java 파일을 만든다.

package com.example.tistorygateway.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;

@Slf4j
@Component
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("[GlobalFilter] baseMessage: {}", config.getBaseMessage());

            if (config.isPreLogger()) {
                log.info("[GlobalFilter] Start: request id {}", request.getId());
            }

            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                if (config.isPostLogger()) {
                    log.info("[GlobalFilter] End: response code: {}", response.getStatusCode());
                }
            }));
        };*/


        GatewayFilter filter = new OrderedGatewayFilter((exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            ServerHttpResponse response = exchange.getResponse();

            log.info("[LoggingFilter]: baseMessage: {}", config.getBaseMessage());
            if (config.isPreLogger()) {
                log.info("[LoggingFilter]: Start request id {}", request.getId());
            }

            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                if (config.isPostLogger()) {
                    log.info("[LoggingFilter]: End response code {}", response.getStatusCode());
                }
            }));
        }, Ordered.LOWEST_PRECEDENCE);
        return filter;
    }

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

이번에는 Lambda식이 아니고 인스턴스 생성 후 인스턴스를 리턴하는 방식으로 구현해보자. 정확히 같은 내용인데 이렇게 된 코드를 Lambda 표현식으로도 사용할 수 있음을 이해하기 위해서 이렇게 작성했다.

 

다른건 다 똑같고 OrderedGatewayFilter()의 두번째 인자로 Ordered.LOWEST_PRECEDENCE를 적용하면 이 LoggingFilter가 가장 나중에 실행된다. Ordered.HIGHEST_PRECEDENCE도 있는데 이는 GlobalFilter보다도 더 먼저 실행된다. 그래서 그 차이를 확인해보자.

 

이 필터를 만들었으면 application.yml 파일에 적용해야한다. 

 

 

 

LoggingFilter 적용하기

 

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

spring:
  application:
    name: api-gateway-service
  cloud:
    gateway:
      default-filters:
        - name: GlobalFilter
          args:
            baseMessage: Spring Global Filter
            preLogger: true
            postLogger: true
      routes:
        - id: user-service
          uri: http://localhost:8081/user-service/**
          predicates:
            - Path=/user-service/**
          filters:
#            - AddRequestHeader=user-request, user-request-header2
#            - AddResponseHeader=user-response, user-response-header2
            - name: CustomFilter
            - name: LoggingFilter
              args:
                baseMessage: Logging Filter
                preLogger: true
                postLogger: true

어떠한 arguments도 가지지 않는 필터라면 그냥 필터 이름만 작성하면 되는데 그게 아니라면 name, args 키를 작성해줘야한다. 

이렇게 작성한 후 실행해서 gateway로 요청을 보내면 다음과 같은 로그가 찍힌다.

gateway에 요청을 보내고 로그를 확인해보면 다음과 같다.

GlobalFilter -> CustomFilter -> LoggingFilter 순으로 실행되고 이 역순으로 필터가 종료된다.

이렇게 필터에 우선순위를 최하위로 선언하면 가장 마지막에 실행이 되고 이번엔 최우선으로 선언해본다.

package com.example.tistorygateway.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;

@Slf4j
@Component
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("[GlobalFilter] baseMessage: {}", config.getBaseMessage());

            if (config.isPreLogger()) {
                log.info("[GlobalFilter] Start: request id {}", request.getId());
            }

            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                if (config.isPostLogger()) {
                    log.info("[GlobalFilter] End: response code: {}", response.getStatusCode());
                }
            }));
        };*/


        GatewayFilter filter = new OrderedGatewayFilter((exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            ServerHttpResponse response = exchange.getResponse();

            log.info("[LoggingFilter]: baseMessage: {}", config.getBaseMessage());
            if (config.isPreLogger()) {
                log.info("[LoggingFilter]: Start request id {}", request.getId());
            }

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

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

Ordered.HIGHEST_PRECEDENCE로 두번째 인자를 추가해서 다시 실행해보면 다음과 같은 로그가 찍힌다.

이번엔 LoggingFilter가 가장먼저 실행됐다. 이렇게 필터의 우선순위를 지정할 수도 있다.

 

728x90
반응형
LIST
728x90
반응형
SMALL
반응형
SMALL

Spring Cloud Netflix Eureka는 Service discovery tool을 말한다. Service discovery는 분산 시스템에서 각 인스턴스(서비스)들을 등록하고 관리해 주는데 관리해 준다는 건 외부에서 요청이 들어올 때 그 요청을 처리할 수 있는 인스턴스(서비스)가 어떤 서비스인지를 찾아주는 것을 포함한다. 

위 그림에서 Netflix Eureka는 API Gateway 바로 다음 단계에 존재하는데, 서비스에 외부 요청이 들어오면 API Gateway는 요청을 받아 해당 요청을 처리할 수 있는 서비스를 찾기 위해 Eureka에게 물어본다. Eureka는 해당 요청을 처리할 수 있는 서비스가 본인한테 등록된 게 있는지 확인 후 있다면 해당 서비스에게 요청을 전달한다. 이렇게 각 서비스들을 관리하고 등록하는 작업을 하는 게 Service discovery고 Spring에서는 Netflix Eureka를 사용할 수 있다. 

 

위 그림에서 각 서비스는 각기 다른 서버에서 구현될 수도 있고 같은 서버내에 포트번호를 다르게 설정하여 동시에 띄울 수 있다. 그에 따라 각 서비스별 호출 URL이 달라질 수 있음을 그림에서 표현한다. 이제 이 Service discovery를 직접 구현해 보자.

 

 

Spring Eureka Server 생성

IntelliJ IDEA를 이용해서 프로젝트를 생성할 거다. 우선 New Project로 프로젝트를 만들기 시작하면 좌측 Generators 섹션에 Spring Initializr가 보인다.

여기서 나는 다음과 같이 설정을 했다.

Name, Location은 원하는 대로 설정하면 되고 Language는 Java를 Type은 Maven을 설정했다.

 

Group은 회사라면 회사 도메인을 거꾸로 쓰는 게 일반적이다. 여기서는 그냥 com.example로 설정했다.

Artifact는 애플리케이션 이름을 작성하면 되며

Package name은 Group.Artifact를 이어 붙여서 설정한다. 

JDK는 20으로 설정했고 Java 버전은 17로 설정했다.

Packaging은 Jar를 선택하면 된다.

 

Next를 누르면 Spring Boot 버전과 Dependencies를 설정할 수 있다.

Spring Boot는 3.1.4 버전을 선택했고 좌측 Dependencies에서 Spring Cloud Discovery > Eureka Server를 선택한다. 

선택하면 우측 Added dependencies 항목에 선택한 dependencies들이 추가되는 것을 확인할 수 있다. 

 

Create 누르면 프로젝트가 생성된다.

 

pom.xml

프로젝트가 생성되면 가장 먼저 확인할 것은 pom.xml 파일이다. 내가 선택한 Eureka dependency가 잘 추가되었는지, 다른 설정에 이상은 없는지 확인해 보자. 

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.1.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>discovery-service</artifactId>
    <version>0.0.1</version>
    <name>discovery-service</name>
    <description>discovery-service</description>
    <properties>
        <java.version>17</java.version>
        <spring-cloud.version>2022.0.4</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

spring-cloud-starter-netflix-eureka-server가 dependency로 잘 등록되어 있는 것을 확인할 수 있으며 아래쪽 spring-cloud-dependencies로 version이 ${spring-cloud.version}으로 명시된 것을 확인할 수 있는데 이는 위에 properties안에 <spring-cloud.version>2022.0.4<spring-cloud.version>로 세팅된 값을 가져온다.

 

문제없이 잘 등록된 것 같다.

 

@SpringBootApplication

다음으로 확인할 것은 현재 상태에서 유일하게 생성되어 있는 .java 파일이다. 이 파일에서 main()이 있고 스프링은 최초의 시작점을 이 파일로 시작하는데 그때 필요한 Annotation이 @SpringBootApplication이다. 이 어노테이션이 있는 파일을 Spring Boot가 찾아서 최초의 시작을 한다. 

package com.example.discoveryservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DiscoveryServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(DiscoveryServiceApplication.class, args);
    }

}

 

이 서버는 Eureka Server로 만들 거니까 위 클래스에 @EnableEurekaServer 어노테이션을 붙여주자.

package com.example.discoveryservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class DiscoveryServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(DiscoveryServiceApplication.class, args);
    }

}

이 밖에 추가적으로 해줄 일은 없다.

 

 

application.yml

src/main/resources경로에 보면 기본으로 application.properties 파일이 있을 건데. properties를. yml파일로 변경해서 사용할 거다.

둘 중 아무거나 사용해도 상관없다만. yml파일이 나는 더 좋다.

 

파일이름을 변경했으면 해당 파일에 설정값을 추가해야 한다.

server:
  port: 8761

spring:
  application:
    name: discoveryservice

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

server.port는 8761로 spring.application.name은 discoveryservice로 설정했다. 일반적인 설정 내용이고 중요한 부분은 eureka항목이다. 두 가지 설정을 해줬다. 

eureka.client.register-with-eureka: false

eureka.client.fetch-registry: false

Eureka server로 기동 할 서버인데 왜 client값을 설정해야 하는가에 대한 의문이 생기는데 이 내용은 spring boot가 기본적으로 eureka server를 띄우면 본인도 eureka에 서비스로 등록이 된다. 그러나 본인은 서버이기 때문에 eureka에서 서비스로 등록할 필요가 없기 때문에 본인은 client로 등록하지 않을 것을 명시하는 설정값이라고 생각하면 된다.

 

 

Started Eureka Server

이제 필수적으로 수행할 설정을 다 끝냈으니 서버를 시작해 보자. 서버를 시작하면 하단에 Console창에 아래 같은 로그가 출력되어야 한다.

2023-10-06T10:58:50.210+09:00  INFO 4334 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8761 (http) with context path ''
2023-10-06T10:58:50.212+09:00  INFO 4334 --- [           main] .s.c.n.e.s.EurekaAutoServiceRegistration : Updating port to 8761
2023-10-06T10:58:50.214+09:00  INFO 4334 --- [       Thread-9] e.s.EurekaServerInitializerConfiguration : Started Eureka Server
2023-10-06T10:58:50.244+09:00  INFO 4334 --- [           main] c.e.d.DiscoveryServiceApplication        : Started DiscoveryServiceApplication in 4.949 seconds (process running for 6.551)

서버를 띄웠으니 웹 브라우저에서 http://localhost:8761을 입력해 진입해 보면 다음과 같이 Eureka server가 띄워진다.

 

728x90
반응형
LIST

'MSA' 카테고리의 다른 글

[MSA] Part 6. Gateway Filter  (2) 2023.10.10
[MSA] Part 5. API Gateway  (0) 2023.10.06
[MSA] Part 4. Service 등록 (User)  (0) 2023.10.06
[MSA] Part 2. Spring Cloud란?  (0) 2023.10.06
[MSA] Part 1. Spring Microservices Architecture  (0) 2023.10.05
728x90
반응형
SMALL
반응형
SMALL

Spring Cloud는 분산 시스템 (MSA 역시 포함)에서 흔하게 사용되는 구조 및 패턴을 쉽게 빌드 및 배포할 수 있도록 도와주는 툴을 제공해 준다. 그런 분산 시스템에서 흔하게 사용되는 기능(툴)이란 건 configuration management, service discovery, circuit breakers, routing, proxy, control bus, authentication, cluster 등 여러 기술이 있다.

 

참고 문서 (Spring Cloud 공식 문서)

 

Spring Cloud

Spring Cloud provides tools for developers to quickly build some of the common patterns in distributed systems (e.g. configuration management, service discovery, circuit breakers, intelligent routing, micro-proxy, control bus, one-time tokens, global locks

spring.io

 

Features

  • Distributed/versioned configuration
  • Service registration and discovery
  • Routing
  • Service to service calls
  • Load balancing
  • Circuit Breakers
  • Global locks
  • Leadership election and cluster state
  • Distributed messaging

 

Distributed/versioned configuration은 각 서비스마다 필요한 세팅 및 환경변수가 존재하기 마련인데 이런 모든 필요한 세팅을 한 곳에서 관리하여 유지보수 및 환경 세팅의 변화가 생겼을 때 서비스의 재빌드 및 재배포를 거치지 않고 서비스를 다운타임 없이 지속 운영할 수 있는 방법을 말한다. 아래 그림을 보자.

위 그림처럼 각 서비스마다 필요한 환경과 세팅 내용을 한 곳에서 관리하고 그 관리하는 서버를 Git과 연동하여 Spring Cloud Config Server는 Git으로부터 저장되어 있는 환경 정보를 불러와 각 서비스들에게 제공한다.

 

Service discovery, registration은 Part 1에서 말한 Spring Eureka와 같은 도구를 말한다. 각 서비스들을 등록, 관리하여 이 분산 시스템에서 사용되는 서비스들은 어떤것들이 있고 각 서비스들을 상황에 맞게 관리해 주는 역할을 한다.

 

Routing, Load Balancing, Service to service callsSpring Cloud Gateway로 구현할 수 있는데 클라이언트로부터 요청을 받아 해당 요청을 처리할 수 있는 서비스에게 요청을 전달하며 전달할 때 부하를 분산해주며 각 서비스와 서비스 사이에서 요청을 전달하고 전달받을 수 있게 도와준다.

 

Circuit Breakers는 장애 처리를 도와주는 기술인데 위 그림이랑 똑같이 서비스가 총 3개가 있다고 가정했을 때, 유저가 유저 정보를 확인하기 위해서 유저 정보를 확인하는 요청을 했을 때 유저 정보에는 본인이 주문했던 또는 주문 중인 이력도 존재한다. 이때 주문 이력을 알아오기 위해 오더서비스를 호출하는데 오더서비스에 문제가 생겼을 때 오더서비스 하나 때문에 전체 유저 정보를 못 가져온다면 사용자 경험은 좋지 못할 것이다. 이때 서킷브레이커를 이용해서 오더서비스로부터 데이터를 받아오지 못하더라도 유저 정보만은 데이터를 가져올 수 있도록 처리해 줄 수 있다. 이 내용도 차차 구현해 보도록 하겠다.

728x90
반응형
LIST

'MSA' 카테고리의 다른 글

[MSA] Part 6. Gateway Filter  (2) 2023.10.10
[MSA] Part 5. API Gateway  (0) 2023.10.06
[MSA] Part 4. Service 등록 (User)  (0) 2023.10.06
[MSA] Part 3. Spring Cloud Netflix Eureka  (0) 2023.10.06
[MSA] Part 1. Spring Microservices Architecture  (0) 2023.10.05
728x90
반응형
SMALL
반응형
SMALL

요즘 한창 재미 들려 공부하는 MSA. 하나도 빠짐없이 배우고 공부한 내용을 기록해 보고자 한다.

 

MSA(MicroService Application)

우선 MSAMicro Service Application의 약자로, 어떤 서비스가 가진 기능을 제공할 때 하나의 큰 애플리케이션에서 모든 기능을 수행하는 것이 아니라 기능별, 특징별, 구성별로 서비스들을 작게 나누어 각 기능에 특화된 하나의 작은 서비스를 구축하고 그 구축한 여러 개의 서비스들이 모여 거대한 하나의 애플리케이션이 되는 형태를 말한다. 

위 사진에서 우측 SERVICE A, SERVICE B가 있다. 이 두 개의 서비스들은 각자 자신이 수행해야 할 최소 단위의 기능들만을 모아 구성된 하나의 작은 서비스다. 예를 들어, 쇼핑몰 기능을 제공하는 하나의 서비스에 결제 관련 기능을 담당하는 SERVICE A, 장바구니 관련 기능을 담당하는 SERVICE B로 나뉘어 그렇게 각 모든 작은 서비스들이 모여 하나의 큰 서비스를 이루는 것처럼 말이다.

 

사용자는 이 쇼핑몰이 어떤 구조로 서비스를 제공하는지 알 필요 없이 단순하게 쇼핑몰 사이트에 들어가 원하는 행위를 하면 그 각각의 행위가 필요한 기능, API를 서비스 내에서 알아서 관리하고 호출, 신청, 반환, 응답하면서 서비스가 수행된다.

 

위 사진에서는 클라이언트에서 요청을 하는 게 첫 번째 흐름인데 요청을 SERVICE A 또는 SERVICE B에 직접적으로 하는 게 아닌 중간에 있는 API Gateway에게 요청한다. API Gateway 역시 Microservice가 된다. API Gateway는 외부의 모든 요청을 이 녀석이 책임지고 받아 이 요청을 처리할 수 있는 서비스를 찾아서 그 서비스에게 돌려주는 Service Discovery Server(Netflix Eureka)에게 전달한다.

여기서 잠깐 Eureka에 대해 얘기하자면, Eureka는 Netflix에서 분산된 서비스들을 등록하고 관리하는 도구이다. 이 Eureka는 분산된 시스템 구조에서 각 서비스들을 등록관리소통하게 해준다. 이 Eureka를 Service discovery tool이라고 한다. 

Eureka 서버는 해당 요청을 처리할 수 있는 서비스를 찾아서 API Gateway에게 그 서비스를 알려주면 API Gateway는 해당 서비스에게 요청을 전달해 준다. 받은 요청을 처리할 수 있는 서비스는 해당 서비스를 처리 후 응답할 결과를 API Gateway에게 다시 돌려주고 최종적으로 클라이언트에게 돌아가게 된다.

 

전반적인 흐름을 봤을 때 이런 구조를 왜 굳이 가져야 하나 싶지만 이런 구조를 가지는 이유는 다음과 같다.

만약 각 마이크로서비스가 가져야 할 기능을 하나의 서버에서 전부 담당한다면 A가 처리할 기능 A, B가 처리할 기능 B를 모두 담당하고 있을 것이다. 이때 A가 처리할 기능 A를 수정 작업하는 상황이 생겼을 때 B는 어떠한 작업도 필요하지 않지만 서비스의 빌드와 배포가 다시 일어나야 하고 그렇기에 서버의 다운타임이 생긴다. 그 반대도 역시 마찬가지. 그 결과 사용자는 좋지 않은 사용자 경험을 할 수 있다. 그렇다면 이를 작은 단위로 서비스를 분리하여 특정 기능의 수정 및 추가가 일어날 때 해당 서비스만 변경 작업을 하고 해당 서비스가 제공하는 기능을 사용하는 서버 및 게이트웨이는 변경의 진행 여부조차 알 필요 없이 서비스는 계속해서 실행 상태를 유지할 수 있다.

API Gateway는 사용자의 요청을 받아 해당 요청에 대한 라우팅, 필터링, 요청에 대한 트래픽 관리를 해준다. 요청에 대한 트래픽 관리는 부하 분산과 관련이 있는데 이는 사용자에게 받은 요청에 대한 Load Balancing 처리를 해준다. 예를 들어, 쇼핑몰 서비스의 유저 관련 기능을 제공하는 하나의 마이크로서비스가 3개의 다른 포트로 실행되어 Eureka에 등록되면 사용자가 유저 조회와 같은 유저 관련 API를 호출할 때 현재 각 3개의 마이크로서비스 중 이 요청에 응답할 수 있는 서비스를 알아서 찾아 그 서비스로부터 데이터 요청 및 응답을 받는다.

 

이러한 MSA 구조를 Spring을 이용해서 구현해 보자! 

728x90
반응형
LIST

'MSA' 카테고리의 다른 글

[MSA] Part 6. Gateway Filter  (2) 2023.10.10
[MSA] Part 5. API Gateway  (0) 2023.10.06
[MSA] Part 4. Service 등록 (User)  (0) 2023.10.06
[MSA] Part 3. Spring Cloud Netflix Eureka  (0) 2023.10.06
[MSA] Part 2. Spring Cloud란?  (0) 2023.10.06

+ Recent posts