Hello, Freakin world!

[Eureka] 서비스 검색하기 - DiscoveryClient 본문

Spring Cloud/Service discovery

[Eureka] 서비스 검색하기 - DiscoveryClient

johnna_endure 2021. 2. 19. 14:38
 

[Eureka] 서비스 디스커버리에 서비스 등록하기

시나리오 간단하게 유레카 서버와 유레카에 등록되는 서비스 서버를 띄운다. 그리고 유레카 서버의 대시보드를 활용해 서비스 서버가 등록되는지 확인해본다. 유레카 서버 스프링 이니셜라이

javachoi.tistory.com

지난 글에서 서비스를 등록해봤으니 이제 서비스 검색을 구현해보자.


아키텍쳐

 

전체적인 아키텍쳐는 다음 그림과 같다.

 

출처 : https://www.codeprimers.com/client-side-service-discovery-in-spring-boot-with-netflix-eureka/

 

각 서비스는 시작과 동시에 유레카 서버에 등록된다. 서비스끼리 서로를 호출할 때 유레카 서버가 이를 중개해 라우팅해준다.

이 때문에 각 서비스 인스턴스는 서로의 물리적인 위치를 몰라도 된다.

유레카 클라이언트는 리본을 사용해 클라이언트측 로드 밸런싱을 수행한다. 이를 위해 서비스 레지스트리를 로컬에 캐싱하고 주기적으로 유레카 서버와 통신하면서 이를 업데이트한다.

서비스 레지스트리를 로컬에 캐싱하기 때문에 유레카 서버가 멈추더라도 서비스끼리 통신할 수 있다는 점이 장점이다.


서비스 검색

서비스 검색에는 몇 가지가 있다.

우선은 DiscoveryClient를 직접 이용해 통신해보자.


시나리오

화살표가 좀.. 크흠

위 그림처럼 간단한 member 서비스와 team 서비스를 구현할 예정이다.

 

웹에서 멤버 정보를 요청할 때 member가 팀에 가입된 상태라면 member 서비스는 team 서비스에 해당 팀의 정보를 요청한다. 그리고 member 서비스에서 정보들을 조합해 사용자에게 응답을 반환한다.

 

member 서비스가 team 서비스를 호출하는 과정에서 클라이언트측 로드 밸런싱 기능을 확인하고 싶기 때문에 team 서비스는 인스턴스가 2개인 클러스터로 구성하자. 이는 도커 컴포즈로 구현할 예정이다.


유레카 서버 구현

 

build.gradle

