gRPC

gRPC는 Google에서 개발한 고성능, 오픈소스 RPC(Remote Procedure Call) 프레임워크이다.
‘g’는 원래 Google을 의미했지만, 현재는 “gRPC Remote Procedure Calls"의 재귀적 약자로 사용된다. 2015년에 공개되었으며, 현재 Cloud Native Computing Foundation(CNCF)의 졸업 프로젝트이다.

gRPC는 마이크로서비스 아키텍처와 높은 성능이 필요한 시스템에 이상적인 강력한 RPC 프레임워크이다. Protocol Buffers, HTTP/2, 코드 생성 및 다양한 통신 패턴을 통해 효율적이고 확장 가능한 API를 구축할 수 있다.

시작하기 전에 Protocol Buffers 문법, HTTP/2 기본 사항, 스트리밍 패턴 및 오류 처리에 대한 확실한 이해가 필요하다. 이러한 기초를 다진 후에는 점진적으로 더 복잡한 기능을 탐색하고 실제 프로젝트에 gRPC를 통합할 수 있다.

gRPC의 핵심 개념

Protocol Buffers (protobuf)

gRPC의 핵심 요소 중 하나는 Protocol Buffers(줄여서 protobuf)라는 데이터 직렬화 형식이다.
Protocol Buffers는 구조화된 데이터를 효율적으로 직렬화하는 Google의 언어 중립적, 플랫폼 중립적인 기술이다.

gRPC API를 정의할 때, 먼저 .proto 파일에 서비스와 메시지 형식을 정의한다.

아래는 간단한 예시:

 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
// user.proto 파일 예시
syntax = "proto3"; // 프로토콜 버퍼 버전 지정

package user;

// 사용자 서비스 정의
service UserService {
  // 사용자 정보를 가져오는 RPC
  rpc GetUser (UserRequest) returns (UserResponse) {}
  // 사용자 생성 RPC
  rpc CreateUser (CreateUserRequest) returns (UserResponse) {}
}

// 요청 메시지 정의
message UserRequest {
  int32 user_id = 1;
}

// 사용자 생성 요청 메시지
message CreateUserRequest {
  string name = 1;
  string email = 2;
}

// 응답 메시지 정의
message UserResponse {
  int32 user_id = 1;
  string name = 2;
  string email = 3;
  enum Status {
    ACTIVE = 0;
    INACTIVE = 1;
    BANNED = 2;
  }
  Status status = 4;
}

Protocol Buffers의 주요 특징:

RPC(Remote Procedure Call) 모델

gRPC는 클라이언트가 서버의 메서드를 마치 로컬 객체인 것처럼 호출할 수 있게 해주는 RPC 모델을 따른다. 서버와 클라이언트는 서로 다른 언어로 작성될 수 있으며, Protocol Buffers가 이들 간의 통신을 처리한다.

HTTP/2 기반 통신

gRPC는 HTTP/2 프로토콜을 기반으로 하며, 다음과 같은 이점을 제공한다:

네 가지 통신 패턴

gRPC는 다음 네 가지 통신 패턴을 지원한다:

  1. 단일 요청-응답 (Unary RPC): 클라이언트가 서버에 단일 요청을 보내고, 서버가 단일 응답을 반환한다. 전통적인 함수 호출과 유사하다.

    1
    
    rpc SayHello (HelloRequest) returns (HelloResponse);
    
  2. 서버 스트리밍 (Server Streaming RPC): 클라이언트가 단일 요청을 보내고, 서버가 일련의 메시지를 스트림으로 반환한다.

    1
    
    rpc SubscribeUpdates (SubscriptionRequest) returns (stream Update);
    
  3. 클라이언트 스트리밍 (Client Streaming RPC): 클라이언트가 일련의 메시지를 스트림으로 보내고, 서버가 단일 응답을 반환한다.

    1
    
    rpc ProcessBulkData (stream DataChunk) returns (ProcessSummary);
    
  4. 양방향 스트리밍 (Bidirectional Streaming RPC): 클라이언트와 서버가 모두 독립적으로 메시지 스트림을 주고받을 수 있다.

    1
    
    rpc Chat (stream ChatMessage) returns (stream ChatMessage);
    

