최신 마이크로서비스가 REST보다 gRPC를 선호하는 이유

최신 마이크로서비스가 REST보다 gRPC를 선호하는 이유

모놀리식 아키텍처에서 구성 요소는 즉각적이고 안정성이 높은 인메모리 메서드 호출을 통해 통신합니다. 그러나 마이크로서비스 아키텍처로 전환하면 이러한 구성 요소가 네트워크 경계로 분리됩니다. 통신은 프로세스 외부 네트워크 호출(프로세스 간 통신, IPC)이 됩니다.

수년 동안 JSON 페이로드를 사용하는 HTTP/1.1을 통한 **REST(Representational State Transfer)**는 웹 API 구축을 위한 기본 표준이었습니다. REST는 공용 웹 서비스 및 클라이언트-서버 상호 작용에 탁월하지만, 빈도가 높고 대기 시간이 짧은 내부 서비스 간 통신에 사용될 경우 심각한 병목 현상이 발생합니다.

이것이 바로 최신 마이크로서비스가 **gRPC(Google Remote Procedure Call)**로 빠르게 전환하고 있는 이유입니다. 이 문서에서는 REST의 제한 사항을 분석하고, gRPC의 아키텍처 기반을 분석하고, Java에서 gRPC 서비스의 완전한 구현을 안내합니다.


1. 마이크로서비스에서 REST의 병목 현상

REST는 20년 넘게 웹을 강화해 왔습니다. 그러나 기본 기술은 내부 분산 아키텍처에 최적화되어 있지 않습니다.

A. 일반 텍스트 JSON의 오버헤드

JSON은 사람이 읽을 수 있으므로 디버깅이 쉽지만 기계 간 통신에는 매우 비효율적입니다.

  • 직렬화/역직렬화 비용: 텍스트 문자열 토큰을 구문 분석하고 이를 개체로 변환하는 데 상당한 CPU 주기가 소모됩니다.
  • 큰 페이로드 크기: JSON 키는 모든 단일 요청(예: {"transactionId": "123", "amount": 99.99})에서 반복됩니다. 초당 수백만 개의 요청을 처리하는 시스템의 경우 이 중복 메타데이터는 막대한 네트워크 대역폭을 낭비합니다.

B. HTTP/1.1 연결 제한

REST는 일반적으로 HTTP/1.1에서 실행되며, 이는 여러 가지 구조적 비효율성을 나타냅니다.

  • HoL(Head-of-Line) 차단: 단일 TCP 연결에서 클라이언트는 다음 요청을 보내기 전에 현재 요청의 응답을 기다려야 합니다.
  • 연결 풀링 고갈: 동시 요청을 처리하려면 클라이언트가 여러 TCP 연결을 열어야 합니다. 이로 인해 연결이 계속 열리고 닫힐 때 상당한 OS 리소스 오버헤드, 소켓 고갈 및 느린 시작 성능 저하가 발생합니다.

C. 약한 API 계약

REST API에는 내장된 컴파일 타임 계약이 없습니다. OpenAPI/Swagger는 API 문서화를 지원하지만 코드베이스와는 별개입니다. 백엔드 개발자가 JSON 필드 이름을 변경하고 컴파일 시간 경고 없이 실수로 다운스트림 서비스를 중단하는 것은 쉽습니다.


2. gRPC의 아키텍처 기반

2015년 Google에서 출시한 gRPC는 클라우드 기반 애플리케이션을 위해 특별히 설계된 오픈 소스 고성능 RPC 프레임워크입니다. 세 가지 핵심 기술을 사용하여 REST의 병목 현상을 해결합니다.

gRPC의 3가지 아키텍처 기반: 프로토콜 버퍼, HTTP/2 전송 및 코드 생성

A. 프로토콜 버퍼(Protobuf)

gRPC는 JSON 대신 프로토콜 버퍼를 IDL(인터페이스 정의 언어) 및 메시지 직렬화 형식으로 사용합니다.

  • Protobuf는 고도로 최적화된 바이너리 직렬화 메커니즘입니다.
  • 필드는 텍스트 이름이 아닌 작은 숫자 태그로 직렬화됩니다.
  • 페이로드는 JSON보다 최대 70~80% 더 작고 직렬화는 최대 10배 더 빠르며 CPU 및 대역폭 사용량이 크게 줄어듭니다.

B. HTTP/2 다중화

