RATSENO

[Spring]Spring Cloud Netflix - Eureka + Zuul 본문

DEV/SPRING

[Spring]Spring Cloud Netflix - Eureka + Zuul

RATSENO 2020. 1. 30. 10:45

이전 포스팅
[Spring] Spring Cloud Netflix - Eureka[1]

[Spring]Spring Cloud Netflix - Eureka[2]

[Spring]Spring Cloud Netflix - Eureka[3]

를 토대로 Neflix API GATEWAY 인 Zuul에 대해서 알아보겠습니다.


API GATEWAY란?

Microservice Architecture(이하 MSA)에서 언급되는 컴포넌트 중 하나이며, 모든 클라이언트 요청에 대한 end point를 통합하는 서버입니다.

마치 프록시 서버처럼 동작합니다. 그리고, 인증 및 권한, 모니터링, logging 등 추가적인 기능이 있습니다.
모든 비즈니스 로직이 하나의 서버에 존재하는 Monolithic Architecture와 달리 MSA는 도메인 별 데이터를 저장하고 도메일 별로 하나 이상의 서버가 따로 존재합니다. 한 서비스에 한 개 이상의 서버가 존재하기 때문에 이 서비스를 사용하는 클라이언트 입장에서는 다수의 end-point가 생기게 되며,

end-point를 변경이 일어났을 때, 관리하기가 힘들어집니다. 그래서 MSA환경에서 서비스에 대한 도메인인 하나로 통합할 수 있는 API GATEWAY가 필요한 것입니다.

 

위에서 설명한 여러 분산 서비스 환경에서는 여러 가지 문제점이 발생할 수 있습니다.

 

  • UI에서 여러 서비스들을 직접 호출하게 됨

  • Auto scaling으로 인해 host, port 정보가 동적으로 변경됨

  • API 서비스들에 대해 각각 인증, CORS 처리 등을 해야 함

  • API 서비스들이 다양한 프로토콜을 갖게 될 수 있음

  • API 서비스들이 언제든 합쳐지거나 쪼개질 수 있음

이러한 문제점들을 해결하기 위해서 API GATEWAY가 필요하게 되었고, 우리가 익히 잘 알고 있는 플랫폼인 Neflix에서는 Zuul이라는

API GATEWAY를 사용하고 있습니다. 또한 배달의 민족으로 유명한 우아한 형제들에서도 위와 같은 사례를 토대로 Zuul을 적용하여

사용하고 있으며 이에 대한 자세한 과정과 설명을 기술 블로그에 게시하여 저 또한 Zuul에 대해서 어렴풋이 개념을 잡는데 도움이 되었습니다.

배민 API GATEWAY - spring cloud zuul 적용기
 

배민 API GATEWAY - spring cloud zuul 적용기 - 우아한형제들 기술 블로그

서비스를 운영하고 개발하는 팀이라면, LEGACY라는 거대한 괴물이 얼마나 다루기가 힘든 일인지 동감 할 것이다. 이 괴물이 오래되면 될수록, 크면 클수록… 제가 운영하고 개발하고 있는 팀에도 7년 묵은 괴물이 살고 있습니다.이 괴물을 한번에 팍~하고 변화시키기에는 너무나 많은...

woowabros.github.io

 


Spring Cloud Netflix

Spring Boot에 Netflix OSS를 통합적으로 제공하고 있습니다. 앞선 포스팅으로 설명드렸던 Eureka, Ribbon과 Circuit Breaker(?) 역할을 하하는 Hystrix 같은 OSS를 Spring Boot에서 간단한 어노테이션과 설정 파일들로 쉽게 적용할 수 있습니다.

간단한 예제를 통하여 Zuul을 적용해 보도록 하겠습니다. 예제 내용은 포스팅 상단의 Eureka 적용 이후부터 시작하겠습니다.

 

앞선 포스팅에서 Employee-Producer, Employee-consumer 두 개의 MicroService를 개발하였습니다.

MicroService의 연관관계는 아래의 그림과 같습니다.

현재의 모습은 Zuul이라는 API GATEWAY를 통하지 않고 employee-consumer에서 employee-producer로 직접 REST 통신을 하고 있는 모습입니다.

 

