이 연재글은 Spring Cloud 알아보기의 2번째 글입니다.

이번장에서는 Spring Cloud를 이용하여 Gateway(Routing & Filter)서버를 구축해 보도록 하겠습니다. SpringCloud에서 Gateway는 서로 분산되어있는 서비스들을 하나로 모아주는 관문같은 역할입니다.

기존의 로드밸런서, 리버스 프락시

기존의 리버스 프락시나 로드밸런서는 특정 요청 또는 URI에 대해 매핑될 서버의 ip나 도메인을 사전에 등록해놓아야 합니다. 등록이나 수정하는 과정에서 프로세스의 재시작이 필요할수도 있고 그에 따른 순단이 발생할수도 있습니다. 이러한 점은 시스템 운영을 위한 추가적인 인력이나 리소스가 필요하다는 것으로 해석할 수 있습니다.

Spring Cloud Gateway의 차별점

Spring Cloud Gateway는 다수의 서비스 엔드포인트를 하나로 통일하고 트래픽의 특성에 따라 알맞는 서비스로 라우팅 할 수 있는 기능을 제공합니다. 라우팅 설정은 무중단으로 적용할 수 있는 방법이 제공됩니다. 클라이언트 입장에서는 엔드포인트가 한군데로 통일되므로 연동시 고려해야될 관리 포인트가 줄어들게 됩니다.

서비스 제공자 입장에서는 트래픽을 한군데로 모을때 다음과 같은 장점이 있습니다.

  1. 유입되는 모든 요청/응답을 관리 할 수 있게 되므로 앞 단에 인증 및 보안을 적용하기가 좋습니다.
  2. URI에 따라 서비스 엔드포인트를 다르게 가져가는 동적 라우팅이 가능해집니다. 한가지 예를 들면 도메인 변경없이 레거시 시스템을 신규 시스템으로 점진적으로 교체해나가야 하는 작업에 유연하게 대처할 수 있습니다.
  3. 모든 트래픽을 감시할수 있으므로 모니터링 시스템 구성이 단순해집니다.
  4. 동적 라우팅이 가능하므로 신규 스펙을 서비스 일부에만 적용하거나, 트래픽을 점진적으로 늘려가는 테스트를 수행할 수 있습니다.

Configuration 서버에 Gateway Router 설정 추가

Gateway 서버에서 사용할 환경설정 정보를 Configuration 서버에서 조회할 수 있도록 설정을 추가합니다.

local 환경 설정

local 디렉터리 ${user.home}/server-configs 하위에 zuul gateway 설정 파일을 추가합니다.

$ pwd
/Users/happydaddy/server-configs
$ ls
member-service-local.yml        contents-service-local.yml        zuul-gateway-local.yml

접근 path에 따라 다른 서비스(8080 or 8081)로 라우팅 되도록 설정합니다.

# zuul-gateway-local.yml
spring:
  profiles: local
