Server-side Discovery

Server-side Discovery는 클라이언트가 서비스의 위치를 직접 찾지 않고, 중간에 위치한 로드 밸런서나 프록시 서버가 서비스 위치를 찾아 요청을 라우팅하는 방식이다.

Server-side Discovery는 클라이언트를 단순화하고 중앙 집중식 관리를 가능하게 하는 장점이 있지만, 추가 인프라와 관리가 필요한 단점도 있다. 프로젝트의 요구사항과 팀의 역량을 고려하여 적절히 선택해야 한다.

작동 원리

  1. 서비스 등록: 각 서비스 인스턴스는 시작 시 자신의 정보를 서비스 레지스트리에 등록한다.
  2. 클라이언트 요청: 클라이언트는 서비스의 실제 위치를 모르고, 로드 밸런서에 요청을 보낸다.
  3. 서비스 조회: 로드 밸런서는 서비스 레지스트리에서 해당 서비스의 가용한 인스턴스 정보를 조회한다.
  4. 요청 라우팅: 로드 밸런서는 적절한 서비스 인스턴스를 선택하여 요청을 전달한다.
  5. 응답 반환: 서비스의 응답은 로드 밸런서를 통해 클라이언트에게 전달된다.

장점

  1. 클라이언트 단순화: 클라이언트는 서비스 디스커버리 로직을 구현할 필요가 없어 단순해진다.
  2. 언어 중립성: 클라이언트 측 구현이 필요 없어 다양한 프로그래밍 언어로 개발된 서비스들을 쉽게 통합할 수 있다.
  3. 보안 강화: 로드 밸런서 수준에서 추가적인 보안 계층을 구현할 수 있다.
  4. 중앙 집중식 관리: 서비스 디스커버리와 로드 밸런싱을 중앙에서 관리할 수 있다.

단점

  1. 추가 인프라 필요: 로드 밸런서나 프록시 서버와 같은 추가 인프라가 필요하다.
  2. 단일 실패 지점: 로드 밸런서가 단일 실패 지점이 될 수 있어 고가용성 설계가 중요하다.
  3. 복잡성 증가: 전체 시스템의 복잡성이 증가할 수 있다.

구현 예시

  • AWS Elastic Load Balancer (ELB): 클라이언트는 ELB의 DNS 이름을 통해 요청을 보내며, ELB는 등록된 EC2 인스턴스나 ECS 컨테이너 사이에서 부하를 분산한다.
  • Kubernetes의 kube-proxy: Kubernetes에서는 각 노드에서 실행되는 kube-proxy가 서비스 디스커버리와 로드 밸런싱을 담당하며, 클러스터 내의 서비스 요청을 적절한 파드(Pod)로 전달한다.
  • Node.js를 사용한 Server-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
// 서버 사이드 디스커버리 라우터 구현
class ServiceRouter {
    constructor(options = {}) {
        this.registryUrl = options.registryUrl || 'http://service-registry:8500';
        this.serviceCache = new Map();
        this.cacheTimeout = options.cacheTimeout || 30000; // 30초
        this.loadBalancer = new LoadBalancer();
    }

    async handleRequest(req, res) {
        const serviceName = this.extractServiceName(req);
        
        try {
            // 서비스 인스턴스 찾기
            const serviceInstance = await this.findServiceInstance(serviceName);
            
            // 요청 전달
            const response = await this.forwardRequest(req, serviceInstance);
            
            // 응답 전달
            this.sendResponse(res, response);
            
        } catch (error) {
            this.handleError(res, error);
        }
    }

    async findServiceInstance(serviceName) {
        // 캐시된 서비스 확인
        const cachedInstances = this.getCachedInstances(serviceName);
        if (cachedInstances && cachedInstances.length > 0) {
            return this.loadBalancer.selectInstance(cachedInstances);
        }

        // 서비스 레지스트리 조회
        const instances = await this.queryRegistry(serviceName);
        this.updateCache(serviceName, instances);
        
        return this.loadBalancer.selectInstance(instances);
    }

    async forwardRequest(req, serviceInstance) {
        const targetUrl = this.buildTargetUrl(serviceInstance, req.path);
        
        return await fetch(targetUrl, {
            method: req.method,
            headers: req.headers,
            body: req.body,
            timeout: 5000
        });
    }
}

참고 및 출처