여기에 Zuul을 적용하게 되면 아래와 같은 모습으로 Zuul을 통하여 employee-producer에 통신을 하게 됩니다.

이전 포스팅까지 두 모듈은 Eureka까지 적용이 되어있기 때문에, Eureka + Zuul의 구조를 가지게 됩니다, 그림으로 표현하게 되면 아래와 같습니다.

 

현재 기준으로 3개의 MicroService가 있습니다.

  • employee-producer

  • employee-consumer

  • eureka-server

여기에 Zuul을 적용하기 위한 서비스가 하나 더 추가될 것이며, 기존의 employee-consumer 모듈을 Zuul 통해 통신하도록 수정하겠습니다.

먼저 Zuul을 적용하기 위한 모듈을 생성하겠습니다. 해당 모듈의 이름은 employee-zuul-service로 정하겠습니다.

 

Spring Boot프로젝트 또는 Maven 프로젝트로 생성하고, Zuul관련 dependency를 pom.xml에 추가하겠습니다.

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>employee-zuul-service</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>SpringBootHelloWorld</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.4.1.RELEASE</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zuul</artifactId>
        </dependency>

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

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Camden.SR6</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>

이어서 resources 폴더 아래 application.properties 파일에 설정 내용을 작성합니다.

zuul.routes.producer.url=http://localhost:8080
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka
server.port=8079

여기에서 zuul.routes.producer.url 속성은 "/producer" 포함한 요청 트래픽들을 localhost:8080 즉 employee-producer 모듈로 라우팅 겠다는 의미입니다. 

물론 다른 MicroService에도 유사한 방식으로 적용할 수 있습니다.

 

resources 폴더 아래 bootstrap.properties 파일에 애플리케이션의 이름을 설정합니다.

spring.application.name=employee-zuul-service

이어서 Zuul의 중요한 기능들을 담당하는 4가지 Filter에 대해서 정의할 것입니다.

  • pre - 라우팅 전에 실행되는 필터이다. 주로 logging, 인증 등이 pre filter에서 이루어진다.

  • route - 요청에 대한 라우팅을 다루는 필터이다. Apache HttpClient를 사용하여 정해징 URL로 보낼 수 있고, Neflix Ribbon을              사용하여 동적으로 라우팅 할 수도 있다.

  • post -  라우팅 후에 실행되는 필터이다. Response에 HTTP Header를 추가하거나, Response에 대한 응답속도, Status  Code 등 응답에 대한                    Statistics and Metrics를 수집한다.

  • error - 에러 발생 시 실행되는 필터이다.

순서대로 filter에 대한 파일을 작성하겠습니다. ZuulFilter를 상속받게 되면 각각의 필터들에 대하여 정의할 수 있습니다.

 

PreFilter

package com.example.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;

import javax.servlet.http.HttpServletRequest;

public class PreFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return "pre";
    }

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

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

    @Override
    public Object run() {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest httpServletRequest = requestContext.getRequest();

        System.out.println("Request Method : " + httpServletRequest.getMethod() + " Request URL : " + httpServletRequest.getRequestURI().toString());

        return null;
    }
}

 RouteFilter

package com.example.filter;

import com.netflix.zuul.ZuulFilter;

public class RouteFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return "route";
    }

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

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

    @Override
    public Object run() {
        System.out.println("Using Route Filter");
        return null;
    }
}

PostFilter

package com.example.filter;

import com.netflix.zuul.ZuulFilter;

public class PostFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return "post";
    }

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

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

    @Override
    public Object run() {
        System.out.println("Using Post Filter");
        return null;
    }
}

ErrorFilter

package com.example.filter;

import com.netflix.zuul.ZuulFilter;

public class ErrorFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return "error";
    }

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

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

    @Override
    public Object run() {
        System.out.println("Using Route Filter");
        return null;
    }
}

 

 

마지막으로 Spring boot main class에 @EnableZuulProxy 어노테이션을 적용합니다. @EnableZuulProxy을 적용함으로써 해당 애플리케이션은 zuul 서버(Proxy Server, API GATEWAY)로서 구축됩니다. 

