기본 콘텐츠로 건너뛰기

Edge server와 Ribbon, 그리고 API 구성

서론


여기서 말하는 Edge Server는 다양한 API 서버들을 연결하는 Gatekeeper와 비슷한 역할을 그 주요 기능으로 한다. 이 외에 구성에 따라서는 외부 클라이언트와의 연결에서 Security의 진입점 같은 역할도 하기는 하지만, 그 부분은 다른 포스트에서 정리해볼까 한다.

이번 포스트를 정리할 때 사용한 예제는 대부분을 Calista 블로그의 Building microservices with Spring Cloud and Netflix OSS, part 1 에서 참고하였다. 사실 위 블로그의 내용을 거의 똑같이 따라 하면서 정리했다고 보는 것이 맞을 것 같다. 이 포스트보다 정리가 잘 되어있는 만큼 영어에 어려움이 없는 개발자라면 Calista 블로그의 내용을 보는 것이 더 나을지도 모르겠다.

한가지 참고할 수 있는 자료가 더 있는데 다른 포스트에서도 소개했던 적이 있는 Devoxx 2015 의 동영상 자료가 있다. 제목은 Getting started with Spring Cloud by Josh Long 으로 1시간 정도 라이브 코딩으로 Josh Long이 열정적으로 설명해준다. 영어를 못하는 나도 어느 정도 따라갈 수 있었던 동영상으로 강추하고 싶은 동영상이다.

시작하기에 앞서서 좋은 블로그 자료를 공개해줘서 샘플을 만들고 정리하는 데 도움을 준 Calista 블로그에 감사의 마음을 전하며, 자료를 함부로 가져다 쓰게 된 점에 대해 매우 미안한 마음을 전한다.

이번 포스트를 정리하기 위해 사용한 샘플은 아래와 같은 조건으로 작업이 되었다.
  • Configuration Server 와 Discover Server : 이전 포스트 참고
  • JDK 8 : 1.8.0_65
  • Intellij 15.0.3
  • Gradle build : 2.10
  • Spring IO : 2.0.1 
  • Spring Boot : 1.3.2 (이전 포스트 작성후에 버전이 1.3.1에서 1.3.2로 업그레이드됐다)
  • Spring Cloud :  Angel.SR4
  • Lombok : 1.12.6
  • IDEA Lombok plugin : 0.9.7
  • Configuration server의 git 저장소 : https://github.com/roadkh/blog-cloud-sample-config.git
  • 설정파일은 모두 yml 을 사용했다. (모두 properties 파일로 변경한다고 해도 크게 달라지지는 않는다)
샘플 소스는 https://github.com/roadkh/blog-cloud-sample.git 에서 clone 가능하며, branch는 blog_02 에서 확인할 수 있다. 소스 관련 자세한 정보는 https://github.com/roadkh/blog-cloud-sample/tree/blog_02 에서도 확인할 수 있다.

샘플에서는 lombok을 사용해서 반복작업을 최소화했다.
lombok을 사용하기 위해서는 eclipse와 Intellij에 따라 별도의 작업이 좀 필요하다. eclipse의 경우는 https://projectlombok.org/  또는 CoolioSo! 블로그의 Lombok 소개 및 설치방법을 참고하기 바란다.
IntellJ(IDEA 15를 기준)의 경우는 아래의 과정을 거쳐서 설치할 수 있다.

  • Settings > Plugins > Install JetBrains plugin... 을 클릭한다.
  • lombok을 검색한다.
  • Lombok Plugin을 선택하고 설치를 한다.
  • Settings > Build, Execution, Deployment > Compiler > Annotation Processors 를 선택한다.
  • Enable annotation processing 에 체크한다. (매번 프로젝트를 새로 진행할 때 이 부분을 잊어버려서 Lombok 이 정상 동작하지 않는 경우가 많았다. 나처럼 멍청한 실수를 하시는 분들이 없기를 바란다)



기본 구성


샘플의 시스템 구성은 앞서 서론에서 이야기한 Calista 블로그의 글에 정리된 것과 같다.

[Calista 블로그에서 소개한 구성 이미지]

위의 그림에 이번 샘플에서 적용된 포트 정보나 기타 정보를 조금 추가하면 아래와 같은 그림이 되겠다.



Entity 는 Product, Review, Recommendation 이렇게 구성이 되며, 그 관계의 그림과 같다.