이러한 다양한 통신 패턴은 gRPC를 다양한 사용 사례에 적용할 수 있게 해준다.

코드 생성

.proto 파일이 준비되면, Protocol Buffers 컴파일러(protoc)와 특정 언어용 플러그인을 사용하여 서버와 클라이언트 코드를 자동으로 생성한다. 이 생성된 코드에는 메시지 클래스, 직렬화/역직렬화 로직, 그리고 클라이언트 스텁과 서버 스켈레톤이 포함된다.
이는 개발 프로세스를 간소화하고 언어 간 일관성을 보장한다.

1
2
3
4
# protoc 명령어 예시 (Node.js용 코드 생성)
protoc --js_out=import_style=commonjs,binary:./generated \
       --grpc-web_out=import_style=commonjs,mode=grpcwebtext:./generated \
       ./protos/*.proto

예를 들어, Java의 경우:

1
protoc --java_out=./generated-sources --grpc-java_out=./generated-sources greeting.proto

이 명령은 Java 언어로 된 클라이언트와 서버 코드를 생성한다. 다른 언어(Go, Python, C++, JavaScript 등)에 대해서도 유사한 명령을 사용할 수 있다.

gRPC 구현에 필요한 핵심 지식

Protocol Buffers 문법

Protocol Buffers의 기본 문법을 이해해야 한다:

스트리밍 개념

gRPC의 스트리밍 기능을 효과적으로 활용하려면 다음을 이해해야 한다:

오류 처리

gRPC는 표준화된 상태 코드 시스템을 제공한다:

1
2
3
4
5
6
7
8
// Node.js에서의 오류 처리 예시
try {
  const response = await client.getUser({ user_id: 123 });
  console.log(response);
} catch (error) {
  console.error(`오류 발생: ${error.code} - ${error.message}`);
  // error.code는 gRPC 상태 코드를 포함합니다
}

메타데이터 및 컨텍스트

gRPC에서는 메타데이터를 통해 추가 정보를 전달할 수 있다:

1
2
3
4
// 클라이언트 측 메타데이터 예시 (Node.js)
const metadata = new grpc.Metadata();
metadata.set('authorization', 'Bearer ' + token);
client.getUser({ user_id: 123 }, metadata, callback);

인증 및 보안

gRPC는 다양한 인증 메커니즘을 지원한다:

SSL/TLS 암호화

gRPC는 SSL/TLS를 통한 통신 암호화를 지원한다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 서버 측 SSL/TLS 설정
const fs = require('fs');
const grpc = require('@grpc/grpc-js');

const server = new grpc.Server();
// 서비스 등록…

// SSL/TLS 자격 증명 생성
const serverCredentials = grpc.ServerCredentials.createSsl(
  fs.readFileSync('ca.pem'),  // 루트 인증서
  [{
    private_key: fs.readFileSync('server-key.pem'),
    cert_chain: fs.readFileSync('server-cert.pem')
  }],
  true  // 클라이언트 인증 요구 여부
);

server.bindAsync('0.0.0.0:50051', serverCredentials, (err, port) => {
  if (!err) server.start();
});
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 클라이언트 측 SSL/TLS 설정
const grpc = require('@grpc/grpc-js');
const fs = require('fs');

// SSL/TLS 자격 증명 생성
const channelCredentials = grpc.credentials.createSsl(
  fs.readFileSync('ca.pem'),  // 루트 인증서
  fs.readFileSync('client-key.pem'),  // 클라이언트 키
  fs.readFileSync('client-cert.pem')  // 클라이언트 인증서
);

// 보안 채널을 통한 클라이언트 생성
const client = new userProto.UserService(
  'localhost:50051',
  channelCredentials
);
토큰 기반 인증

OAuth, JWT 등과 같은 토큰 기반 인증을 메타데이터를 통해 구현할 수 있다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 클라이언트 측 토큰 전송
function getAuthenticatedUser(userId) {
  // 메타데이터 객체 생성
  const metadata = new grpc.Metadata();
  metadata.add('authorization', 'Bearer ' + authToken);
  
  return new Promise((resolve, reject) => {
    client.getUser({ user_id: userId }, metadata, (err, response) => {
      if (err) {
        reject(err);
        return;
      }
      resolve(response);
    });
  });
}
 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
// 서버 측 토큰 검증
function authInterceptor(options, nextCall) {
  return new grpc.InterceptingCall(nextCall(options), {
    start: function(metadata, listener, next) {
      const authHeader = metadata.get('authorization');
      
      if (!authHeader || !validateToken(authHeader[0])) {
        const error = {
          code: grpc.status.UNAUTHENTICATED,
          details: 'Invalid or missing authentication token'
        };
        
        const newListener = {
          onReceiveStatus: function(status, next) {
            next(error);
          }
        };
        
        next(metadata, newListener);
        return;
      }
      
      // 인증 성공, 계속 진행
      next(metadata, listener);
    }
  });
}

// 토큰 검증 로직
function validateToken(authHeader) {
  // "Bearer <token>" 형식에서 토큰 추출
  const token = authHeader.split(' ')[1];
  
  // 토큰 검증 로직 (JWT 등)
  // 실제 구현에서는 JWT 라이브러리 등을 사용하여 검증
  return token && token.length > 0;
}

// 인터셉터를 서버에 등록
const server = new grpc.Server({
  interceptors: [authInterceptor]
});
역할 기반 접근 제어 (RBAC)

특정 메서드에 대한 접근을 제한하기 위해 역할 기반 접근 제어를 구현할 수 있다:

 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
// 권한 확인 함수
function checkPermission(metadata, method) {
  // 토큰에서 사용자 정보 및 역할 추출
  const authHeader = metadata.get('authorization')[0];
  const token = authHeader.split(' ')[1];
  const userInfo = decodeToken(token); // JWT 등에서 사용자 정보 추출
  
  // 메서드별 필요 권한 매핑
  const methodPermissions = {
    'GetUser': ['user:read', 'admin'],
    'CreateUser': ['user:write', 'admin'],
    'DeleteUser': ['admin'] // 관리자만 가능
  };
  
  // 필요한 권한 확인
  const requiredRoles = methodPermissions[method] || [];
  const userRoles = userInfo.roles || [];
  
  // 사용자 역할이 필요 권한 중 하나라도 포함하는지 확인
  return requiredRoles.some(role => userRoles.includes(role));
}

// 인터셉터에서 권한 확인
function authorizationInterceptor(options, nextCall) {
  return new grpc.InterceptingCall(nextCall(options), {
    start: function(metadata, listener, next) {
      // 메서드 이름 추출
      const method = options.method_definition.path.split('/').pop();
      
      if (!checkPermission(metadata, method)) {
        const error = {
          code: grpc.status.PERMISSION_DENIED,
          details: `Permission denied for method: ${method}`
        };
        
        const newListener = {
          onReceiveStatus: function(status, next) {
            next(error);
          }
        };
        
        next(metadata, newListener);
        return;
      }
      
      // 권한 확인 성공, 계속 진행
      next(metadata, listener);
    }
  });
}
속도 제한 (Rate Limiting)

API 남용을 방지하기 위해 클라이언트별 요청 속도를 제한할 수 있다:

 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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// 간단한 메모리 기반 속도 제한 구현
class RateLimiter {
  constructor(limit, windowMs) {
    this.limit = limit; // 윈도우 기간 동안 허용되는 최대 요청 수
    this.windowMs = windowMs; // 밀리초 단위의 시간 윈도우
    this.clients = new Map(); // 클라이언트 IP 또는 ID별 요청 카운터
  }
  
  isAllowed(clientId) {
    const now = Date.now();
    
    if (!this.clients.has(clientId)) {
      // 새 클라이언트 등록
      this.clients.set(clientId, {
        count: 1,
        resetTime: now + this.windowMs
      });
      return true;
    }
    
    const client = this.clients.get(clientId);
    
    // 윈도우 기간이 지났으면 카운터 리셋
    if (now > client.resetTime) {
      client.count = 1;
      client.resetTime = now + this.windowMs;
      return true;
    }
    
    // 제한 내에 있는지 확인
    if (client.count < this.limit) {
      client.count++;
      return true;
    }
    
    // 제한 초과
    return false;
  }
}

// 속도 제한 인터셉터
const rateLimiter = new RateLimiter(100, 60 * 1000); // 분당 100 요청

function rateLimitInterceptor(options, nextCall) {
  return new grpc.InterceptingCall(nextCall(options), {
    start: function(metadata, listener, next) {
      // 클라이언트 ID 추출 (IP, 인증 토큰 등)
      const clientId = extractClientId(metadata);
      
      if (!rateLimiter.isAllowed(clientId)) {
        const error = {
          code: grpc.status.RESOURCE_EXHAUSTED,
          details: 'Rate limit exceeded. Try again later.'
        };
        
        const newListener = {
          onReceiveStatus: function(status, next) {
            next(error);
          }
        };
        
        next(metadata, newListener);
        return;
      }
      
      // 속도 제한 내에 있음, 계속 진행
      next(metadata, listener);
    }
  });
}

// 클라이언트 ID 추출 함수
function extractClientId(metadata) {
  // 인증 토큰에서 사용자 ID 추출
  const authHeader = metadata.get('authorization');
  if (authHeader && authHeader.length > 0) {
    const token = authHeader[0].split(' ')[1];
    const decoded = decodeToken(token);
    return decoded.sub || decoded.userId;
  }
  
  // IP 주소 사용 (실제로는 X-Forwarded-For 등 고려 필요)
  const clientIp = metadata.get('x-forwarded-for') || 'unknown';
  return clientIp;
}

gRPC API의 장점과 단점

장점

  1. 높은 성능:
    • HTTP/2 기반으로 요청 멀티플렉싱이 가능하다.
    • Protocol Buffers의 바이너리 직렬화는 JSON보다 더 작고 빠르게 처리된다.
    • 같은 작업에 대해 REST API보다 네트워크 대역폭을 적게 사용한다.
  2. 강력한 타입 안전성:
    • 서비스 계약이 .proto 파일로 명확하게 정의된다.
    • 컴파일 시점에 타입 오류를 발견할 수 있어 런타임 오류가 줄어든다.
    • API 변경 시 호환성 문제를 쉽게 감지할 수 있다.
  3. 다양한 언어 지원:
    • Java, Go, Python, Ruby, C++, JavaScript 등 다양한 언어에 대한 코드 생성을 지원한다.
    • 다른 언어로 작성된 서비스 간에도 원활한 통신이 가능하다.
  4. 양방향 스트리밍:
    • 실시간 통신, 대용량 데이터 전송 등 복잡한 통신 패턴을 쉽게 구현할 수 있다.
    • 클라이언트-서버 간 지속적인 연결을 통해 효율적인 데이터 교환이 가능하다.
  5. 코드 생성:
    • 자동 생성된 클라이언트와 서버 코드로 개발 속도가 향상됩니다.
    • 직렬화, 역직렬화, 네트워킹 로직을 매번 작성할 필요가 없습니다.
  6. 상호 운용성:
    • 다양한 환경과 프레임워크 간에 일관된 방식으로 통신할 수 있습니다.
    • 마이크로서비스 아키텍처에 특히 적합합니다.

단점

  1. 학습 곡선:
    • Protocol Buffers, RPC 개념, 스트리밍 패턴 등 새로운 개념 학습이 필요합니다.
    • REST와 같은 잘 알려진 패턴에 비해 초기 진입 장벽이 더 높을 수 있습니다.
  2. 브라우저 지원 제한:
    • 웹 브라우저는 기본적으로 gRPC를 직접 지원하지 않습니다.
    • 브라우저에서 사용하려면 gRPC-Web과 같은 추가적인 프록시나 변환 계층이 필요합니다.
  3. 디버깅의 어려움:
    • 바이너리 프로토콜이기 때문에 일반 HTTP 도구로 요청을 쉽게 검사하거나 테스트하기 어렵습니다.
    • 특수한 도구(예: grpcurl, BloomRPC 등)가 필요합니다.
  4. 성숙도:
    • REST에 비해 생태계와 도구가 덜 성숙했지만, 빠르게 개선되고 있습니다.
    • 일부 기능(예: 파일 업로드)에 대한 표준화된 접근 방식이 아직 발전 중입니다.
  5. 방화벽 및 프록시 문제:
    • 일부 네트워크 환경에서는 HTTP/2 트래픽이 방화벽이나 프록시에 의해 차단될 수 있습니다.
    • 특히 레거시 네트워크 인프라에서 문제가 발생할 수 있습니다.

gRPC API와 다른 API 스타일 비교

gRPC vs. REST

특성gRPCREST
프로토콜HTTP/2주로 HTTP/1.1
데이터 형식Protocol Buffers(바이너리)주로 JSON(텍스트)
계약강력한 타입의.proto 파일느슨한 계약(OpenAPI로 문서화 가능)
스트리밍네 가지 스트리밍 패턴 지원제한적(Server-Sent Events, WebSockets 필요)
코드 생성내장되어 있음별도 도구 필요(Swagger 등)
브라우저 지원제한적(gRPC-Web 필요)네이티브 지원
디버깅 용이성특수 도구 필요브라우저, curl로 쉽게 디버깅
성능높은 처리량, 낮은 지연 시간중간 수준의 성능
학습 곡선다소 가파름상대적으로 완만함

언제 gRPC를 선택해야 하나요?

언제 REST를 선택해야 하나요?

gRPC vs. GraphQL

특성gRPCGraphQL
설계 철학RPC(원격 프로시저 호출)쿼리 언어
주요 용도서비스 간 통신클라이언트에 맞춘 데이터 가져오기
데이터 형식Protocol BuffersJSON
서비스 정의.proto 파일GraphQL 스키마
오버페칭 문제존재할 수 있음해결됨(클라이언트가 필요한 필드 지정)
성능매우 높음중간~높음(구현에 따라 다름)
클라이언트 유연성고정된 엔드포인트클라이언트가 쿼리 구성
캐싱복잡함클라이언트 측 캐싱에 최적화됨

언제 gRPC를 선택해야 하나요?

언제 GraphQL을 선택해야 하나요?

gRPC vs. WebSocket

특성gRPCWebSocket
프로토콜HTTP/2 기반TCP 기반
구조구조화된 서비스/메서드원시 메시지 스트림
타입 안전성강력한 타입 체크프로토콜에 따라 다름(일반적으로 약함)
양방향 통신지원(스트리밍 RPC)완전 지원
브라우저 지원제한적(gRPC-Web)네이티브 지원
상태 관리상태 비저장(stateless)연결 상태 유지(stateful)
사용 사례서비스 간 통신, API채팅, 게임, 실시간 대시보드

언제 gRPC를 선택해야 하나요?

언제 WebSocket을 선택해야 하나요?

gRPC 구현을 위한 도구와 라이브러리

프로토콜 버퍼 컴파일러

1
2
3
# Protocol Buffer 컴파일러 설치 (Ubuntu)
$ apt-get install -y protobuf-compiler
$ protoc --version  # 버전 확인

주요 언어별 gRPC 라이브러리

개발 및 테스트 도구

gRPC API 모니터링 및 디버깅

gRPC API를 실제 환경에서 운영할 때는 모니터링과 디버깅이 중요하다.

로깅 및 추적

gRPC 호출을 로깅하고 추적하기 위한 인터셉터를 구현할 수 있다:

 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
// 로깅 인터셉터
function loggingInterceptor(options, nextCall) {
  return new grpc.InterceptingCall(nextCall(options), {
    start: function(metadata, listener, next) {
      const startTime = Date.now();
      const method = options.method_definition.path;
      console.log(`[${new Date().toISOString()}] gRPC 호출 시작: ${method}`);
      
      // 응답 리스너 래핑
      const newListener = {
        onReceiveMessage: function(message, next) {
          console.log(`[${new Date().toISOString()}] 메시지 수신: ${JSON.stringify(message)}`);
          next(message);
        },
        onReceiveStatus: function(status, next) {
          const duration = Date.now() - startTime;
          console.log(`[${new Date().toISOString()}] gRPC 호출 완료: ${method}, 상태: ${status.code}, 소요 시간: ${duration}ms`);
          next(status);
        }
      };
      
      next(metadata, newListener);
    },
    sendMessage: function(message, next) {
      console.log(`[${new Date().toISOString()}] 메시지 전송: ${JSON.stringify(message)}`);
      next(message);
    }
  });
}

// 인터셉터 등록
const server = new grpc.Server({
  interceptors: [loggingInterceptor]
});

OpenTelemetry 통합

OpenTelemetry는 분산 추적을 위한 개방형 표준으로, gRPC와 통합하여 성능 모니터링과 문제 해결을 지원한다:

 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
const opentelemetry = require('@opentelemetry/api');
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { registerInstrumentations } = require('@opentelemetry/instrumentation');
const { GrpcInstrumentation } = require('@opentelemetry/instrumentation-grpc');
const { ZipkinExporter } = require('@opentelemetry/exporter-zipkin');
const { SimpleSpanProcessor } = require('@opentelemetry/sdk-trace-base');

// 트레이서 제공자 설정
const provider = new NodeTracerProvider();

// Zipkin 익스포터 설정
const zipkinExporter = new ZipkinExporter({
  url: 'http://localhost:9411/api/v2/spans',
  serviceName: 'user-service'
});

// 스팬 프로세서 등록
provider.addSpanProcessor(new SimpleSpanProcessor(zipkinExporter));

// 제공자를 글로벌로 등록
provider.register();

// gRPC 계측 등록
registerInstrumentations({
  instrumentations: [
    new GrpcInstrumentation()
  ]
});

// 이제 gRPC 호출이 자동으로 추적됩니다

상태 모니터링 (Health Checking)

gRPC는 서비스 상태 확인을 위한 표준 헬스 체킹 프로토콜을 제공한다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// health.proto
syntax = "proto3";

package grpc.health.v1;

message HealthCheckRequest {
  string service = 1;
}

message HealthCheckResponse {
  enum ServingStatus {
    UNKNOWN = 0;
    SERVING = 1;
    NOT_SERVING = 2;
    SERVICE_UNKNOWN = 3;
  }
  ServingStatus status = 1;
}

service Health {
  rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
  rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
}

Node.js에서 구현 예시:

 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
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const path = require('path');

// 헬스 체크 proto 로드
const HEALTH_PROTO_PATH = path.join(__dirname, 'protos/health.proto');
const healthPackageDefinition = protoLoader.loadSync(HEALTH_PROTO_PATH);
const healthProto = grpc.loadPackageDefinition(healthPackageDefinition).grpc.health.v1;

// 서비스 상태 저장
const serviceStatus = {
  '': { status: 'SERVING' },  // 기본 상태 (전체 서버)
  'userservice.UserService': { status: 'SERVING' }  // 특정 서비스 상태
};

// 헬스 체크 서비스 구현
const healthImplementation = {
  check: (call, callback) => {
    const service = call.request.service;
    
    if (serviceStatus[service]) {
      callback(null, { 
        status: healthProto.HealthCheckResponse.ServingStatus[serviceStatus[service].status] 
      });
    } else {
      callback(null, { 
        status: healthProto.HealthCheckResponse.ServingStatus.SERVICE_UNKNOWN 
      });
    }
  },
  
  watch: (call) => {
    const service = call.request.service;
    
    if (!serviceStatus[service]) {
      call.write({ 
        status: healthProto.HealthCheckResponse.ServingStatus.SERVICE_UNKNOWN 
      });
      call.end();
      return;
    }
    
    // 초기 상태 전송
    call.write({ 
      status: healthProto.HealthCheckResponse.ServingStatus[serviceStatus[service].status] 
    });
    
    // 실제 구현에서는 서비스 상태 변경 이벤트를 구독하고
    // 상태가 변경될 때마다 새 상태를 전송할 수 있습니다
  }
};

// 메인 서버에 헬스 체크 서비스 추가
function addHealthService(server) {
  server.addService(healthProto.Health.service, healthImplementation);
}

디버깅 도구

gRPC API 디버깅을 위한 주요 도구들:

  1. grpcurl: curl과 유사한 커맨드라인 도구로, gRPC 서비스에 요청을 보낼 수 있다.

    1
    2
    3
    4
    5
    
    # 서비스 리스트 조회
    grpcurl -plaintext localhost:50051 list
    
    # 특정 서비스 메서드 호출
    grpcurl -plaintext -d '{"user_id": "1"}' localhost:50051 userservice.UserService/GetUser
    
  2. BloomRPC: gRPC용 GUI 클라이언트로, Postman과 유사한 인터페이스를 제공한다.

  3. gRPC Web UI: 웹 기반 gRPC 클라이언트로, 서비스 검색 및 테스트가 가능하다.

    1
    2
    3
    4
    5
    
    # 설치
    npm install -g grpc-web-ui
    
    # 실행
    grpc-web-ui
    

실제 사용 사례

gRPC는 다음과 같은 시나리오에서 특히 유용하다:

  1. 마이크로서비스: 내부 서비스 간의 효율적인 통신
  2. 모바일 애플리케이션: 저대역폭 환경에서의 효율적인 통신
  3. 다중 언어 시스템: 다양한 언어로 작성된 서비스 간의 통신
  4. 실시간 서비스: 채팅, 게임, 협업 도구와 같은 양방향 스트리밍이 필요한 서비스
  5. IoT 애플리케이션: 제한된 리소스 환경에서의 효율적인 통신

gRPC API 구현하기: 단계별 안내

gRPC API를 실제로 구현하는 방법을 단계별로 살펴보자.
간단한 예제로 Node.js를 사용하겠지만, 다른 언어에서도 유사한 절차를 따른다.

1단계: 프로젝트 설정

먼저 필요한 패키지를 설치한다:

1
2
3
4
5
6
7
# 프로젝트 디렉토리 생성 및 초기화
mkdir grpc-example
cd grpc-example
npm init -y

# 필요한 패키지 설치
npm install @grpc/grpc-js @grpc/proto-loader

2단계: 서비스 정의하기

.proto 파일을 생성하여 서비스를 정의한다. 간단한 사용자 서비스를 예로 들어보면:

 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
// 파일: protos/user_service.proto
syntax = "proto3";

package userservice;

// 사용자 서비스 정의
service UserService {
  // 사용자 정보 조회
  rpc GetUser (UserRequest) returns (UserResponse);
  
  // 사용자 목록 스트리밍
  rpc ListUsers (UsersRequest) returns (stream UserResponse);
  
  // 사용자 생성
  rpc CreateUser (CreateUserRequest) returns (UserResponse);
}

// 사용자 조회 요청
message UserRequest {
  string user_id = 1;
}

// 사용자 목록 요청
message UsersRequest {
  int32 limit = 1;  // 조회할 최대 사용자 수
}

// 사용자 생성 요청
message CreateUserRequest {
  string name = 1;
  string email = 2;
  int32 age = 3;
}

// 사용자 응답
message UserResponse {
  string user_id = 1;
  string name = 2;
  string email = 3;
  int32 age = 4;
  string created_at = 5;
}

3단계: 서버 구현하기

이제 이 서비스를 구현하는 gRPC 서버를 작성한다:

 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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// 파일: server.js
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const path = require('path');

// Proto 파일 로드
const PROTO_PATH = path.join(__dirname, 'protos/user_service.proto');
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
  keepCase: true,
  longs: String,
  enums: String,
  defaults: true,
  oneofs: true
});

const userProto = grpc.loadPackageDefinition(packageDefinition).userservice;

// 임시 데이터 저장소 (실제 애플리케이션에서는 데이터베이스를 사용할 것입니다)
const users = [
  {
    user_id: '1',
    name: '홍길동',
    email: 'hong@example.com',
    age: 30,
    created_at: new Date().toISOString()
  },
  {
    user_id: '2',
    name: '김철수',
    email: 'kim@example.com',
    age: 25,
    created_at: new Date().toISOString()
  }
];

// 서비스 메서드 구현
const getUser = (call, callback) => {
  const userId = call.request.user_id;
  const user = users.find(u => u.user_id === userId);
  
  if (user) {
    callback(null, user);
  } else {
    callback({
      code: grpc.status.NOT_FOUND,
      details: `User with ID ${userId} not found`
    });
  }
};

const listUsers = (call) => {
  const limit = call.request.limit || users.length;
  
  // 스트리밍 응답을 보냅니다
  users.slice(0, limit).forEach(user => {
    call.write(user);
  });
  
  // 스트림 종료
  call.end();
};

const createUser = (call, callback) => {
  const user = {
    user_id: (users.length + 1).toString(),
    name: call.request.name,
    email: call.request.email,
    age: call.request.age,
    created_at: new Date().toISOString()
  };
  
  users.push(user);
  callback(null, user);
};

// 서버 시작
function startServer() {
  const server = new grpc.Server();
  server.addService(userProto.UserService.service, {
    getUser,
    listUsers,
    createUser
  });
  
  server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), (err, port) => {
    if (err) {
      console.error('Failed to bind server:', err);
      return;
    }
    
    console.log(`Server running at http://0.0.0.0:${port}`);
    server.start();
  });
}

startServer();

4단계: 클라이언트 구현하기

gRPC 서비스를 호출하는 클라이언트를 구현한다:

  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
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
// 파일: client.js
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const path = require('path');

// Proto 파일 로드 (서버와 동일)
const PROTO_PATH = path.join(__dirname, 'protos/user_service.proto');
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
  keepCase: true,
  longs: String,
  enums: String,
  defaults: true,
  oneofs: true
});

const userProto = grpc.loadPackageDefinition(packageDefinition).userservice;

// 클라이언트 생성
const client = new userProto.UserService(
  'localhost:50051',
  grpc.credentials.createInsecure()
);

// 사용자 조회 메서드 호출
function getUser(userId) {
  return new Promise((resolve, reject) => {
    client.getUser({ user_id: userId }, (err, response) => {
      if (err) {
        reject(err);
        return;
      }
      resolve(response);
    });
  });
}

// 사용자 목록 스트리밍
function listUsers(limit) {
  return new Promise((resolve, reject) => {
    const users = [];
    const call = client.listUsers({ limit });
    
    call.on('data', (user) => {
      users.push(user);
      console.log('Received user:', user.name);
    });
    
    call.on('end', () => {
      console.log('Streaming completed');
      resolve(users);
    });
    
    call.on('error', (err) => {
      console.error('Streaming error:', err);
      reject(err);
    });
  });
}

// 사용자 생성 메서드 호출
function createUser(userData) {
  return new Promise((resolve, reject) => {
    client.createUser(userData, (err, response) => {
      if (err) {
        reject(err);
        return;
      }
      resolve(response);
    });
  });
}

// 메서드 호출 테스트
async function main() {
  try {
    // 사용자 생성 테스트
    console.log('Creating a new user...');
    const newUser = await createUser({
      name: '이영희',
      email: 'lee@example.com',
      age: 28
    });
    console.log('Created user:', newUser);
    
    // 사용자 조회 테스트
    console.log('\nFetching user with ID 1...');
    const user = await getUser('1');
    console.log('User details:', user);
    
    // 사용자 목록 조회 테스트
    console.log('\nFetching all users (streaming)...');
    const users = await listUsers(10);
    console.log(`Received ${users.length} users in total`);
    
  } catch (error) {
    console.error('Error:', error);
  }
}

main();

5단계: 실행하기

별도의 터미널에서 서버와 클라이언트를 실행한다:

1
2
3
4
5
# 터미널 1: 서버 시작
node server.js

# 터미널 2: 클라이언트 실행
node client.js

이것으로 기본적인 gRPC API 구현이 완료되었다. 이 예제는 단일 요청-응답(GetUser, CreateUser)과 서버 스트리밍(ListUsers)을 보여준다.


용어 정리

용어설명

참고 및 출처