그리고 앞서 작성한 4개의 필터들을 @Bean 어노테이션을 이용하여 Bean으로서 등록, 관리되게 합니다.

package com.example;

import com.example.filter.ErrorFilter;
import com.example.filter.PostFilter;
import com.example.filter.PreFilter;
import com.example.filter.RouteFilter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;

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

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

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

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

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

 

employee-zuul-service 모듈은 여기서 마무리 짓고, employee-consumer를 수정하겠습니다.

employee-consumer 모듈의 수정사항은 아래와 같습니다.

  1. 기존의 employee-producer 인스턴스를 찾는 부분을 employee-zuul-service 인스턴스로 변경

  2. employee-zuul-service 모듈의 appication.properties 파일에서 "/producer"를 가진 요청 트래픽에 대하여 "localhost:8080" 즉 employee-producer 모듈로 라우팅 하는 부분에 맞게 요청 URL부분 변경

package com.example.controller;

import java.io.IOException;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
@Controller
public class ConsumerControllerClient {

    @Autowired
    private DiscoveryClient discoveryClient;

    public void getEmployee() throws RestClientException, IOException {
	//수정사항 1번
        List<ServiceInstance> instances=discoveryClient.getInstances("employee-zuul-service");
        ServiceInstance serviceInstance=instances.get(0);
	//수정사항 2번
        String baseUrl=serviceInstance.getUri().toString();
        baseUrl=baseUrl+"/producer/employee";
        System.out.println("baseUrl:"+baseUrl);

        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> response=null;
        try{
            response=restTemplate.exchange(baseUrl,HttpMethod.GET, getHeaders(),String.class);
        }catch (Exception ex)
        {
            System.out.println(ex);
        }
        System.out.println(response.getBody());
    }

    private static HttpEntity<?> getHeaders() throws IOException {
        HttpHeaders headers = new HttpHeaders();
        headers.set("Accept", MediaType.APPLICATION_JSON_VALUE);
        return new HttpEntity<>(headers);
    }
}

employee-zuul-service 작성과 employee-consumer 수정이 모두 끝났습니다.

  • employee-producer

  • eureka-server

  • empolyee-zuul-service

  • employee-consumer

4개의 모듈은 Run 한 후 결과를 확인해 보겠습니다. (위의 순서대로 Run 권유)

원하는 기대 결과는

employee-consumer =====> employee-producer에서

employee-consumer =====> employee-zuul-service =====> employee-producer

로의 라우팅 된 결과가 나와야 합니다.

 

먼저 employee-consumer 모듈의 log를 확인하여 정상적으로 employee-producer의 응답 값이 출력되는지 확인합니다.

위의 사진에서 알 수 있듯이, 요청 URL은 "/producer"가 붙은 URL이며

정상적으로 employee-producer의 응답 값이 출력되었습니다.

 

다음으로 확인할 부분은 employee-zuul-service 즉 API GATEWAY으로 요청이 들어오고

employee-producer 인스턴스로 라우팅 된 log를 확인해야 합니다.

 

employee-zuul-service 모듈 log를 확인해 봅시다.

Request Method : GET Request URl : /producer/employeee

>해당 로그는 PreFilter에 작성해 둔 내용입니다.

Using Route Filter

>해당 로그는 RouterFilter 동작시 출력되는 로그입니다.

Using Post Filter

>해당 로그는 PostFilter 동작시 출력되는 로그입니다.

위 로그들을 통해 정상적으로 Zuul(API GATEWAY)가 동작하는 것을 확일할 수 있습니다.

 

지금까지 아주 간단한 예제로 Eureka + Zuul 이용해 API GATEWAY를 구성해 보았습니다.

물론  Zuul에는 더 많은 유용한 기능들이 존재합니다. 

앞으로 추가적으로 조사 후 알게 된 내용들을 보강하도록 하겠습니다 ㅠㅠ

 

부족한 포스팅 봐주셔서 감사합니다.

 

Github : https://github.com/RATSENO/spring-cloud-example/blob/master/README.md

Comments