보면 각 Entity가 관계를 맺도록 구성되어있지만, 위 엔티티 하나씩에 대해 처리하는 API를 구성할 것이다. 실제 서비스에서는 저렇게 구성하는 경우는 없겠지만, Microservice에서의 API 예제를 위한 것이므로 다소 무리가 있더라도 Calista의 구성을 그대로 정리해 볼까 한다.

API 서비스와 Configuration Server


API 서비스 서버들이 시작될 때 Configuration server에서 설정을 조회하도록 하기 위해서는 아래와 같은 두 가지의 작업이 필요하다.

  • spring-cloud-starter-config 에 대한 dependency 를 추가한다. 샘플에서는 build.gradle에 compile('org.springframework.cloud:spring-cloud-starter-config') 을 추가했다.
  • classpath에 bootstrap.yml(application.yml 이 아니다) 을 추가한다. 샘플에서는 src/main/resources 폴더에 추가했다.

Production/Recommendation/Review 프로젝트의 형태가 거의 비슷하므로, 여기서는 Production만 가지고 정리를 하고자 한다. 자세한 내용은 github의 소스를 확인하면 되리라 본다.

Discovery Server(여기서는 Eureka Server) 에 Configuration Server를 등록해서 사용하는 경우에는 이 정보를 등록하려는 서비스의 bootstrap 파일에 아래와 같이 등록해서 사용할 수 있다.

spring:
  cloud:
    config:
      discovery:
        enabled: true
        service-id: config-server
      failFast: true
  application:
    name: production

즉, Configuration server의 service id 를 이용한 접근 방법이다.
이 방법이 참 좋아 보이지만, 현재 버전에서 한가지 문제점이 있다.
Configuration을 받아가는 서비스의 server.port 를 랜덤으로 처리할 수가 없는 문제가 있다.
예를 들어서 위의 구성도를 보면 이번 샘플은 Edge server를 제외하고는 거의 모두 랜덤 포트를 이용하도록 설정이 되어있는데, 이 경우 configuration을 discovery 방식으로 사용하면 정상적으로 동작하지 않는다. 이 문제는 Discovery server에 등록될 때 port가 0으로 등록되는 문제가 있어서 그런 것으로 보인다.

따라서 위 구성과 같이 random port를 사용하고자 한다면, 위 방식은 쓸 수가 없다. 혹시 모든 서비스의 포트가 정해져 있다면, discovery 방식을 이용해서 configuration server 운영에서 유연성이 확보될 수 있다는 장점이 있지 않을까 싶다. 이것은 각자의 상황에 맞춰서 결정하면 되지 않을까 생각한다.

이번 샘플에서는 위 방식이 아닌 Configuration server의 아이피와 포트를 지정해서 사용했다.

spring:
  cloud:
    config:
      uri: http://localhost:8888
      failFast: true
  application:
    name: production

두 예에서 옵션을 보면 모두 failFast를 true로 지정했다. 해당 값은 기본값이 false로 만약 Configuration server에서 정상적으로 설정을 조회하지 못했다면, 기본 application.yml 등의 설정을 읽어서 시작할 수 있도록 처리할 수 있다. 다만, 이번 샘플에서는 기본 application.yml 을 생성하지 않고 모두 Configuration server 에서 조회하도록 했기 때문에 fail-fast의 값을 true로 했다. 이렇게 하면 설정을 받아오지 못하면 서버 기동에 실패한다.