gRPC는 HTTP/2를 전송 프로토콜로 활용하여 여러 가지 성능 최적화를 제공합니다.

  • 진정한 멀티플렉싱: 단일 수명이 긴 TCP 연결을 통해 여러 요청과 응답을 동시에 인터리브할 수 있으므로 HOL(Head-of-Line) 차단이 완전히 제거됩니다.
  • 헤더 압축(HPACK): HTTP 헤더가 압축되어 요청 크기가 더욱 줄어듭니다.
  • 스트리밍 지원: HTTP/2는 기본적으로 단방향 클라이언트 스트리밍, 서버 스트리밍 및 전체 양방향 스트리밍을 지원합니다.

C. 코드 생성 및 엄격한 계약

.proto 파일에 API 구조를 정의함으로써 gRPC는 수십 가지 언어(Java, Go, Python, C++ 등)로 클라이언트 스텁과 서버 기본 클래스를 생성합니다. 계약은 컴파일 타임에 안전합니다. 서버가 인터페이스를 업데이트하는 경우 새 계약과 일치하지 않으면 클라이언트 빌드가 즉시 실패합니다.


3. Java 구현: gRPC 서비스 구축

실용적인 Java gRPC 애플리케이션을 빌드해 보겠습니다. 고객이 결제 요청을 제출하고 거래 확인을 받는 높은 처리량의 결제 처리 서비스를 구현합니다.

1단계: API 계약 정의(payment.proto)

프로토콜 버퍼 파일에서 서비스 인터페이스와 메시지 구조를 정의하는 것부터 시작합니다.

syntax = "proto3";

option java_multiple_files = true;
option java_package = "com.ghaznix.grpc.payment";
option java_outer_classname = "PaymentProto";

package payment;

// The Payment Service definition
service PaymentService {
  // Unary RPC: Sends a single payment request and receives a confirmation
  rpc ProcessPayment (PaymentRequest) returns (PaymentResponse);
}

// The Payment Request message
message PaymentRequest {
  string transaction_id = 1;
  string customer_id = 2;
  double amount = 3;
  string currency = 4;
}

// The Payment Response message
message PaymentResponse {
  string transaction_id = 1;
  string status = 2;
  string message = 3;
  string timestamp = 4;
}

2단계: 서버측 로직 구현

.proto 파일(일반적으로 Maven 또는 Gradle gRPC 플러그인에 의해 자동으로 처리됨)을 컴파일한 후 도구는 PaymentServiceImplBase 클래스를 생성합니다. 우리는 이 클래스를 확장하여 비즈니스 로직을 구현합니다.

package com.ghaznix.grpc.payment;

import io.grpc.stub.StreamObserver;
import java.time.Instant;

public class PaymentServiceImpl extends PaymentServiceGrpc.PaymentServiceImplBase {

    @Override
    public void processPayment(PaymentRequest request, StreamObserver<PaymentResponse> responseObserver) {
        System.out.println("Received payment request for customer: " + request.getCustomerId() 
                + " with amount: " + request.getAmount() + " " + request.getCurrency());

        // Process payment logic (simulation)
        String status = request.getAmount() > 0 ? "SUCCESS" : "DECLINED";
        String message = status.equals("SUCCESS") ? "Payment processed successfully." : "Invalid payment amount.";

        PaymentResponse response = PaymentResponse.newBuilder()
                .setTransactionId(request.getTransactionId())
                .setStatus(status)
                .setMessage(message)
                .setTimestamp(Instant.now().toString())
                .build();

        // Send the response to the client
        responseObserver.onNext(response);
        
        // Signal that the RPC execution is complete
        responseObserver.onCompleted();
    }
}

3단계: gRPC 서버 부트스트랩

다음으로, 특정 포트에서 gRPC 서버를 시작하고 서비스 구현을 등록하기 위한 서버 클래스를 만듭니다.

package com.ghaznix.grpc.payment;

import io.grpc.Server;
import io.grpc.ServerBuilder;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

public class PaymentServer {

    private Server server;

    public void start() throws IOException {
        int port = 50051;
        server = ServerBuilder.forPort(port)
                .addService(new PaymentServiceImpl())
                .build()
                .start();
        
        System.out.println("gRPC Server started, listening on port " + port);

        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.err.println("*** Shutting down gRPC server since JVM is shutting down");
            try {
                PaymentServer.this.stop();
            } catch (InterruptedException e) {
                e.printStackTrace(System.err);
            }
            System.err.println("*** Server shut down");
        }));
    }

    public void stop() throws InterruptedException {
        if (server != null) {
            server.shutdown().awaitTermination(30, TimeUnit.SECONDS);
        }
    }

    public void blockUntilShutdown() throws InterruptedException {
        if (server != null) {
            server.awaitTermination();
        }
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        final PaymentServer server = new PaymentServer();
        server.start();
        server.blockUntilShutdown();
    }
}