zuul:
  routes:
    member:
      stripPrefix: false
      path: /v1/member/**
      url: http://localhost:8080
    pay:
      stripPrefix: false
      path: /v1/pay/**
      url: http://localhost:8081
    else:
      stripPrefix: false
      path: /v1/**
      url: http://localhost:8081

alpha 환경 설정 추가

GitHub의 server-configs 하위에 zuul-gateway-alpha.yml을 추가하고 내용작성후 커밋합니다.

# zuul-gateway-alpha.yml
spring:
  profiles: alpha
zuul:
  routes:
    member:
      stripPrefix: false
      path: /v1/member/**
      url: http://localhost:8080
    pay:
      stripPrefix: false
      path: /v1/pay/**
      url: http://localhost:8081
    else:
      stripPrefix: false
      path: /v1/**
      url: http://localhost:8081

Gateway Config 확인

profile 설정을 native, local로 설정하고( -Dspring.profiles.active=native,local ) Configuration서버를 실행합니다. 아래와 같이 Gateway설정 결과를 확인합니다.

$ curl http://localhost:9000/zuul-gateway/local
{
  "name": "zuul-gateway",
  "profiles": [
    "local"
  ],
  "label": null,
  "version": null,
  "state": null,
  "propertySources": [
    {
      "name": "classpath:/server-configs/zuul-gateway-local.yml",
      "source": {
        "zuul.routes.member.stripPrefix": false,
        "zuul.routes.member.path": "/v1/member/**",
        "zuul.routes.member.url": "http://localhost:8080",
        "zuul.routes.pay.stripPrefix": false,
        "zuul.routes.pay.path": "/v1/pay/**",
        "zuul.routes.pay.url": "http://localhost:8080",
        "zuul.routes.else.stripPrefix": false,
        "zuul.routes.else.path": "/v1/**",
        "zuul.routes.else.url": "http://localhost:8081"
      }
    }
  ]
}

Netflix Zuul Gateway 프로젝트 생성

SpringCloud는 Netflix에서 Opensource로 공개한 Zuul Gateway를 Spring 환경에 통합하여 기능을 제공하고 있습니다. Gateway의 경우 아무래도 클라이언트 접점의 최전선이므로 단일 서비스로 운영하기 보다는 여러대로 구성하고 LoadBalancer로 묶어 HA(High Availability)를 확보하는 것이 좋습니다.

신규 Boot 프로젝트를 하나 생성합니다. 이전장에서 구축했던 member, contents서비스와 Configuration서버의 연동과 설정방법은 거의 비슷합니다. 예제에서는 따로 프로젝트를 생성하지 않고 기존 프로젝트에 multi module로 생성하였습니다.

신규 프로젝트를 초기 구성하는데 어려움이 있다면 Spring initializr를 통해 간편하게 프로젝트를 생성할 수 있습니다. 다음 포스팅을 참고해 주세요.
>> Spring initializr로 Spring 프로젝트 생성하기

build.gradle

spring-cloud-starter-netflix-zuul과 spring-cloud-starter-config를 사용합니다. Configuration서버를 통해 환경설정을 연동하지 않을것이면 netflix-zuul만 사용하면 됩니다.

plugins {
    id 'org.springframework.boot' version '2.1.4.RELEASE'
    id 'java'
}

apply plugin: 'io.spring.dependency-management'

group = 'com.spring'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
    mavenCentral()
}

ext {
    set('springCloudVersion', 'Greenwich.SR1')
}

dependencies {
    implementation 'org.springframework.cloud:spring-cloud-starter-netflix-zuul'
    implementation 'org.springframework.cloud:spring-cloud-starter-config'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
}

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

application.yml

config server를 사용하지 않고 아래처럼 application.yml에 직접 라우팅 할 서버 정보를 나열하여 운영이 가능합니다. 하지만 이렇게 할경우 라우팅 정보가 변경될때마다 Gateway서버를 수정하고 배포해야 하므로 좋은 방법이 아닙니다. 참고만 하고 application파일 대신 bootstrap 파일을 생성하여 configuration server와의 연동을 진행 합니다.

# application.yml - 이 방법을 사용하지 않을것이므로 생성하지 않습니다.
server:
  port: 9100
zuul:
  routes:
    member-service.url: http://localhost:8080
    contents-service.url: http://localhost:8081

bootstrap 설정

application.properties를 삭제하고 bootstrap.yml을 생성합니다.

공통 환경 세팅
application_name과 디폴트 profile, 그리고 환경설정 변경을 위한 endpoint( /actuator/refresh)를 활성화합니다.

# bootstrap.yml
server:
  port: 9100
spring:
  application:
    name: zuul-gateway
  profiles:
    active: local
management:
  endpoints:
    web:
      exposure:
        include: refresh

local 환경 세팅
configuration 서버 접속 정보를 명시합니다.

# bootstrap-local.yml
spring:
  profiles: local
  cloud:
    config:
      uri: http://localhost:9000
      fail-fast: true

alpha 환경 세팅
configuration 서버 접속 정보를 명시합니다.

# bootstrap-alpha.yml
spring:
  profiles: alpha
  cloud:
    config:
      uri: http://localhost:9000
      fail-fast: true

Application 설정

일반적인 Boot설정과 동일합니다. @EnableZuulProxy를 선언하여 Gateway 서버를 활성화 합니다.

@EnableZuulProxy
@SpringBootApplication
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}

member-service, contents-service Controller 내용 추가

이전장에서 생성한 서비스의 Controller에 내용을 좀더 추가합니다.

// MemberController
@RestController
@RequestMapping("/v1")
@RefreshScope
public class MemberController {

    @Value("${server.port}")
    private int port;

    @Value("${spring.message}")
    private String message;

    @GetMapping("/member/detail")
    public String member() {
        return "Member Detail - Port " + port + " - " + message;
    }

    @GetMapping("/pay/detail")
    public String pay() {
        return "Pay Detail - Port " + port + " - " + message;
    }
}

// ContentsController
@RestController
@RequestMapping("/v1")
@RefreshScope
public class ContentsController {

    @Value("${server.port}")
    private int port;

    @Value("${spring.message}")
    private String message;

    @GetMapping("/member/detail")
    public String member() {
        return "Member Detail - Port " + port + " - " + message;
    }

    @GetMapping("/pay/detail")
    public String pay() {
        return "Pay Detail - Port " + port + " - " + message;
    }

    @GetMapping("/comment")
    public String comment() {
        return "Comment - Port " + port + " - " + message;
    }

    @GetMapping("/board")
    public String userDetail() {
        return "Board - Port " + port + " - " + message;
    }
}

Gateway 테스트

Configuration server(9000), Gateway server(9100), member-service(8080), contents-service(8081)를 차례로 실행합니다. profile은 로컬환경을 기준으로 하고 서버를 실행합니다.

서비스 endpoint인 Gateway를 통해 서비스 주소를 요청 해봅니다. 요청을 Gateway로 하지만 라우팅 정보에 따라 서로 다른 서비스가 호출되어 결과가 출력되는것을 확인할 수 있습니다.

# 8080,8081 양쪽에 존재하나 라우팅된 8080 서버에서 처리합니다.
$ curl "http://localhost:9100/v1/member/detail"
Member Detail - Port 8080 - Hello Spring MemberService Local Env!!!!!!
# 8080,8081 양쪽에 존재하나 라우팅된 8081 서버에서 처리합니다.
$ curl "http://localhost:9100/v1/pay/detail"
Pay Detail - Port 8081 - Hello Spring ContentsService Local Env
# 매칭안되는 path는 모두 8081 서버에서 처리됩니다. 
$ curl "http://localhost:9100/v1/comment"
Comment - Port 8081 - Hello Spring ContentsService Local Env
$ curl "http://localhost:9100/v1/board"
Board - Port 8081 - Hello Spring ContentsService Local Env

Gateway 라우팅 실시간 변경 테스트

server-configs 아래의 zuul-gateway-local.yml 파일을 수정합니다. 8081으로 라우팅되던 /v1/pay/** 의 라우팅을 8080서버로 가도록 변경합니다. 그리고 Gateway서버로 /actuator/refresh 를 요청하여 설정 내용을 Gateway서버에 반영 합니다. 다시 url을 요청하면 변경된 8080 서버에서 요청이 처리되는 것을 볼 수 있습니다.

# zuul-gateway-local.yml
zuul:
  routes:
    member:
      stripPrefix: false
      path: /v1/member/**
      url: http://localhost:8080
    pay:
      stripPrefix: false
      path: /v1/pay/**
      url: http://localhost:8080
    else:
      stripPrefix: false
      path: /v1/**
      url: http://localhost:8081
$ curl -XPOST http://localhost:9100/actuator/refresh
["zuul.routes.pay.url"]
$ curl localhost:9100/v1/pay/detail
Pay Detail - Port 8080 - Hello Spring ContentsService Local Env

필터 적용

Gateway에는 필터를 적용할 수 있습니다. 필터는 클라이언트의 HTTP 요청을 받고 응답하는 과정에서 리퀘스트를 라우팅하는 동안 수행할 액션을 지정할 수 있습니다. 종류는 다음과 같이 4가지가 있습니다

PRE
백엔드 서버로 라우팅 되기 전에 수행되는 필터로 요청에 대한 인증, 로깅, 디버깅 등을 처리할 수 있습니다.
ROUTING
요청에 대한 라우팅을 제어할 때 사용되는 필터로 Apache HttpClient 또는 Ribbon을 사용하여 백엔드 서버로 요청을 동적으로 라우팅 할 수 있습니다.
POST
백엔드 서버로 요청이 라우팅되고 난 후에 수행되는 필터로서 응답에 HTTP 헤더를 추가하거나, API 응답속도, 각종 지표나 메트릭을 수집할때 사용할 수 있습니다.
ERROR
위의 단계 중 에러가 발생하면 실행되는 필터입니다.

다음과 같이 테스트를 위한 필터를 작성합니다.

pre filter

@Slf4j
public class GatewayPreFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

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

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

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        log.info("Using Pre Filter : "+request.getMethod() + " request to " + request.getRequestURL().toString());
        return null;
    }
}

route filter

@Slf4j
public class GatewayRouteFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return FilterConstants.ROUTE_TYPE;
    }

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

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

    @Override
    public Object run() {
        log.info("Using Route Filter");
        return null;
    }
}

post filter

@Slf4j
public class GatewayPostFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return FilterConstants.POST_TYPE;
    }

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

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

    @Override
    public Object run() {
        log.info("Using Post Filter");
        return null;
    }
}

error filter

@Slf4j
public class GatewayErrorFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return FilterConstants.ERROR_TYPE;
    }

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

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

    @Override
    public Object run() {
        log.info("Using Error Filter");
        return null;
    }
}

Application에 filter 등록

@EnableZuulProxy
@SpringBootApplication
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }

    @Bean
    public GatewayPreFilter preFilter() {
        return new GatewayPreFilter();
    }

    @Bean
    public GatewayPostFilter postFilter() {
        return new GatewayPostFilter();
    }

    @Bean
    public GatewayRouteFilter routeFilter() {
        return new GatewayRouteFilter();
    }

    @Bean
    public GatewayErrorFilter errorFilter() {
        return new GatewayErrorFilter();
    }
}

필터 테스트

Gateway서버를 재실행하고 다음과 같이 실행하면 Gateway Log에서 필터를 거치는것을 확인할 수 있습니다.

$ curl "http://localhost:9100/v1/member/detail
2019-05-17 15:37:21.734  INFO 92558 --- [nio-9100-exec-5] com.spring.msa.filter.GatewayPreFilter   : 
Using Pre Filter : GET request to http://localhost:9100/member/detail
2019-05-17 15:37:21.734  INFO 92558 --- [nio-9100-exec-5] c.spring.msa.filter.GatewayRouteFilter   : 
Using Route Filter
2019-05-17 15:37:21.773  INFO 92558 --- [nio-9100-exec-5] com.spring.msa.filter.GatewayPostFilter  : 
Using Post Filter

필터 예제는 아래 Github에서 더 다양한 내용을 살펴볼 수 있습니다.

https://github.com/spring-cloud/spring-cloud-netflix/tree/master/spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre

실습에서 사용한 소스는 다음 Github에서 확인하실수 있습니다.

https://github.com/codej99/SpringCloudMsa/tree/feature/zuul-gateway

연재글 이동[이전글] Spring Cloud MSA(1) – Configuration server 구성
[다음글] Spring Cloud MSA(3) – Service Discovery by Eureka