plugins {
    id 'org.springframework.boot' version '2.4.3'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'springboot.cloud'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

ext {
    set('springCloudVersion', "2020.0.1")
}

dependencies {
    implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

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

test {
    useJUnitPlatform()
}

 

application.properties

server.port=8761

eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false

 

EurekaserverApplication.class

@EnableEurekaServer
@SpringBootApplication
public class EurekaserverApplication {

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

}

유레카 서버 구현이 끝났다. 이는 이미 전글에 다룬 내용이므로 패스~


멤버 서비스 구현

 

멤버 서비스는 개념적으로 3가지 부분으로 나뉜다.

 

1. 멤버 정보를 저장/조회 가능한 리파지토리

2. 공개할 api가 있는 컨트롤러

3. 팀 서비스와 통신하기 위한 TeamDiscoveryClient

 

1번은 스프링 데이터에서 제공하는 JpaRepository를 그대로 이용하고 2번은 @RestController를 이용한 컨트롤러다.

모든 코드를 일일이 설명하기엔 글이 너무 길어길 것 같으니까 중요한 3번만 살펴보자.

 

먼저 시동 클래스에 @EnableDiscoveryClient를 추가한다.

 

MemberserviceApplication.class

import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
...

@EnableDiscoveryClient
@SpringBootApplication
public class MemberserviceApplication {

    @Autowired
    MemberRepository memberRepository;
    public static void main(String[] args) {
        SpringApplication.run(MemberserviceApplication.class, args);
    }

}

@EnableDiscoveryClient를 추가하면 스프링부트는 DiscoveryClient 타입의 빈을 생성해준다.


TeamDiscoveryClient.class

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.client.RestTemplate;
...


@Component
public class TeamDiscoveryClient {

    private final DiscoveryClient discoveryClient;

    public TeamDiscoveryClient(DiscoveryClient discoveryClient) {
        this.discoveryClient = discoveryClient;
    }

    public TeamDto getTeam(Long teamId) {
        List<ServiceInstance> instances = discoveryClient.getInstances("team-service");

        int index = getRandomIndex(instances.size());
        ServiceInstance instance = instances.get(index);
        System.out.format("index : %d, instance : %s\n", index, instance);

        String serviceUrl = String.format("%s/teams/%d", instance.getUri(), teamId);

        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<TeamDto> exchange = restTemplate.exchange(serviceUrl, HttpMethod.GET, null, TeamDto.class);
        return exchange.getBody();
    }

    private int getRandomIndex(int size) {
        Random random = new Random();
        return random.nextInt(size);
    }

}

DiscoveryClient는 내부적으로 넷플릭스 리본 라이브러리를 이용해 클라이언트측 로드 밸런싱을 수행한다.

discoveryClient 객체의 getInstances(서비스 이름)을 호출하면 서비스 레지스트리에서 해당 서비스의 정보를 가져 온다.

정보를 가져올 때, 캐시가 있다면 유레카에 요청을 보내지 않는다. 이 캐시는 주기적으로 갱신된다.

 

예제에선 RestTemplate 객체를 직접 생성해 웹 요청을 보냈다. 이는 DiscoveryClient를 직접 이용하기 위한 억지스런 방식이다.

@EnableDiscoveyClient 애너테이션을 추가하면 스프링은 리본 인터셉터를 주입하고 RestTemplate 호출을 인터셉트한다.

이 때문에 RestTemplate의 동작 방식이 바뀌게 되는데 이번 예제에서는 DiscoveryClient만을 이용하기 위해

빈으로 등록된 RestTemplate가 아닌 직접 생성된 RestTemplate를 이용했다.

 

이 방법의 단점은 다음과 같다.

 

1. 로드 밸런싱 코드를 직접 작성해야 한다.

- 예제에서는 랜덤 인덱스를 만들어 호출될 인스턴스를 정했다.

2. 호출 URL을 직접 조립해서 만들고 있다.

- 이 과정은 스프링 빈으로 등록된 RestTemplate를 사용하면 이 과정을 좀 더 편하게 할 수 있다.


팀 서비스 구현

 

팀 서비스의 구현은 간단하다.

Team 엔티티와 리포지토리, RestController 만 간단하게 생성하자.(전체 코드는 깃허브에 올려둘테니 궁금하신 분들은 맨 아래 링크를 통해 살펴보세용)

 

application.yml

server:
  port: 8081

spring:
  application:
    name: team-service

  datasource:
#    url: jdbc:mysql://localhost:3306/team_database
    url: jdbc:mysql://team-db:3306/team_database
    username: sa
    password: sa

  jpa:
    show-sql: true
    hibernate:
      ddl-auto: create
    properties:
      hibernate.dialect: org.hibernate.dialect.MySQL5Dialect

eureka:
  client:
    serviceUrl:
#      defaultZone: http://localhost:8761/eureka/
      defaultZone: http://eureka-server:8761/eureka/
    fetch-registry: true
    register-with-eureka: true
  instance:
    prefer-ip-address: true

도커로 실행환경 구성하기

 

먼저 각 서비스의 도커파일을 생성하자

 

멤버 서비스

Dockerfile

FROM openjdk:11
RUN mkdir /app
ADD build/libs/memberservice-1.0.jar /app
CMD [ "java", "-jar", "/app/memberservice-1.0.jar" ]

팀 서비스

Dockerfile

FROM openjdk:11
RUN mkdir /app
ADD build/libs/teamservice-1.0.jar /app
CMD [ "java", "-jar", "/app/teamservice-1.0.jar" ]

유레카 서버

Dockerfile

FROM openjdk:11
RUN mkdir /app
ADD /build/libs/eurekaserver-0.0.1-SNAPSHOT.jar /app
CMD [ "java", "-jar", "/app/eurekaserver-0.0.1-SNAPSHOT.jar" ]

그리고 컨테이너들을 조합해 사용하기 위해 docker-compose.yml 을 작성.

 

docker-compose.yml

version: "3.0"
services:
  member-service:
    build: ./memberservice
    ports: 
      - 8080:8080
    restart: on-failure
    depends_on: 
      - eureka-server
      - member-db

  member-db:
    image: mysql:8
    ports: 
      - 3307:3306
    environment: 
      MYSQL_ROOT_PASSWORD: root_password
      MYSQL_USER: sa
      MYSQL_PASSWORD: sa
      MYSQL_DATABASE: member_database
    
  team-service-1:
    build: ./teamservice
    ports:
      - 8081:8081
    restart: on-failure
    depends_on: 
      - eureka-server
      - team-db
  
  team-service-2:
    build: ./teamservice
    ports:
      - 8082:8081
    restart: on-failure
    depends_on: 
      - eureka-server
      - team-db
  
  team-db:
    image: mysql:8
    ports: 
      - 3308:3306
    environment: 
      MYSQL_ROOT_PASSWORD: root_password
      MYSQL_USER: sa
      MYSQL_PASSWORD: sa
      MYSQL_DATABASE: team_database
  
  eureka-server:
    build: ./eurekaserver
    restart: on-failure
    ports: 
      - 8761:8761
  

실행 및 확인

 

후~ 얼추 완성됐다. 일단 유레카 서버 대시보드를 확인해 서비스들이 등록됐는지 확인해보자.

멤버 서비스 1개, 팀 서비스 2개 모두 등록됐다!

 

로드 밸런싱이 수행되는 걸 확인하려면 /member/{memberId}가 호출될 때마다 콘솔에 찍히는 메세지를 확인하면 됩니다.

도커 컴포즈를 실행할 때 -d 옵션을 주면 실시간 로그를 확인할 수 없다. 이럴 땐 -d 옵션을 빼고 실행.

 

0과 1이 랜덤하게 생성되면서 라우팅 되는걸 확인할 수 있다.

 

휴~~~~~~~~~~~ 길이 꽤 길어졌다.

다음 글은 DiscoveryClient를 직접 이용하는 대신 스프링에서 주입하는 RestTemplate를 이용해보자.

 


전체 코드

 

 

johnna-endure/service-registry-study

Contribute to johnna-endure/service-registry-study development by creating an account on GitHub.

github.com

 

Comments