지난 글에서는 채팅방에 관련된 API 를 구현하였다. 다음으로 Kafka 와의 통신을 구현할 계획이었으나, 현재 Kafka 를 외부에서 접근할 수 있게 API-Gateway 를 설정중에 있기 때문에 , 그 다음 스텝인 gRPC 를 먼저 공부하고 설정해보는 시간을 갖도록 한다. gRPC 는 알림서비스에서 사용할 예정이며 우리 프로젝트는 MSA 로 설계하였기 때문에 gRPC 의 통신 방법이 적합하다고 판단, 또한 gRPC 에서 자체 스트림 기능을 활용하기로 하여 채택하게 되었다.
1. RPC 란 ?
gRPC 를 알기 전에 RPC 에 관해서 먼저 알아야 한다. RPC(Remote Procedure Call) 의 약자이며 프로세스 간 통신을 위해 설계된 프로토콜 또는 개념으로, 네트워크 상의 다른 컴퓨터에서 실행되는 함수를 마치 로컬 함수처럼 호출할 수 있도록 해준다. RPC는 언어 독립적이기 때문에 서로 다른 프로그래밍 언어를 사용하는 서버와 클라이언트 간 사용이 가능하다. 따라서 개발자는 서버나 통신 과정에 대해 고려할 필요 없이 로컬 함수를 가져다 사용하는 것처럼 기능들에 집중할 수 있다. RPC 와 RESTful API 의 핵심 차이는 다음과 같다.
RPC는 함수 호출에 초점이 맞춰져 있으며 컨트롤러 계층 없이 서비스 메서드가 요청을 직접 처리한다. 이는 마이크로서비스 간의 고성능 통신에서 효율적이다.
RESTful API는 리소스 중심 설계로, 컨트롤러가 요청을 처리하고 데이터를 HTTP 메서드와 URI로 조작하는 방식이다. 이는 클라이언트와 서버간의 표준화된 통신에 적합하다.
2. gRPC란 ?
그렇다면 gRPC 란, Google 에서 개발한 고성능, 오픈소스 RPC 프레임워크이다. gRPC 는 네트워크를 통해 원격 함수를 호출할 수 있게 설계된 프레임워크로서 HTTP/2 프로토콜과 Protobuf 를 하용하여 더 효율적이고 빠른 통신을 제공한다. gRPC 의 주요 특징은 다음과 같다.
- HTTP/2 기반 : 비동기 스트리밍 및 다중화 지원으로 인해 높은 속도와 낮은 대기시간을 보장하여 연결을 효율적으로 관리.
- Protobuf 사용 : Protocol Buffers 를 통해 데이터 직렬화 및 역직렬화. JSON, XML 보다 더 작은 크기의 데이터로 빠르게 전송 가능.
- 다양한 언어 지원 : gRPC 는 다양한 언어 (Java, Python, Go, C++ 등) 을 지원하기 때문에 여러 플랫폼에서의 통합이 쉬움.
- 양방향 스트리밍 지원 : 클라이언트와 서버 간 데이터 스트리밍이 가능, 단순 요청 뿐만 아니라 실시간 데이터 전송에도 적합.
- MSA 에 적합 : MSA 에서 서비스 간의 고성능 통신을 위해 많이 사용됨.
3. gRPC 를 설정하는 이유
우리 프로젝트는 MSA 로 설계된 프로젝트이다. MSA 에서 각 서비스는 독립적으로 개발되고 배포된다. 예를들어 채팅서비스와 사용자 인증 서비스는 서로 다른 서버와 인스턴스에서 동작한다. 따라서 각 서비스는 독립적으로 gRPC 서버를 설정하여, 해당 서비스의 기능에 대해 클라이언트 요청을 처리할 수 있어야 한다.
4. 흐름도 (1차)
- A가 메시지 전송
클라이언트 A 가 메시지를 Kafka 로 전달 - Kafka 로 대화 내용 전달
Kafka 를 통해 메시지를 처리하고, ScyllaDB 에 데이터를 저장 - ScyllaDB 에 대화 내용 전달
메시지 내용이 ScyllaDB 에 저장되어 로그로 기록 - Socket.IO 로 연결 상태 확인
Socket.IO 를 통해 현재 방(Room) 에 연결된 사용자의 상태를 확인 - 오프라인 사용자 도출
Socket.IO 의 연결 상태를 기반으로 오프라인 사용자 목록을 생성 - gRPC 로 알림 요청
오프라인 사용자들에게 알림을 전송하기 위해 알림서비스로 gRPC 요청 - gRPC 에서 응답 및 알림 전송
알림서비스가 요청을 처리하고, 오프라인 사용자에게 알림 전달 - Socket.IO 로 연결된 사용자에게 메시지 전송
온라인 상태의 사용자에게는 Socket.IO 를 통해 메시지를 실시간으로 전달
5. 구현
gRPC 를 구현하기 위해선 3가지 파일이 필요하다.
- .proto 파일 : 서비스와 메시지를 정의하는 인터페이스로, 서버와 클라이언트 간의 계약을 명시
- 서버(Server) : 정의된 .proto 를 구현하여 gRPC 요청을 처리하는 쪽.
- 클라이언트(Client) : .proto 를 사용해 서버와 통신하는 쪽.
현재 채팅서비스에서는 Socket.IO 를 통해 오프라인 사용자를 판단한 후에 오프라인 사용자에게만 알림을 보내기 위해 알림서비스에 요청하는 쪽이기 때문에 Server 파일은 생성하지 않고 Client 파일만 구현하면 된다.
5.1 .proto파일
syntax = "proto3";
package notification;
// Notification Service 정의
service NotificationService {
// 오프라인 사용자에게 알림을 전송하는 메서드
rpc NotifyOfflineUsers (OfflineUserRequest) returns (google.protobuf.Empty);
}
// 요청 메시지 구조
message OfflineUserRequest {
repeated string userIds = 1; // 오프라인 사용자 ID 목록
string message = 2; // 전송할 알림 메시지
string roomId = 3; // 방 ID
}
// gRPC Empty 메시지
import "google/protobuf/empty.proto";
필자는 알림서비스에 gRPC 통신을 위한 .proto 파일을 구성하였다. 이제 gRPC 로 통신되는 파일은 다음과 같이 작성되면 된다. 통신규약, 네임스페이스, 메서드, 요청 데이터 구조 순으로 작성하였다.
5.2 Client.js
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
// Proto 파일 경로
const PROTO_PATH = '../grpc/chat.proto';
// Proto 파일 로드
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
// gRPC 패키지 생성
const notificationProto = grpc.loadPackageDefinition(packageDefinition).notification;
// 알림 서비스 클라이언트 생성
const client = new notificationProto.NotificationService(
'localhost:50051', // 알림 서비스 서버의 주소
grpc.credentials.createInsecure()
);
// NotifyOfflineUsers 호출 함수
function notifyOfflineUsers(userIds, message, roomId) {
const request = {
userIds,
message,
roomId,
};
client.NotifyOfflineUsers(request, (error) => {
if (error) {
console.error('Failed to notify offline users:', error.message);
} else {
console.log('Notification request sent successfully');
}
});
}
알림서비스 서버의 주소는 추후 변경될 예정이다. 환경변수로 빠져서 설정될 것 같다. 이것으로 설정은 마치고 알림서비스의 서버가 연결이 되면 다시 테스트해봐야 할 것 같다.
알림서비스와의 테스트 이전에 개인적으로 서버와 클라이언트를 동시에 열어 테스트 해보았다.
1. chat.proto
syntax = "proto3";
package chat;
service ChatService {
rpc SendNotification (SendNotificationRequest) returns (SendNotificationResponse);
rpc StreamNotifications(stream NotificationRequest) returns (stream NotificationResponse);
}
message SendNotificationRequest {
string user_id = 1; // 수신자 ID
string message = 2; // 알림 내용
}
message SendNotificationResponse {
string status = 1; // 처리 상태 (success, failure)
}
message NotificationRequest {
string user_id = 1;
string status = 2;
}
message NotificationResponse {
string user_id = 1;
string update = 2;
}
2. chatServer.js
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const path = require('path');
// .proto 파일 경로
const PROTO_PATH = path.join(__dirname, '../proto/chat.proto');
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const chatProto = grpc.loadPackageDefinition(packageDefinition).chat;
// 사용자 상태 관리
const connectedUsers = {};
// gRPC 서버 생성
const server = new grpc.Server();
server.addService(chatProto.ChatService.service, {
StreamNotifications: (call) => {
call.on('data', (request) => {
const { user_id, status } = request;
connectedUsers[user_id] = status;
console.log(`[Stream] User ${user_id} is now ${status}`);
});
call.on('end', () => {
call.end();
});
},
SendNotification: (call, callback) => {
const { user_id, message } = call.request;
console.log(`[Notification] Sending to ${user_id}: ${message}`);
callback(null, { status: 'success' });
}
});
// 서버 실행
const PORT = '50051';
server.bindAsync(`0.0.0.0:${PORT}`, grpc.ServerCredentials.createInsecure(), () => {
console.log(`gRPC Server is running on port ${PORT}`);
});
3. chatClient.js
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const path = require('path');
// .proto 파일 경로
const PROTO_PATH = path.join(__dirname, '../proto/chat.proto');
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const chatProto = grpc.loadPackageDefinition(packageDefinition).chat;
// gRPC 클라이언트 생성
const client = new chatProto.ChatService('localhost:50051', grpc.credentials.createInsecure());
// 알림 전송 테스트
function sendNotification() {
client.SendNotification({ user_id: 'A', message: 'Hello, A!' }, (err, response) => {
if (err) {
console.error('Error sending notification:', err);
} else {
console.log('Notification response:', response.status);
}
});
}
// 스트리밍 테스트
function streamNotifications() {
const call = client.StreamNotifications();
call.write({ user_id: 'A', status: 'connected' });
setTimeout(() => {
call.write({ user_id: 'A', status: 'disconnected' });
call.end();
}, 5000);
call.on('data', (response) => {
console.log('Stream response:', response);
});
call.on('end', () => {
console.log('Stream ended');
});
}
// 테스트 실행
sendNotification();
streamNotifications();
4. 테스트 성공
먼저 서버를 node 로 실행 후, 클라이언트를 실행시키면 작동하는 코드이다. 클라이언트에서 스트리밍 테스트 코드를 요청하게 되면 서버에서는 다음과 같은 응답을 보낸다.
'Project > ST00CK' 카테고리의 다른 글
ScyllaDB 연결 및 채팅방 API 구현 (1) | 2025.01.01 |
---|---|
ScyllaDB ERD 작성, API 명세서 작성 (1차) (0) | 2024.12.31 |
Github 연동 및 Swagger 설치 (2) | 2024.12.16 |
RKE2 Rancher 설치 실패 및 GCP 무료 크레딧 만료 ... 추후 계획 (2) | 2024.12.16 |
GCP VM에 SSH 로 접근하기 (1) | 2024.12.10 |