4단계: 클라이언트 구현(차단 및 비동기 스텁)

gRPC의 주요 이점은 클라이언트에 대해 두 가지 유형의 스텁을 생성한다는 것입니다.

  1. BlockingStub: 동기식 차단 실행(표준 REST 호출과 유사)
  2. 스텁: 콜백 관찰자를 사용하는 비동기식 비차단 실행입니다.
package com.ghaznix.grpc.payment;

import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import java.util.concurrent.TimeUnit;

public class PaymentClient {

    private final ManagedChannel channel;
    private final PaymentServiceGrpc.PaymentServiceBlockingStub blockingStub;

    public PaymentClient(String host, int port) {
        // Create a communication channel to the server
        this.channel = ManagedChannelBuilder.forAddress(host, port)
                .usePlaintext() // Disables TLS for local development
                .build();
        
        // Create the synchronous blocking stub
        this.blockingStub = PaymentServiceGrpc.newBlockingStub(channel);
    }

    public void shutdown() throws InterruptedException {
        channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
    }

    public void executePayment(String txId, String customerId, double amount) {
        PaymentRequest request = PaymentRequest.newBuilder()
                .setTransactionId(txId)
                .setCustomerId(customerId)
                .setAmount(amount)
                .setCurrency("USD")
                .build();

        System.out.println("Sending payment request for: " + txId);
        
        // Execute unary synchronous call
        PaymentResponse response = blockingStub.processPayment(request);
        
        System.out.println("Server Response: " + response.getStatus() + " | " + response.getMessage());
    }

    public static void main(String[] args) throws InterruptedException {
        PaymentClient client = new PaymentClient("localhost", 50051);
        try {
            client.executePayment("TX-99081", "customer-abc-123", 250.75);
        } finally {
            client.shutdown();
        }
    }
}

4. 아키텍처 비교: gRPC와 REST

아래 표에는 두 패러다임의 핵심 특성이 요약되어 있습니다.

건축적 특징 REST(표현 상태 전송) gRPC(Google 원격 프로시저 호출)
프로토콜 HTTP/1.1(표준), HTTP/2(선택 사항) HTTP/2(엄격한 요구 사항)
페이로드 형식 일반 텍스트 JSON, XML, HTML 바이너리 프로토콜 버퍼(Protobuf)
API 패러다임 리소스(URI 경로 + GET/POST/PUT/DELETE) 원격 프로시저(메소드/함수 호출)
계약 품질 느슨함(OpenAPI/Swagger는 선택 사항/외부) 엄격(컴파일 시 .proto 파일에 정의됨)
멀티플렉싱 아니요(HTTP/1.1의 Head-of-Line 차단) 예(단일 TCP 연결의 다중 스트림)
스트리밍 유형 단방향 서버 전송 이벤트(SSE)만 클라이언트, 서버 및 양방향 스트리밍
코드 생성 필요한 외부 도구(예: Swagger Codegen) Protobuf 컴파일러를 통해 내장됨(protoc)
브라우저 지원 범용(직접 웹 클라이언트 액세스) 제한됨(grpc-web 프록시 번역 필요)

5. REST 대신 gRPC를 선택해야 하는 경우는 언제인가요?

gRPC의 엄청난 성능 이점에도 불구하고 만능은 아닙니다. 프로토콜 선택은 시스템 아키텍처 내의 상황에 따라 달라집니다.

  • 내부 마이크로서비스 메시에 gRPC 사용: 대용량 서비스 간 통신의 경우 gRPC의 짧은 대기 시간, 바이너리 압축 및 스트리밍 기능이 탁월한 선택입니다. 내부 서버 CPU 오버헤드를 줄이고 전체 응답 시간을 단축합니다.
  • 에지/공용 API에 REST 사용: 웹 브라우저는 기본적으로 변환 레이어 없이 gRPC 호출을 수행할 수 없기 때문에 REST는 공개 API, 타사 웹후크와의 통합, 클라이언트 브라우저에서 아키텍처 엣지까지의 직접 통신을 위한 최선의 선택입니다.

API 게이트웨이 수준에서 REST를 노출하고 내부 마이크로서비스 통신을 위해 gRPC를 활용하는 두 가지를 결합함으로써 설계자는 상호 운용성이 뛰어나고 매우 빠른 시스템을 구축할 수 있습니다.


Ghaznix 블로그에서 더 많은 소프트웨어 개발 및 백엔드 엔지니어링 통찰력을 살펴보세요 →