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의 주요 특징:
- 바이너리 직렬화 형식으로 JSON보다 작고 빠름
- 강력한 타입 시스템 제공
- 버전 호환성과 확장성 지원
- 다양한 프로그래밍 언어에 대한 코드 생성
RPC(Remote Procedure Call) 모델#
gRPC는 클라이언트가 서버의 메서드를 마치 로컬 객체인 것처럼 호출할 수 있게 해주는 RPC 모델을 따른다. 서버와 클라이언트는 서로 다른 언어로 작성될 수 있으며, Protocol Buffers가 이들 간의 통신을 처리한다.
HTTP/2 기반 통신#
gRPC는 HTTP/2 프로토콜을 기반으로 하며, 다음과 같은 이점을 제공한다:
- 하나의 TCP 연결을 통한 다중 동시 요청 (멀티플렉싱)
- 헤더 압축
- 서버 푸시 지원
- 양방향 스트리밍
- 낮은 지연 시간
네 가지 통신 패턴#
gRPC는 다음 네 가지 통신 패턴을 지원한다:
단일 요청-응답 (Unary RPC): 클라이언트가 서버에 단일 요청을 보내고, 서버가 단일 응답을 반환한다. 전통적인 함수 호출과 유사하다.
1
| rpc SayHello (HelloRequest) returns (HelloResponse);
|
서버 스트리밍 (Server Streaming RPC): 클라이언트가 단일 요청을 보내고, 서버가 일련의 메시지를 스트림으로 반환한다.
1
| rpc SubscribeUpdates (SubscriptionRequest) returns (stream Update);
|
클라이언트 스트리밍 (Client Streaming RPC): 클라이언트가 일련의 메시지를 스트림으로 보내고, 서버가 단일 응답을 반환한다.
1
| rpc ProcessBulkData (stream DataChunk) returns (ProcessSummary);
|
양방향 스트리밍 (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의 기본 문법을 이해해야 한다:
- 메시지 정의
- 필드 타입과 번호
- 열거형
- 서비스 정의
- 네임스페이스(패키지)
- 가져오기(imports)
- 옵션
스트리밍 개념#
gRPC의 스트리밍 기능을 효과적으로 활용하려면 다음을 이해해야 한다:
- 스트림 열기 및 닫기
- 백프레셔(backpressure) 처리
- 오류 처리
- 취소 시그널
오류 처리#
gRPC는 표준화된 상태 코드 시스템을 제공한다:
- gRPC 상태 코드 (OK, CANCELLED, UNKNOWN 등)
- 오류 메시지
- 오류 메타데이터
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의 장점과 단점#
- 높은 성능:
- HTTP/2 기반으로 요청 멀티플렉싱이 가능하다.
- Protocol Buffers의 바이너리 직렬화는 JSON보다 더 작고 빠르게 처리된다.
- 같은 작업에 대해 REST API보다 네트워크 대역폭을 적게 사용한다.
- 강력한 타입 안전성:
- 서비스 계약이
.proto
파일로 명확하게 정의된다. - 컴파일 시점에 타입 오류를 발견할 수 있어 런타임 오류가 줄어든다.
- API 변경 시 호환성 문제를 쉽게 감지할 수 있다.
- 다양한 언어 지원:
- Java, Go, Python, Ruby, C++, JavaScript 등 다양한 언어에 대한 코드 생성을 지원한다.
- 다른 언어로 작성된 서비스 간에도 원활한 통신이 가능하다.
- 양방향 스트리밍:
- 실시간 통신, 대용량 데이터 전송 등 복잡한 통신 패턴을 쉽게 구현할 수 있다.
- 클라이언트-서버 간 지속적인 연결을 통해 효율적인 데이터 교환이 가능하다.
- 코드 생성:
- 자동 생성된 클라이언트와 서버 코드로 개발 속도가 향상됩니다.
- 직렬화, 역직렬화, 네트워킹 로직을 매번 작성할 필요가 없습니다.
- 상호 운용성:
- 다양한 환경과 프레임워크 간에 일관된 방식으로 통신할 수 있습니다.
- 마이크로서비스 아키텍처에 특히 적합합니다.
- 학습 곡선:
- Protocol Buffers, RPC 개념, 스트리밍 패턴 등 새로운 개념 학습이 필요합니다.
- REST와 같은 잘 알려진 패턴에 비해 초기 진입 장벽이 더 높을 수 있습니다.
- 브라우저 지원 제한:
- 웹 브라우저는 기본적으로 gRPC를 직접 지원하지 않습니다.
- 브라우저에서 사용하려면 gRPC-Web과 같은 추가적인 프록시나 변환 계층이 필요합니다.
- 디버깅의 어려움:
- 바이너리 프로토콜이기 때문에 일반 HTTP 도구로 요청을 쉽게 검사하거나 테스트하기 어렵습니다.
- 특수한 도구(예: grpcurl, BloomRPC 등)가 필요합니다.
- 성숙도:
- REST에 비해 생태계와 도구가 덜 성숙했지만, 빠르게 개선되고 있습니다.
- 일부 기능(예: 파일 업로드)에 대한 표준화된 접근 방식이 아직 발전 중입니다.
- 방화벽 및 프록시 문제:
- 일부 네트워크 환경에서는 HTTP/2 트래픽이 방화벽이나 프록시에 의해 차단될 수 있습니다.
- 특히 레거시 네트워크 인프라에서 문제가 발생할 수 있습니다.
gRPC API와 다른 API 스타일 비교#
gRPC vs. REST#
특성 | gRPC | REST |
---|
프로토콜 | HTTP/2 | 주로 HTTP/1.1 |
데이터 형식 | Protocol Buffers(바이너리) | 주로 JSON(텍스트) |
계약 | 강력한 타입의.proto 파일 | 느슨한 계약(OpenAPI로 문서화 가능) |
스트리밍 | 네 가지 스트리밍 패턴 지원 | 제한적(Server-Sent Events, WebSockets 필요) |
코드 생성 | 내장되어 있음 | 별도 도구 필요(Swagger 등) |
브라우저 지원 | 제한적(gRPC-Web 필요) | 네이티브 지원 |
디버깅 용이성 | 특수 도구 필요 | 브라우저, curl로 쉽게 디버깅 |
성능 | 높은 처리량, 낮은 지연 시간 | 중간 수준의 성능 |
학습 곡선 | 다소 가파름 | 상대적으로 완만함 |
언제 gRPC를 선택해야 하나요?
- 마이크로서비스 간 내부 통신
- 높은 성능이 필요한 경우
- 다양한 프로그래밍 언어 지원이 필요한 경우
- 양방향 스트리밍이 필요한 경우
언제 REST를 선택해야 하나요?
- 공개 API 제공
- 브라우저에서 직접 호출해야 하는 경우
- 단순한 리소스 중심 API
- 더 넓은 생태계와 도구에 의존해야 하는 경우
gRPC vs. GraphQL#
특성 | gRPC | GraphQL |
---|
설계 철학 | RPC(원격 프로시저 호출) | 쿼리 언어 |
주요 용도 | 서비스 간 통신 | 클라이언트에 맞춘 데이터 가져오기 |
데이터 형식 | Protocol Buffers | JSON |
서비스 정의 | .proto 파일 | GraphQL 스키마 |
오버페칭 문제 | 존재할 수 있음 | 해결됨(클라이언트가 필요한 필드 지정) |
성능 | 매우 높음 | 중간~높음(구현에 따라 다름) |
클라이언트 유연성 | 고정된 엔드포인트 | 클라이언트가 쿼리 구성 |
캐싱 | 복잡함 | 클라이언트 측 캐싱에 최적화됨 |
언제 gRPC를 선택해야 하나요?
- 서버 간 통신
- 미리 정의된 서비스 인터페이스
- 스트리밍 데이터 처리
- 최대 성능이 필요한 경우
언제 GraphQL을 선택해야 하나요?
- 다양한 클라이언트 요구사항 지원
- 모바일 애플리케이션 백엔드
- 오버페칭/언더페칭 방지가 중요한 경우
- 클라이언트가 필요한 데이터를 정확히 지정해야 하는 경우
gRPC vs. WebSocket#
특성 | gRPC | WebSocket |
---|
프로토콜 | 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 라이브러리#
- Node.js:
@grpc/grpc-js
및 @grpc/proto-loader
- Python:
grpcio
및 grpcio-tools
- Java:
io.grpc:grpc-all
- Go:
google.golang.org/grpc
- C#:
Grpc.Core
및 Grpc.Tools
개발 및 테스트 도구#
- Postman: gRPC 요청 테스트 지원
- BloomRPC: gRPC 전용 GUI 클라이언트
- gRPCurl: gRPC 서비스를 위한 curl과 유사한 명령줄 도구
- gRPC Web UI: 웹 기반 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 디버깅을 위한 주요 도구들:
grpcurl: curl과 유사한 커맨드라인 도구로, gRPC 서비스에 요청을 보낼 수 있다.
1
2
3
4
5
| # 서비스 리스트 조회
grpcurl -plaintext localhost:50051 list
# 특정 서비스 메서드 호출
grpcurl -plaintext -d '{"user_id": "1"}' localhost:50051 userservice.UserService/GetUser
|
BloomRPC: gRPC용 GUI 클라이언트로, Postman과 유사한 인터페이스를 제공한다.
gRPC Web UI: 웹 기반 gRPC 클라이언트로, 서비스 검색 및 테스트가 가능하다.
1
2
3
4
5
| # 설치
npm install -g grpc-web-ui
# 실행
grpc-web-ui
|
실제 사용 사례#
gRPC는 다음과 같은 시나리오에서 특히 유용하다:
- 마이크로서비스: 내부 서비스 간의 효율적인 통신
- 모바일 애플리케이션: 저대역폭 환경에서의 효율적인 통신
- 다중 언어 시스템: 다양한 언어로 작성된 서비스 간의 통신
- 실시간 서비스: 채팅, 게임, 협업 도구와 같은 양방향 스트리밍이 필요한 서비스
- 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)을 보여준다.
용어 정리#
참고 및 출처#
gRPC API vs. gRPC gRPC와 gRPC API는 현대 마이크로서비스 아키텍처에서 중요한 역할을 하는 기술이다.
gRPC 기본 개념 gRPC는 Google에서 개발한 고성능, 오픈소스 RPC(Remote Procedure Call) 프레임워크이다. 2015년에 처음 공개되었으며, HTTP/2 프로토콜 위에 구축되어 있다. ‘g’는 원래 Google을 의미했지만, 현재는 독립적인 오픈소스 프로젝트로 발전했다.
gRPC는 다음과 같은 주요 특징을 가지고 있다:
Protocol Buffers(protobuf)를 IDL(Interface Definition Language)로 사용 HTTP/2 기반 통신으로 높은 성능 제공 양방향 스트리밍 지원 다양한 프로그래밍 언어 지원 (C++, Java, Python, Go, Ruby, C# 등) 코드 생성 도구를 통한 클라이언트 및 서버 코드 자동 생성 gRPC API의 정의와 특징 gRPC API는 gRPC 프레임워크를 사용하여 구현된 API를 의미한다. 즉, gRPC는 기술적 프레임워크이고, gRPC API는 이 프레임워크를 사용하여 구축된 실제 응용 프로그램 인터페이스이다.
...
Error Handling gRPC는 분산 시스템에서 서비스 간 통신을 위한 강력한 프레임워크이다. 복잡한 분산 환경에서는 네트워크 문제, 서버 장애, 비즈니스 로직 오류 등 다양한 오류 상황이 발생할 수 있다. gRPC는 이러한 다양한 오류 상황을 일관되고 체계적으로 처리하기 위한 포괄적인 오류 처리 메커니즘을 제공한다.
gRPC의 오류 처리는 HTTP/2 상태 코드, gRPC 상태 코드, 그리고 상세 오류 메시지를 조합하여 클라이언트에게 오류 정보를 전달한다. 이 시스템은 모든 지원 언어에서 일관되게 작동하며, 개발자가 다양한 오류 상황에 적절히 대응할 수 있도록 돕는다.
...
Protocol Buffers (protobuf) Protocol Buffers(이하 protobuf)는 Google에서 개발한 언어 중립적, 플랫폼 중립적, 확장 가능한 구조화된 데이터 직렬화 메커니즘이다. 2008년에 오픈소스로 공개되었으며, 현재는 많은 분산 시스템과 마이크로서비스 아키텍처에서 핵심적인 역할을 담당하고 있다.
protobuf는 XML이나 JSON과 같은 텍스트 기반 직렬화 형식의 대안으로 설계되었으며, 더 작고, 빠르며, 단순한 방식으로 구조화된 데이터를 인코딩하는 것을 목표로 한다. 특히 gRPC 프레임워크의 IDL(Interface Definition Language)로 채택되어 서비스 인터페이스를 정의하는 데 널리 사용된다.
Protocol Buffers는 효율적인 직렬화, 언어 중립성, 스키마 진화 지원 등의 장점으로 인해 분산 시스템과 마이크로서비스 아키텍처에서 널리 사용되고 있다. 특히 gRPC와의 통합을 통해 서비스 간 통신의 표준으로 자리 잡았으며, 높은 성능이 요구되는 환경에서 JSON이나 XML의 강력한 대안으로 작용한다.
...