Client-side Discovery

Client-side Discovery는 서비스 클라이언트가 직접 서비스 레지스트리에 질의하여 필요한 서비스의 위치 정보를 얻고, 그 정보를 바탕으로 서비스를 호출하는 방식이다.

Client-side Discovery는 마이크로서비스 환경에서 유연하고 확장 가능한 서비스 디스커버리 방식을 제공한다. 그러나 클라이언트의 복잡도가 증가하는 단점이 있으므로, 프로젝트의 요구사항과 팀의 기술 스택을 고려하여 적절히 선택해야 한다.

주요 구성 요소

  1. 서비스 레지스트리(Service Registry): 각 서비스 인스턴스의 네트워크 위치(예: IP 주소, 포트)를 저장하고 관리하는 데이터베이스이다. 서비스 인스턴스는 시작 시 자신의 정보를 레지스트리에 등록하고, 종료 시 등록을 해제한다.

  2. 서비스 인스턴스(Service Instance): 실제로 배포되어 실행 중인 서비스의 개별 실행 단위를 말한다.

  3. 서비스 클라이언트(Service Client): 다른 서비스의 기능을 사용하기 위해 해당 서비스에 요청을 보내는 애플리케이션 또는 서비스이다.

작동 원리

  1. 서비스 등록: 각 서비스 인스턴스는 시작 시 자신의 네트워크 위치(IP 주소와 포트)를 서비스 레지스트리에 등록한다.
  2. 서비스 조회: 클라이언트는 서비스 레지스트리에 질의하여 필요한 서비스의 위치 정보를 얻는다.
  3. 로드 밸런싱: 클라이언트는 받은 정보를 바탕으로 로드 밸런싱 알고리즘을 적용하여 서비스 인스턴스를 선택한다.
  4. 서비스 호출: 선택된 서비스 인스턴스에 직접 요청을 보낸다.
  5. 서비스 해제: 서비스 인스턴스가 종료될 때 서비스 레지스트리에서 자동으로 제거된다.

장점

  1. 단순성: 추가적인 중간 계층 없이 구현이 비교적 간단하다.
  2. 유연한 로드 밸런싱: 클라이언트가 서비스 인스턴스를 직접 선택하므로, 각 서비스에 맞는 로드 밸런싱 전략을 구현할 수 있다.
  3. 성능: 중간 계층을 거치지 않아 응답 시간이 빠르다.

단점

  1. 클라이언트 복잡도 증가: 서비스 디스커버리 로직을 클라이언트에 구현해야 한다.
  2. 언어 종속성: 각 프로그래밍 언어와 프레임워크에 맞는 클라이언트 라이브러리가 필요하다.
  3. 서비스 레지스트리 의존성: 클라이언트와 서비스 레지스트리 간의 강한 결합이 생긴다.

구현 예시

  1. Spring Cloud Netflix Eureka는 Client-side Discovery 패턴을 구현한 대표적인 예이다.

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

    위 코드에서 @EnableDiscoveryClient 어노테이션을 통해 해당 애플리케이션을 Eureka 클라이언트로 등록할 수 있다.

  2. Node.js를 사용한 Client-side Discovery

     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
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    
    // 서비스 디스커버리 클라이언트 구현
    class ServiceDiscoveryClient {
        constructor(options = {}) {
            this.registryUrl = options.registryUrl || 'http://service-registry:8500';
            this.serviceCache = new Map();
            this.cacheTimeout = options.cacheTimeout || 30000; // 30초
        }
    
        async getServiceInstances(serviceName) {
            // 캐시된 서비스 정보 확인
            const cachedInstances = this.getCachedInstances(serviceName);
            if (cachedInstances) {
                return cachedInstances;
            }
    
            try {
                // 서비스 레지스트리에서 인스턴스 조회
                const response = await fetch(
                    `${this.registryUrl}/v1/health/service/${serviceName}`
                );
                const instances = await response.json();
    
                // 건강한 인스턴스만 필터링
                const healthyInstances = instances.filter(
                    instance => instance.Checks.every(check => check.Status === 'passing')
                );
    
                // 캐시 업데이트
                this.updateCache(serviceName, healthyInstances);
    
                return healthyInstances;
            } catch (error) {
                throw new Error(`서비스 디스커버리 실패: ${error.message}`);
            }
        }
    
        // 로드 밸런서 구현
        async getNextInstance(serviceName) {
            const instances = await this.getServiceInstances(serviceName);
    
            if (!instances || instances.length === 0) {
                throw new Error(`사용 가능한 ${serviceName} 인스턴스가 없습니다.`);
            }
    
            // 라운드 로빈 방식으로 인스턴스 선택
            const instance = instances[this.getNextIndex(instances.length)];
            return this.formatInstanceUrl(instance);
        }
    
        // 서비스 호출 구현
        async callService(serviceName, path, options = {}) {
            const instance = await this.getNextInstance(serviceName);
    
            try {
                const response = await fetch(`${instance}${path}`, {
                    ...options,
                    timeout: options.timeout || 5000
                });
    
                if (!response.ok) {
                    throw new Error(`Service call failed: ${response.statusText}`);
                }
    
                return await response.json();
            } catch (error) {
                // 실패한 인스턴스 처리
                this.handleFailedInstance(serviceName, instance, error);
                throw error;
            }
        }
    }
    

참고 및 출처