gRPC로 마이크로서비스 간 통신 전환 후기

배경

사내 마이크로서비스 아키텍처에서 서비스 간 통신을 REST API로 처리하고 있었다. 서비스가 늘어나면서 API 스펙 불일치, JSON 파싱 오버헤드, 타입 안정성 문제가 반복적으로 발생했다. gRPC 도입을 검토하게 된 계기였다.

Protocol Buffers 스키마 정의

먼저 .proto 파일로 서비스 인터페이스를 정의했다.

syntax = "proto3";

package user;

service UserService {
  rpc GetUser (GetUserRequest) returns (User);
  rpc ListUsers (ListUsersRequest) returns (ListUsersResponse);
}

message GetUserRequest {
  int64 user_id = 1;
}

message User {
  int64 id = 1;
  string email = 2;
  string name = 3;
}

스키마를 코드로 컴파일하면 타입 안전한 클라이언트/서버 코드가 자동 생성된다. TypeScript와 Go 양쪽에서 동일한 스키마를 공유할 수 있어 타입 불일치 문제가 사라졌다.

Node.js 서버 구현

grpc 패키지로 서버를 구현했다.

const grpc = require('grpc');
const protoLoader = require('@grpc/proto-loader');

const packageDefinition = protoLoader.loadSync('user.proto');
const userProto = grpc.loadPackageDefinition(packageDefinition).user;

function getUser(call, callback) {
  const userId = call.request.user_id;
  // DB 조회 로직
  callback(null, {
    id: userId,
    email: '[email protected]',
    name: 'John Doe'
  });
}

const server = new grpc.Server();
server.addService(userProto.UserService.service, { getUser });
server.bind('0.0.0.0:50051', grpc.ServerCredentials.createInsecure());
server.start();

성능 개선

REST API 대비 평균 응답 시간이 30% 감소했다. HTTP/2 기반의 멀티플렉싱과 바이너리 프로토콜의 효율성 덕분이었다. 특히 빈번한 내부 호출이 있는 서비스에서 효과가 컸다.

한계점

브라우저에서 직접 gRPC를 호출할 수 없어 클라이언트 대응 API는 여전히 REST로 유지했다. gRPC-Web도 있지만 프록시 설정이 필요해 도입하지 않았다. 내부 서비스 간 통신에만 적용하는 것으로 범위를 제한했다.

결론

마이크로서비스 간 통신에는 gRPC가 REST보다 적합했다. 타입 안정성과 성능 모두 개선되었고, 특히 다중 언어 환경에서 스키마 공유가 편리했다.