같은 방식으로 모든 API 프로젝트에 bootstrap.yml을 만들고 application.name 만 git에 등록된 파일명과 동일한 이름으로 만들어주면 Configuration server에 대한 서비스의 설정은 완료된다.
(https://github.com/roadkh/blog-cloud-sample-config 에서 확인할 수 있다.)

API 서비스와 Discovery Server


Discovery server 를 이용하도록 하려면 세 가지 작업이 필요하다.

  • spring-cloud-starter-eureka 에 대한 dependency를 추가한다. 샘플에서는 각 서비스의 build.gradle 에 compile('org.springframework.cloud:spring-cloud-starter-eureka') 을 추가했다.
  • Spring boot 의 메인 클래스에 @EnableDiscoveryClient 를 추가한다.
  • application.yml 에 discovery server 설정을 추가한다. 샘플에서는 Configuration server 의 application.yml 을 이용하도록 하여 모든 서비스가 공통으로 사용하도록 했다.
예전 포스트에서 정리한 적이 있지만, Configuration 에 조회를 요청하면 아래와 같은 파일에서 정보를 찾게 된다.

  • 기본 application.properties 또는 application.yml
  • spring.appliation.name 에 설정된 이름의 설정파일.
  • spring.profiles.active 에 해당되는 설정파일
예를 들어서 production 이라는 application에서 test라는 profile로 요청한 경우 아래와 같은 순서로 조회하며, 후순위 조회의 정보가 가장 큰 우선순위를 가진다. 즉, 같은 설정이 있을 경우 후순위 조회된 정보로 업데이트된다고 보면 된다.

application.yml => production.yml => production-test.yml

샘플에서는 profile을 이용한 부분은 없다.
앞에서 설명했듯이 모든 서비스가 application.yml 의 정보는 이용하게 되므로 Discover server 관련 정보는 application.yml 에 아래와 같이 설정했다.

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
  instance:
    preferIpAddress: true
    leaseRenewalIntervalInSeconds: 30
    leaseExpirationDurationInSeconds: 90
    metadataMap:
      instanceId: ${spring.application.name}:${spring.application.instance_id:${random.value}}

위 설정의 주요내용을 간단히 정리해보면 다음과 같다.

  • eureka.client.service-url : Discovery server에 대한 접속 정보이다. 콤마로 분리해서 여러 개 넣을 수 있는 것으로 보인다. 샘플에서는 peer1과 peer2가 서로 리플리케이션 관계이므로 사실 하나만 넣어도 상관없다.
  • eureka.instance.preferIpAddress : false가 기본값이다. 이 경우 Discovery server의 dashboard에 접속해보면, hostname을 이용해서 등록된 것을 볼 수 있다. 샘플에서는 아이피로 등록되도록 했다. 
  • eureka.instance.leaseRenewalIntervalInSeconds : HeartBeat과 비슷한 의미로 보인다. 30초가 기본값으로 이 설정값 간격으로 Discovery에 registration 정보를 교환한다. 
  • eureka.instance.leaseExpirationDurationInSeconds : 기본값은 90초다. 이 설정값 동안 서비스가 접속하지 않으면 해당 서비스는 정상이 아니라고 판단하게 된다. (이 부분은 정확히 리스트에서 삭제되는지 아니면 상태가 Up이 아니게 되는지까지 확인을 못해봤다)
  • eureka.metadataMap.instanceId : Discovery server에 등록되는 이름이다. Dashboard에서 확인하는 이름과 같다.
위 설정 중에서 두 가지만 좀 더 정리해볼까 한다.

우선 leaseRenewalIntervalInSeconds 에 대해서 정리해보자.
Discovery server에 registration 정보를 다시 등록하는 간격으로 보면 되는데, 실제로는 heartbeat과 같은 의미로 사용이 되는 것으로 보인다. 
샘플의 모든 서비스를 시작하는데, Edge server를 다른 API들보다 먼저 시작하고 API를 시작한 후에 모든 서비스가 Discovery dashboard에 올라왔는데도 forward error가 발생하는 것을 확인할 수 있었다. Loadbalancer 역할을 하는 Ribbon  모듈에 정상적으로 모든 서비스가 등록되지 않아 발생하는 문제라고 하는데, 이 부분에 대해서 Spring Cloud Documentation의 이야기를 그대로 인용해서 정리할까 한다.
Why is it so Slow to Register a Service?

Being an instance also involves a periodic heartbeat to the registry (via the client’s serviceUrl) with default duration 30 seconds. A service is not available for discovery by clients until the instance, the server and the client all have the same metadata in their local cache (so it could take 3 hearbeats). You can change the period using eureka.instance.leaseRenewalIntervalInSeconds and this will speed up the process of getting clients connected to other services. In production it’s probably better to stick with the default because there are some computations internally in the server that make assumptions about the lease renewal period.
실제 서비스에서는 크게 문제가 되지 않으리라고 보이는데, 테스트할 때에는 좀 답답할 수 있다. 그럴 때는 10초 정도로 줄이면 괜찮지 않을까 싶다.

다음 instanceId 이다.
기본 application.name + hostname 의 형태였던 것으로 기억한다(이 부분 테스트를 워낙 오래전에 했어서, 지금은 정확하게 기억이 나지 않는다). 즉, 하나의 호스트에서는 동일 서비스를 여러 개 실행할 수 없다. 두 개 이상을 실행할 경우 Discovery Server에는 뒤에 실행된 애플리케이션의 정보로 바뀌게 된다. 즉, 처음에 8080 포트로 실행하고 다음에 8081로 실행을 해서 동일 서비스를 두 개 실행하게 되면, Discovery server에는 8081로 실행된 정보만 남는다는 이야기이다.
그래서 instanceId를 유일하게 만들려는 방법으로 Spring cloud documentation에는 위와 같은 형태를 예제로 사용하고 있다. 이 샘플에서는 해당 정보를 그대로 이용했다.(Pivotal의 Cloudfoundry 를 사용한다면 이 부분이 해결되는 변수를 넣을 수 있다고 한다)

Boot Main Class를 보면 매우 간단하다. 별다른 설명이 필요 없을 것으로 보인다.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

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

모든 API 들에 공통으로 위와 같은 방법으로 적용하고 실행하면 Discovery server 에는 아래와 같은 모습으로 등록되고 dashboard에서 확인할 수 있다.(Dashboard URL 은 샘플의 경우 http://localhost:8761/ 이거나 http://localhost:8762/ 이다)

Ribbon Module의 역할


첫 부분의 그림을 보면 Edge server와 Composite API에 Ribbon Module이 포함된 것을 볼 수 있다.
나는 현재까지 구성한 모든 서버와 모듈들은 이 Ribbon의 로드밸런서를 사용하기 위해 만들어진 게 아닐까 할 정도로 가장 강력한 기능이라고 생각한다.
몇 가지 옵션이 존재하지만, 사실 아무런 설정을 하지 않고 사용해도 샘플을 구동하는 데는 부족하지 않을 정도다. 이번 포스트에서는 다른 설명은 하지 않고 구체적으로 어떤 식으로 되는지를 샘플을 통해 정리해보자.

Composite API 의 ProductCompositeServiceBean.java 의 내용을 먼저 보자.

@Service
public class ProductCompositieServiceBean implements ProductCompositeService {

    private static final String PRODUCT_API_URL = "http://product/";
    private static final String RECOMMENDATION_API_URL = "http://recommendation/";
    private static final String REVIEW_API_URL = "http://review/";

    @Autowired
    private RestTemplate restTemplate;

    @Override
    public Page getProducts(int page, int size, String sort) {
        String uri = new StringBuffer(PRODUCT_API_URL).append("/?size={size}&page={page}&sort={sort}").toString();
        Map pageMap = new HashMap<>();
        pageMap.put("page", page);
        pageMap.put("size", size);
        pageMap.put("sort", sort);


        ResponseEntity> responseEntity = restTemplate.exchange(uri, HttpMethod.GET, null, new ParameterizedTypeReference>() {
        }, pageMap);

        if (responseEntity == null || responseEntity.getStatusCode() != HttpStatus.OK) {
            return null;
        }

        return responseEntity.getBody();
    }

    @Override
    public Product getProductById(Long id) {

        String uri = new StringBuffer(PRODUCT_API_URL).append("/{productId}").toString();
        Map paramMap = new HashMap<>();
        paramMap.put("productId", id);
        ResponseEntity responseEntity = restTemplate.exchange(uri, HttpMethod.GET, null, new ParameterizedTypeReference() {}, paramMap);
        if (responseEntity == null || responseEntity.getStatusCode() != HttpStatus.OK) {
            return null;
        }

        Product product = responseEntity.getBody();

        Long productId = product.getId();

        List recommendations = getRecommendationsByProduct(productId);

        product.addRecommenations(recommendations);

        List reviews = getReviewsByProduct(productId);

        product.addReviews(reviews);

        return responseEntity.getBody();
    }

    private List getRecommendationsByProduct(Long productId) {
        String uri = new StringBuffer(RECOMMENDATION_API_URL).append("/byProduct/{productId}").toString();
        Map paramMap = new HashMap<>();
        paramMap.put("productId", productId);
        ResponseEntity> responseEntinty = restTemplate.exchange(uri, HttpMethod.GET, null, new ParameterizedTypeReference>(){}, paramMap);

        if(responseEntinty == null || responseEntinty.getStatusCode() != HttpStatus.OK) {
            return new ArrayList<>();
        }

        return responseEntinty.getBody();
    }

    private List getReviewsByProduct(Long productId) {
        String uri = new StringBuffer(REVIEW_API_URL).append("/byProduct/{productId}").toString();
        Map paramMap = new HashMap<>();
        paramMap.put("productId", productId);

        ResponseEntity> responseEntinty = restTemplate.exchange(uri, HttpMethod.GET, null, new ParameterizedTypeReference>(){}, paramMap);

        if(responseEntinty == null || responseEntinty.getStatusCode() != HttpStatus.OK) {
            return new ArrayList<>();
        }

        return responseEntinty.getBody();
    }
}

ProductCompositeServiceBean은 RestTemplate을 이용해서 각 API에 접속해서 데이터를 가져와서 필요한 데이터를 만들어내는 역할을 하는 서비스 클래스이다.

각 API에 접속하기 위한 URL을 보면 모두 http://{discover service id}/ 의 형태를 가지고 있다. 구체적인 호스트와 도메인 또는 아이피의 정보도 없고 포트에 대한 정보도 없다. 단지, Eureka server에 등록된 service id 만을 이용해서 접속할 수 있다. 앞에서 한번 이야기했지만 Ribbon은 Round robin 방식으로 로드밸런싱을 수행한다고 한다.  실제 요청을 통해 어떻게 동작하는지 보자.

샘플을 이용해서 모든 서비스를 실행하는데 Product API 만 2개를 실행해보자.
그럼 Discovery dashboard에는 아래와 같이 PRODUCT 라는 인스턴스가 두 개가 된다.


이 상태에서 Composite API 또는 Edge server 에 Request를 보내고 두 개의 product API 의 로그를 보면 번갈아가면서 요청이 들어오는 것을 확인 할 수 있다.

아래 로그는 Composite API 의 로그 일부로 처음 요청을 했을 때 한번 나오는 로그이다. neflix의 DynamicServerListLoadBalancer 에 의해서 생성된 로그 내용으로 Ribbon 관련 정보 외에 Circuit breaker 정보 등도 포함되어있다.

DynamicServerListLoadBalancer for client product initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=product,current list of Servers=[product:2ecbee36ab9119e9a2d2341d6b54569b, product:9b4c1ec0b4c53295dfdb6d5e21ff6237],Load balancer stats=Zone stats: {defaultzone=[Zone:defaultzone; Instance count:2; Active connections count: 0; Circuit breaker tripped count: 0; Active connections per server: 0.0;]
},Server stats: [[Server:product:2ecbee36ab9119e9a2d2341d6b54569b; Zone:defaultZone; Total Requests:0; Successive connection failure:0; Total blackout seconds:0; Last connection made:Thu Jan 01 09:00:00 KST 1970; First connection made: Thu Jan 01 09:00:00 KST 1970; Active Connections:0; total failure count in last (1000) msecs:0; average resp time:0.0; 90 percentile resp time:0.0; 95 percentile resp time:0.0; min resp time:0.0; max resp time:0.0; stddev resp time:0.0]
, [Server:product:9b4c1ec0b4c53295dfdb6d5e21ff6237; Zone:defaultZone; Total Requests:0; Successive connection failure:0; Total blackout seconds:0; Last connection made:Thu Jan 01 09:00:00 KST 1970; First connection made: Thu Jan 01 09:00:00 KST 1970; Active Connections:0; total failure count in last (1000) msecs:0; average resp time:0.0; 90 percentile resp time:0.0; 95 percentile resp time:0.0; min resp time:0.0; max resp time:0.0; stddev resp time:0.0]
]}ServerList:org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingServerList@4ac40560

Zuul proxy 를 이용한 Edge server 


Edge server는 외부 클라이언트와 서비스 간의 중간 Gatekeeper 역할을 하는 서버이다.
Zuul proxy 는 Netflix OSS 중에서 Proxy server 역할을 하는 모듈이다.
이번에는 별도의 Edge server로 구성했지만, 예전에 다른 작업을 하면서 사용자용 웹 서버에 Zuul proxy를 설정해서 API 서버와 연결을 한 적이 있는데 그런 역할을 하는 모듈이다.
Zuul proxy 는 개별 기능으로도 효용성이 높은 모듈로 Zuul 만을 이용한 예제를 보고 싶다면, Spring Guide의 Spring Security and Angular JS 를 보면 Zuul proxy 만을 사용하는 예제가 있으니 참고가 되지 않을까 싶다.

그럼 Edge server 를 샘플을 통해서 정리해보자.

우선 EdgeServerApplication.java 를 보자. 역시나 매우 간단하다.

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

여기까지만 하고 EdgeServer 를 실행해보면 실행 로그에 아래와 같은 로그가 나오게 된다.

2016-01-31 14:56:36.705  INFO 6917 --- [           main] o.s.c.n.zuul.web.ZuulHandlerMapping      : Mapped URL path [/product/**] onto handler of type [class org.springframework.cloud.netflix.zuul.web.ZuulController]
2016-01-31 14:56:36.705  INFO 6917 --- [           main] o.s.c.n.zuul.web.ZuulHandlerMapping      : Mapped URL path [/review/**] onto handler of type [class org.springframework.cloud.netflix.zuul.web.ZuulController]
2016-01-31 14:56:36.705  INFO 6917 --- [           main] o.s.c.n.zuul.web.ZuulHandlerMapping      : Mapped URL path [/config-server/**] onto handler of type [class org.springframework.cloud.netflix.zuul.web.ZuulController]
2016-01-31 14:56:36.705  INFO 6917 --- [           main] o.s.c.n.zuul.web.ZuulHandlerMapping      : Mapped URL path [/recommendation/**] onto handler of type [class org.springframework.cloud.netflix.zuul.web.ZuulController]
2016-01-31 14:56:36.705  INFO 6917 --- [           main] o.s.c.n.zuul.web.ZuulHandlerMapping      : Mapped URL path [/discovery/**] onto handler of type [class org.springframework.cloud.netflix.zuul.web.ZuulController]
2016-01-31 14:56:36.705  INFO 6917 --- [           main] o.s.c.n.zuul.web.ZuulHandlerMapping      : Mapped URL path [/composite/**] onto handler of type [class org.springframework.cloud.netflix.zuul.web.ZuulController]

로그를 보면 Eureka server에 등록된 모든 서비스 인스턴스들이 /{service instance name}의 형태로 proxy가 설정된 것을 볼 수 있다. 기본 설정 상태에서 실행하면 그렇게 된다는 거다.
저 중에서 composite api 에만 접근할 수 있도록 proxy를 만들고 싶다. 그래서 아래와 같은 설정을 Edge server의 설정 파일에 추가했다. 아래 내용은 github 에 등록되어있는 edge-server.yml 파일에서 Zuul 설정 부분이다.

zuul:
  ignoredServices: '*'
  routes:
    composite:
      path: /composite/**



우선 zuul.ignoredServices 를 통해서 모든 요청의 proxy를 제거한다.
그다음 zuul.routes.{service}.path 를 통해서 설정하면 된다.
위의 경우는 localhost:9000/composite/ 로 들어오는 모든 요청을 composite 서비스로 보낸다는 의미라 하겠다. 이제 다시 Edge server를 실행하면 위의 로그 부분은 없어지고 아래와 같은 로그가 나오게 된다.

2016-01-31 15:07:35.079  INFO 7356 --- [           main] o.s.c.n.zuul.web.ZuulHandlerMapping      : Mapped URL path [/composite/**] onto handler of type [class org.springframework.cloud.netflix.zuul.web.ZuulController]

이제 Composite API 만 공개하게 되고, 클라이언트는 설정된 /composite/produt/?size=10&page=1 라던가 /composite/product/1 같은 형태로 접속해서 서비스를 이용할 수 있게 되었다.

Zuul proxy를 이용할 때 한가지 조심해야 하는 부분이 있는데, Zuul proxy를 이용해서 대용량 파일을 보낼 때는 약간의 설정을 추가해 줄 필요가 있다. 이는 Zuul proxy의 기본 timeout 설정 때문에 발생하는 문제인 것으로 보이는데, Spring cloud documentation을 확인해 보면 아래와 같은 설정으로 해결할 수 있다고 되어있다.

hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 60000
ribbon:
  ConnectTimeout: 3000
  ReadTimeout: 60000

Spring cloud documentation을 보면 Netflix 는 아래와 같은 목적으로 Zuul을 사용한다고 한다.

  • Authentication
  • Insights
  • Stress Testing
  • Canary Testing
  • Dynamic Routing
  • Service Migration
  • Load Shedding
  • Security
  • Static Response handling
  • Active/Active traffic management

결론


개인적으로 생각할 때는 이 포스트에 정리한 내용이 Spring cloud 를 이용해서 마이크로서비스를 구현하는 것에 있어서 가장 핵심이 되지 않을까 싶다. 이전 포스트에서 정리했던 Eureka server의 구성도 결국 이 작업을 위해서 필요한 것이 아닐까 생각한다. 처음 Spring cloud 에 관심을 갖게 된 부분도 오늘 정리한 부분 때문이었다. 특정 아이피/포트/호스트/도메인 을 이용한 API 간의 통신이 아닌 인스턴스 아이디를 이용한 통신이 가능하다는 것이 너무 매력적이었다. 게다가 로드밸런싱까지 해 준다니... 처음 접했을 때는 거의 혁명에 가깝다고 생각할 정도였다.

워낙에 뭔가를 정리하는 데에는 능력이 없어서 정리가 매끄럽지 않지만, calista 블로그의 내용가 Josh Long 의 Devoxx 2015 발표 동영상 등을 참고한 후에 내 github에 있는 소스를 본다면 조금은 이해가 쉽지 않을까 하는 생각을 해본다.



댓글

  1. 블로그 잘봤습니다. 감사합니다.
    혹시 Edge server 에 호출시 "Load balancer does not have available server for client: {service-id}" 오류 가 납니다. service-id 는 eureka 에 등록된 샘플욜 resource api server 입니다.
    edge server 가 service-id 는 인식하고 라우팅을 하려고 하는 거 같은데 위와 같은 오류가 나서 500 오류를 출력하네요.

    답글삭제
    답글
    1. 부족한 블로그 봐주시고 답글까지 감사합니다.
      블로그 소스를 다시 구동해 봤습니다만, 해당 오류를 볼 수가 없네요.
      Configuration 서버와 Discovery(Eureka) 서버가 정상적으로 기동되었다면 일반적으로 라우팅이 되어야 할텐데 말이죠.
      Edge 서버의 Exception 로그를 trace 된 부분까지 포함해서 댓글이나 메일로 주실 수 있을런지요?
      제가 많은 경우를 해 보지 못한 상태에서 정리를 한 상황이라 바로 답변을 드리지 못해 죄송합니다.

      삭제
    2. 아닙니다^^ 정말 유익한 정보였습니다. 감사합니다.^
      컨피그 서버는 필수가 아니라서 생략 했습니다.
      문제는 해결했습니다....
      왜그런지는 모르겠지만 maven dependency 를 수정하니 정상 작동이 되네요. spring-cloud-netflix-starter-eureka 가 아닌 spring-cloud-netflix-eureka-client 로 설정이 되어 있었습니다.

      삭제
    3. 해결하셨다니 다행입니다.
      dependency의 문제였군요.
      아직 저도 실전 적용을 못 한 상황이라 많은 경우를 경험해보지 못해서 정확한 정보를 드리지 못하고 있네요.
      저 혼자 볼거라 생각하고 너무 성의없게 정리한게 아닌가 반성해봅니다. 앞으로 포스트를 쓸 때는 좀 더 노력해야겠네요. ^^

      삭제

댓글 쓰기

이 블로그의 인기 게시물

경력 개발자의 자기소개서에 대해서...

갑자기 뜬금없이 이런 글을 쓰다니 무슨 생각이야? 라고 생각하시는 분들이 있을지도 모르겠네요. 뜬금없음에 대한 변명은 잠시 접어두고 일단 오늘 쓰려고 하는 글을 시작해볼까 합니다. 개발자로 대충 16년을 그럭저럭 보내왔습니다. 시대적 상황으로 5년 차쯤에 대리로 처음 팀장을 시작했으니, 일반 개발자로 산 시간보다는 어쨌건 프로젝트 또는 팀의 리더로 산 시간이 더 많았던 것 같습니다. 그 기간 동안 남들보다 좀 심하게 회사를 많이 옮겨 다니다 보니 꽤 많은 면접을 볼 수 있는 경험이 있었고, 또 옮긴 회사가 대부분 팀을 리빌딩하는 곳이었다 보니 꽤 많은 채용절차에 관여할 기회가 있어서 어린 나이부터 비교적 많은 이력서를 검토했고 면접관으로도 여러 사람을 만날 수 있었습니다. 처음 면접을 보러 다니던 시절의 제 이력서의 자기소개서는 항상 "19XX년 봄 XX업계에 종사하시던 아버님과 집안일에 헌신적인 어머니의 유복한 가정에 1남 1녀의 막내로..." 로 시작되었습니다 (이 문장에 향수를 느끼시는 분들 많으실 거예요. ^^). 경력이 5년이 넘은 어느 날 도대체 이 문장을 왜 써야 하느냐는 의문이 생겨서 조금 바꾸긴 했습니다만, 그 뒤로도 꽤 오랜 세월을 이런 자기소개서가 항상 제 이력서에 붙어있었죠. 요즘 누가 저런 식으로 자기소개서를 써? 라고 생각하시는 분들 많으실 거로 생각해요. (대신 요즘은 대학 시절의 봉사활동이나 해외연수 이력이... 뭐 어차피 그놈이 그놈입니다.) 저런 자기소개서를 써야 한다는 것이 어디서 어떻게 시작된 것인지는 몰라도 회사를 그만두기 전인 2년 전까지도 약간의 표현은 다를지 모르지만 비슷한 문장으로 시작하는 자기소개서를 이력서에 첨부해서 보내는 지원자들을 볼 수 있었습니다. 이제 제가 뜬금없는 이런 글을 쓰게 된 이유를 밝히고 계속 진행해야겠네요. 블로그에 올릴 글을 준비하는 일이 생각보다 힘들어요. 블로그에 올리려고 준비한 주제에 맞는 소스를 작업하고 거기에 글을 입히다 보면 가끔

Springframework 5에서 바뀌는 것들에 대한 간단 정리 및 생각

Spring framework 5 에 대해 많은 분이 기대와 두려움을 가지고 계시지 않을까 생각합니다. 특히 기대를 하고 계신 분들은 Reactive Programming 지원을 기대하고 계시지 않은가 생각이 드는데요. 7월 초에 John Thompson 이란 분이 D-Zone에 아주 깔끔하고 멋지게 정리를 잘해서 글을 쓰셨더라구요. 해당 글은  https://dzone.com/articles/whats-new-in-spring-framework-5 에서 확인을 하실 수 있습니다. 혹시 Spring framework 5에서 달라지는 내용의 좀 더 자세한 내용이 필요하신 분들은 Spring framework github의 wiki 를 참고하시면 됩니다. 본 포스트는 언제나 그렇듯이 윗글에 대한 번역이 아닙니다. 그저 윗글을 다시 정리하면서 제 생각을 한번 정리해 놓은 포스트입니다. Spring framework 5는 현재 5.0.0.RC2(2017.07.23일 기준)까지 릴리즈된 상황입니다. Spring framework 5에서 크게 변화하는 내용을 John Thompson은 8가지로 깔끔하게 정리해주고 있습니다. 1. JDK 지원 버전의 업데이트 5버전은 원래 JDK 9 버전의 지원을 위해서 시작됐던 프로젝트로 알고 있는데 맞는지는 모르겠네요. JDK 9의 Release가 늦어져서 Spring framework 5가 먼저 Release 될 것으로 보이지만, JDK 9가 Release가 되면 언제건 적용할 수 있다고 합니다. 좀 아쉬운 부분은 JDK의 최소 버전은 JDK 8이라는 부분이 아닐까 싶네요. 이 때문에 Spring framework 5에 무관심한 분들도 많으실 거라고 생각합니다. 지금 진행하는 프로젝트는 JDK 8을 기반으로 합니다만, 최근까지 다니던 회사의 경우는 JDK 7까지가 업그레이드 한계였던 회사였습니다. 아마도 JDK 업그레이드를 쉽게 못 하시는 회사들이 많으니 "나랑은 관계없는 얘기군"

Gradle 을 이용해서 Multiproject 를 구성하는 방법 (중 하나)

개요 회사에서 Gradle 을 이용하게 된 이래로 계속 Multi-project 형태로 설정하여 진행을 해 오고 있다. 매번 멀티 프로젝트 형태로 만들어지고 있는 회사의 프로젝트들이다 보니 그때마다 다시 이전 빌드 스크립트를 보면서 만드는데 프로젝트들이 복잡하다보니 필요없는 설정들까지 복사해서 쓰고 있는 부분들이 있어서 한번 정리를 했으면 하고 있었다. 가장 기본적인 상태의 멀티프로젝트용 build.gradle 에 대한 여러가지 방법 중 한가지라 생각하고 참고가 된다면 좋겠다. 요구사항 기본 요구사항은 다음과 같다. 1) 멀티프로젝트는 디렉터리를 기반으로 아래와 같이 그룹으로 만들어 질 수 있어야 한다.   - Shared : 다른 프로젝트에서 Dependency로 추가될 수 있는 공통 라이브러리를 포함하는 라이브러리 모듈 그룹   - Web : Front 모듈 그룹   - Server : 주로 어플리케이션 간의 설정 등을 관리하는 Server 모듈 그룹   - Service : Web API 서버 모듈 그룹 2) 모든 Subproject 들은 Java project 이며, 프로젝트 명은 "모듈그룹명-모듈명" 으로 만든다. 즉, Server 모듈의 configuration-server 모듈이라면 server-configuration-server의 형태로 만들어지면 된다. 이후에 작업된 모든 내용은 Ubuntu 14.04 OS와 IntelliJ IDEA 15에서 작업되었다. (윈도우즈와 이클립스에서도 동일한 내용을 동작이 될 것으로 보인다. 다만, 테스트되지 않았을 뿐이다) Main Gradle Script 작업 1) 파일 구성   - build.gradle : gradle script 파일   - settings.gradle : sub project 관리를 위한 파일 2) build.gradle 파일 작성   - 우선은  프로젝트 기본 정보로 group 정보와 version 을 설정한다. 본인의