ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 5. [MSA 구현 퀵스타트] 서비스 디스커버리 초간단 구현
    Spring Cloud 2021. 3. 28. 14:55

    이번 장에서는 초 간단한 서비스 2개(Service1, Service2)를 만들고 이를 서비스디스커버리 서버인 Eureka Server에 연계하는 작업을 구현해 보겠습니다.

     

    예제 소스

    이번장에 진행될 소스는 아래 Github에서 다운로드 받으시고, "1.Discovery&Service" branch를 체크아웃해주세요.

    Postman API

    또한, 예제를 실행하기위한 API를 POSTMAN Collection을 함께 올렸습니다. 뒤늦게 올려서 최종 main branch에서만 보일 것입니다.

    "Postman" 폴더 들어가 보시면, "Simple MSA.postman_collection.json" 파일이 있습니다. 해당 파일을 설치된 Postman에 import하시면 됩니다.

     

    이번장 개요

    그림5-1 Eureka Server와 서비스 연계

    Service1,2 는 기동시에 1.서비스디스커버리 서버에 서비스 자신을 등록하고 정보를 전달합니다. (앞으로 서비스디스커버리 서버와 Eureka 서버 단어를 혼용해서 쓸수도 있습니다. 같은 의미로 받아들여 주세요.)

    이후 Service1,2는 2.주기적(기본 30초)으로 서비스디스커버리 서버로 부터 다른 서비스의 정보를 가져갑니다(fetch).

     

    이번 예제 구현을 통해 아래 사항을 확인하실 수 있습니다.

    • 서비스디스커버리 서버에 등록된 서비스들의 상세 정보 확인
    • 각 서비스에서 가져간 서비스 목록 확인
    • Service에 구현된 API 기능 동작 확인

    예제는 Spring Boot 2.3.9.RELASE, Spring Cloud Hoxton.SR10 환경에서 구현 되었습니다.

     

     

    서비스 디스커버리 서버 구현

    먼저 Spring Boot 기반으로 "ServiceDiscovery" 명의 프로젝트를 생성합니다.

     

    Library Dependency (build.gradle)

    1
    2
    3
    4
    5
    dependencies {
        implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
        implementation 'org.springframework.boot:spring-boot-starter-actuator'
        .. 중략 ..
    }
    cs

    spring-cloud-starter-netflix-eureka-server (디스커버리 서버를 구성하는 라이브러리)를 설정합니다.

    (이외 라이브러리는 Spring Cloud와 큰 관련이 없기에 생략하였습니다. 소스를 참고해주세요.)

     

    EurekaServer 활성화

    1
    2
    3
    4
    5
    6
    7
    @EnableEurekaServer
    @SpringBootApplication
    public class ServiceDiscoveryApplication {
        public static void main(String[] args) {
            SpringApplication.run(ServiceDiscoveryApplication.class, args);
        }
    }
    cs

     

    ServiceDiscoveryApplication.java에 @EnableEurekaServer만 달아주면 됩니다. JAVA 코딩은 이것으로 끝입니다. 아주 간단하지요?

     

    application.yml 설정

    마지막으로 환경 설정을 해보겠습니다. 퀵스타트 편으로 간단하게 전체적은 MSA를 구성하는 것을 목표로 하기에 대다수의 설정은 Default 설정으로 구현하였습니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    server:
      port: 8761
     
    spring:
      application:
        name: service-discovery
     
    eureka:
      client:
        register-with-eureka: false # 유레카 서비스에 (자신을) 등록 여부
        fetch-registry: false  # (유레카 클라이언트 기반) 다른 서비스들의 정보를 fetch 유무
        serviceUrl:
          defaultZone: http://localhost:8761/eureka/ # 설정을 하지 않으면 기본 http://localhost:8761/ 로 연동된다.
     
      server:
        wait-time-in-ms-when-sync-empty: 5  # 서버가 요청을 받기 전 대기할 초기 시간(ms) : 거의 대기 없음 (default 5분 : 상용에서는 모든 서비스가 등록 되길 기다리기 위해 5분 후 정보를 공유)
     
    management:
      endpoints:
        web:
          exposure:
            include: "*"
    cs

    설정 설명

    • register-with-eureka : 유레카 서버에 클라이언트로서 등록할지 유무 입니다. 자기 자신이기에 여기서는 등록하지 않습니다. * 추후 유레카 서버 이중화 구성 포스팅에서 이 설정을 변경하도록 하겠습니다.
    • fetch-registry : 다른 서비스의 정보를 유레카 서버로 가져와 (로컬메모리에) 캐싱할지 여부입니다. 유레카 서버이기에 false로 합니다.
    • wait-time-in-ms-when-sync-empty : 유레카 서버는 등록된 서비스들의 정보를 공유한다고 하였습니다. 이 설정은 서비스들이 유레카 서버에 등록되기까지 얼마나 대기할지의 설정으로 기본값은 5분입니다. 즉 5분동안 서비스의 등록을 기다린 후 등록된 서비스의 정보를 공유 합니다. 개발환경에서는 기동후 빠른 테스트를 위해 5ms으로 값을 조정하였습니다.
    • management 이하 설정 : spring actuator의 설정입니다. 전 장에서 스프링 부트 운용을 위한 다양한 유틸성 API를 제공한다고 하였습니다. include: "*" 는 모든 API를 열겠다는 뜻입니다. 보안상 이슈가 있으니 실제 운영모드에서는 필요한 API만 지정해야 합니다. 이 장의 마지막에서 Postman을 이용한 실습에서 다뤄보겠습니다.

    이로써 유레카 서버의 구현이 완성 되었습니다. 

    유레카 서버를 기동해 보겠습니다.

    기동이 완료되면 웹브라우저에서 http://localhost:8761/를 열어봅니다. 다음과 같은 화면이 나오면 성공입니다.

    생각보다 간단하지 않으신가요?

     

    위 화면을 유레카서버 대시보드라고 합니다. 모니터링을 위한 다양한 값들이 있는데, 지금은 이런게 있다는 것을 알고 넘어가도록 하겠습니다.

     

     

    서비스 1 구현

    이번에는 서비스1을 구현해 보겠습니다. 스프링부트 생성으로 "Service1"을 생성합니다.

    Library Dependency (build.gradle)

    1
    2
    3
    4
    5
    dependencies {
        implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
        implementation 'org.springframework.boot:spring-boot-starter-actuator'
        .. 중략 ..
    }
    cs

    서버와는 달리 spring-cloud-starter-netflix-eureka-client 라이브러리를 설정합니다.

     

    application.yml 설정

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    server:
      port: 8011
     
    spring:
      application:
       name: service1
     
    eureka:
      instance:
        prefer-ip-address: true
      client:
        register-with-eureka: true # 유레카 서비스에 (자신을) 등록 여부
        fetch-registry: true
        serviceUrl:
          defaultZone: http://localhost:8761/eureka/
     
    management:
      endpoints:
        web:
          exposure:
            include: "*"
    cs
    여기서 중요한것이 "spring.application.name" 입니다. 앞서 이론을 설명한 장에서 통신을 할때는 IP:PORT 대신 각 서비스의 고유ID, 즉 어플리케이션 이름로 한다고 하였습니다.여기서 쓰이는 값입니다.
    이후 서비스 기동시에 유레카 대시보드에 어떻게 등록되는지 확인해 보겠습니다.

    prefer-ip-address : 유레카서버에 어플리케이션이름에 매핑되는 호스트명이 기본적으로 등록되게 됩니다. 이 값의 설정으로 호스트명이 아닌 IP 주소를 대신 등록하겠다는 것입니다.

    그 이유는 컨테이너 기반의 MSA 환경에서 보통 DNS서버가 없기에 임의로 생성된 호스트 네임을 부여 받습니다. 이는 결국 생성된 호스트 네임의 정상적인 위치를 얻지 못할수 있기에 명확한 IP로 등록해 달라고 지정하는 것입니다.

    defaultZone : 등록할 유레카 서버의 위치를 지정합니다.

     

    여기서 추가로 설명을 드리자면 eureka 설정은 크게 3가지로 분류 됩니다.

    1. 디스커버리 서버 자체에 대한 설정

    eureka.server.*의 접두어로 시작하는 설정입니다.

    서버로서 어떻게 구성될지의 설정을 합니다. (* 즉 Eureka 서버의 프로퍼티 설정에 들어가는 내용입니다.)

     

    2. 클라이언트 자체에 대한 설정

    eureka.instance.*의 접두어로 시작하는 설정입니다.

    클라이언트로서 어떻게 구성될지의 설정입니다. 

     

    3. 클라이언트 행위(Action) 설정

    eureka.client.*의 접두어로 시작하는 설정입니다.

    클라이언트가 연계할 유레카 서버 설정(defaultZone, register-with-eureka), 다른 서비스들의 정보를 가져오는(fetch-registry)등 행위에 대한 설정을 합니다.

     

    나머지 설정은 유레카 서버 설정에서 설명 드렸습니다.

     

    코드 구현

    유레카 클라이언트로서의 설정과 관련 API를 구현해 보겠습니다.

    1
    2
    3
    4
    5
    6
    7
    @SpringBootApplication
    @EnableDiscoveryClient
    public class Service1Application {
        public static void main(String[] args) {
            SpringApplication.run(Service1Application.class, args);
        }
    }
    cs
    우선 유레카 클라이언트라고 @EnableDiscoveryClient 어노테이션을 명시 합니다.

    이것으로 유레카 클라이언트로서의 구현은 완료된 것입니다.

     

    이후는 DiscoveryClient 객체를 이용하여 유레카 서버에 등록된 서비스 목록을 가져오는 API를 구현해 보겠습니다.

    소스는 DiscoveryController.java와 DiscoveryService.java 2개의 파일로 구성된 간단한 API입니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @RestController
    public class DiscoveryController {
        @Autowired
        DiscoveryService discoveryService;
     
        @GetMapping(value = "/services")
        public List<String> services() {
            return discoveryService.getServices();
        }
    }
    cs

    Controller에서 "/services" API를 구현 합니다. DiscoveryService를 호출합니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @Service
    public class DiscoveryService {
        @Autowired
        private DiscoveryClient discoveryClient;
     
        public List getServices(){
            List<String> services = new ArrayList<String>();
     
            /** 람다스트림 표현 */
            discoveryClient.getServices().forEach(serviceName -> {
                discoveryClient.getInstances(serviceName).forEach(instance->{
                    services.addString.format("%s:%s",serviceName,instance.getUri()));
                });
            });
            return services;
        }
    }
     
    cs

    유레카 서버에 등록된 서비스 목록을 가져오는 핵심 코드입니다. 

    DiscoveryClient는 유레카 클라이언트에서 제공하는 객체로서 서비스들의 정보를 찾아서 제공해주는 객체 입니다.

    위 소스는 유레카 서버에 등록된 모든 서비스들을 반복문을 돌면서 List에 저장 후 반환해 주는 소스입니다.

    * 람다 표현식에 익숙하지 않으실 분들을 위해 실제 Git 소스에는 for문으로도 구현하였으니 참고해 주세요.

     

    이로서 서비스1의 모든 구현이 완성 되었습니다.

     

    서비스 2구현

    마지막으로 서비스2를 구현해보겠습니다.

    Library Dependency와 application.yml 설정 및 @EnableDiscoveryClient 등록 코드는 서비스1과 동일함으로 생략하고 바로 API 구현을 해보겠습니다.

     

    앞장에서 서비스2의 기능은 고객의 {id}를 받으면 해당 이름을 {name} 반환하는 API가 있다고 하였습니다.

    • 일반적으로는 이름 조회 등의 기능은 DB에서 SQL로 가져오겠지만, 우리는 Spring Cloud에만 집중하고 복잡성을 제거하기 위해 다른 소스를 제외한다고 하였습니다. 하여 단순히 switch문으로 대체 하였습니다.

     * 우선 Spring Cloud를 충분히 이해하고 추후에 메모리 DB(H2 등) 혹은 DBMS에 연결하여, Mybatis나 JPA를 이용해 확장해보는 것 도 추천 드립니다.

     

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    // NameController.java
    @RestController
    @RequestMapping(value="/name")
    public class NameController {
     
        @Autowired
        NameService nameService;
     
        @GetMapping(value = "/{id}")
        public String name(@PathVariable("id"String id) {
            return nameService.getName(id);
        }
    }

    //NameService.java
    @Service
    public class NameService {
     
        public String getName(String id) {
            switch (id) {
                case "1":
                    return "Jesse";
                case "2":
                    return "Jimmy";
                default:
                    return "UnKnown";
            }
        }
    }
    cs

    소스는 아주 단순하여 별다른설명이 필요 없을 듯 합니다.

     

     

    동작 확인

    이번 장에의 모든 기능 구현이 완료 되었습니다.

    이제 유레카 서버, 서비스1, 서비스2 를 모두 실행한 후에 유레카 대시 보드를 확인해 보겠습니다.

     

    유레카 대시보드

    아까 화면과 다른점이 보이지 않나요? 처음 대시보드에서는 유레카 서버만 기동하였기에 서비스들의 정보가 나오지 않았습니다.

    이번 화면의 Instances currently registered with Eureka에서는 서비스1과 서비스2가 자신의 어플리케이션명(ID)으로 유레카에 등록된 것을 확인하실 수 있습니다.

    AvaliablAvailability Zones은 각각 1로 설정되어 있어 하나의 인스턴스가 등록되었음을 나타냅니다. (추후 이중화 구성으로 각각의 서비스를 2개씩 띄우면 2로 설정됩니다.)

    Status에는 현재 상태(UP)와 각 서비스당 등록된 인스턴스 명이 보입니다. 만약 서비스1을 8012포트로 하나 더 띄우면 

    hostname:service1:8011,hostname:service1:8012가 추가로 나타납니다.

    • 퀵스타트 과정이 끝난 후 모든 모듈의 이중화 구성을 진행해 보겠습니다.

    UP은 서비스가 등록된 상태를 뜻하고, DOWN으로 표시되면 서비스가 내려갔다는 것을 뜻합니다. DOWN 표시 이후 해당 서비스는 목록에서 제거 됩니다.

    실제 서비스의 기동을 중지하더라도 바로 서비스가 DOWN으로 전환되지는 않습니다. 그 이유는 유레카의 Self Preservation 모드가 가동중이기 때문입니다. 자세한 설명은 퀵스타트에서는 무거울 수 있으니 차후에 상세한 포스팅을 올리도록 하겠습니다. 지금은 클라이언트 보호를 위해 바로 등록제거하지 않고 일정시간(90초 이상) 유지 후에 삭제된다는 걸 인지 하시면 됩니다. (실제 기동을 중지하여 테스트를 해보는 것도 좋을 듯합니다.)

     

    API 실행

    공유된 Postman collection을 이용하여 API를 실행해보겠습니다. (GET 명령은 브라우저에서도 가능합니다.)

    이번장과 관련된 API는 총 3개로 구성되었습니다.

     

    첫번째 API (http://localhost:8761/eureka/apps)

    유레카 서버를 호출하여 등록된 서비스들의 상세정보를 확인합니다. 2개의 서비스가 UP 되었다는 내용과 각 서비스의 상세 정보를 알수 있습니다.

     

    두번째 API (http://localhost:8011/services)

    서비스1에 구현한 서비스들의 목록을 가져오는 API입니다.

    여기서 주의하실 점은 각 서비스들이 유레카 서버로 등록이 되었어도 바로 정보를 받아볼 수는 없다는 것입니다. (예제를 실행하면 대략 30초 이후에 서비스 리스트를 보실 수 있으실 겁니다.)

    그 이유는 유레카 클라이언트는 기본적으로 30초 주기로 유레카 서버로부터 정보를 가져오도록 구성되어 있습니다. 하여 빨리 정보를 확인하고 싶으신분들은 이 주기를 줄여야 합니다.

     

    각 클라인트의 application.yml에 아래 설정을 추가해보세요.

    1
    2
    3
    4
    5
    eureka:
      client:
        registryFetchIntervalSeconds: 5 # eureka에게 서비스 정보를 가져오는 주기
        disable-delta: true # eureka에게 서비스 정보를 가져올때 변경된 내용만 가져온다. (false로 하면 전체를 가져오기에 대역폭 낭비이다.)
     
    cs

    registryFetchIntervalSeconds를 통해 30초를 5초로 줄였습니다. disable-delta는 다른 서비스 정보중에 변경된 부분만 가져옴으로써 자원 낭비를 줄일 수 있습니다.

    • 실제 운영 모드에서는 5초를 짧게 설정하여 자원을 낭비하는 것을 권장하지 않습니다. 이중화 구성이 되어있고 재기동시에도 인스턴스 1대씩 간격을 두고 처리하여야 하기에 너무 짧은 간격으로 가져올 필요가 없습니다.

     

    세번째 API (http://localhost:8011/services)

    마지막으로 서비스2의 {id}를 통해 {name}을 가져오는 API 입니다. 별다른 설명이 필요 없을 듯 합니다.

     

    정리

    이로써 서비스 디스커버리의 전반에 대해서 구현해 보았습니다. 다음 장에서는 이렇게 등록되고 공유된 서비스 정보들을 이용하여 서비스간의 HTTP 통신을 할 수 있는 라이브러리를 알아보도록 하겠습니다.

    댓글

Designed by Tistory.