gRPC는 Google에서 개발한 고성능 원격 프로시저 호출(Remote Procedure Call, RPC) 프레임워크로, 마이크로서비스 아키텍처에서 빠르고 안정적인 통신을 가능하게 한다.

 

HTTP/2 기반의 비동기 통신과 Protocol Buffers를 사용해 성능과 확장성을 극대화하는 것이 특징이다.

 

이 글에서는 gRPC의 서비스 요청 방식에 대해 설명하고, 각각의 요청 방식에 대해 다룬다.

 

🔍  gRPC 서비스 요청 방식이란?


gRPC에서는 클라이언트-서버 아키텍처를 기반으로 요청(Request)과 응답(Response)을 주고받는다.

다른 RPC와 달리, gRPC는 HTTP/2의 스트리밍 기능을 활용하여 다양한 요청/응답 방식을 지원한다.

 

gRPC의 네 가지 서비스 요청 방식

  1. Unary RPC ➡️ 클라이언트(요청 1회) -> 서버 (응답 1회)
  2. Server Streaming RPC ➡️ 클라이언트(요청 1회) -> 서버(여러 응답 스트리밍)
  3. Client Streaming RPC ➡️ 클라이언트(여러 요청 스트리밍) -> 서버(응답 1회)
  4. Bidirectional Streaming RPC ➡️ 클라이언트 <-> 서버 -> 양방향 스트리밍 통신

 

1️⃣ Unary RPC (단일 요청-응답)


 

Unary RPC는 가장 기본적인 요청-응답 방식이다.

  • 클라이언트(요청): 1번 전송
  • 서버(응답): 1번 반환

✔ 마치 REST API POST 요청처럼 동작하며, 가장 일반적인 형태이다.

 

예시: 사용자 정보 요청

 

🔹 ProtoBuf 정의 (user_service.proto)

syntax = "proto3";

service UserService {
  rpc GetUser(UserRequest) returns (UserResponse);
}

message UserRequest {
  int32 user_id = 1;
}

message UserResponse {
  int32 user_id = 1;
  string name = 2;
  string email = 3;
}

 

🔹 서버 측 구현 (Java)

public class UserServiceImpl extends UserServiceGrpc.UserServiceImplBase {
    @Override
    public void getUser(UserRequest request, StreamObserver<UserResponse> responseObserver) {
        // 사용자 정보 조회
        UserResponse response = UserResponse.newBuilder()
            .setUserId(request.getUserId())
            .setName("Alice")
            .setEmail("alice@example.com")
            .build();

        // 응답 전송
        responseObserver.onNext(response);
        responseObserver.onCompleted();
    }
}

 

🔹 클라이언트 요청 (Java)

UserServiceGrpc.UserServiceBlockingStub stub = UserServiceGrpc.newBlockingStub(channel);
UserRequest request = UserRequest.newBuilder().setUserId(1).build();
UserResponse response = stub.getUser(request);

System.out.println("User Name: " + response.getName());

 

✔️ 결과: 단일 요청에 대해 단일 응답을 받아 출력한다.

 


2️⃣ Server Streaming RPC (서버 스트리밍 요청)


 

Server Streaming RPC는 클라이언트가 요청을 한 번 보내고, 서버에서 여러 번 데이터를 스트리밍 형태로 응답한다.

 

💭 주로 대용량 데이터 전송이나 실시간 로그 처리 등에 사용된다.

 

예시: 사용자 활동 로그 요청

 

🔹 ProtoBuf 정의

service ActivityService {
  rpc GetUserActivities(UserRequest) returns (stream ActivityResponse);
}

message ActivityResponse {
  string activity = 1;
  string timestamp = 2;
}

 

🔹 서버 측 구현 (Java)

public class ActivityServiceImpl extends ActivityServiceGrpc.ActivityServiceImplBase {
    @Override
    public void getUserActivities(UserRequest request, StreamObserver<ActivityResponse> responseObserver) {
        // 예시: 사용자 활동 스트리밍
        for (int i = 0; i < 5; i++) {
            ActivityResponse response = ActivityResponse.newBuilder()
                .setActivity("Login Event " + i)
                .setTimestamp(Instant.now().toString())
                .build();

            responseObserver.onNext(response);  // 스트리밍으로 응답 전송
        }
        responseObserver.onCompleted();  // 스트리밍 종료
    }
}

 

🔹 클라이언트 요청 (Java)

ActivityServiceGrpc.ActivityServiceBlockingStub stub = ActivityServiceGrpc.newBlockingStub(channel);
UserRequest request = UserRequest.newBuilder().setUserId(1).build();

stub.getUserActivities(request).forEachRemaining(activity -> {
    System.out.println("Activity: " + activity.getActivity() + ", Time: " + activity.getTimestamp());
});

 

결과: 여러 번 응답을 받으며 실시간으로 출력됩니다.

 


3️⃣ Client Streaming RPC (클라이언트 스트리밍 요청)


 

Client Streaming RPC는 클라이언트에서 여러 요청을 스트리밍으로 전송하고, 서버에서 요청 수신이 끝난 후 한 번의 응답을 반환한다.

 

💭 주로 데이터 업로드배치 작업에 유용하게 사용됩니다.

 

예시: 파일 업로드

 

🔹 ProtoBuf 정의

service FileService {
  rpc UploadFile(stream FileChunk) returns (UploadStatus);
}

message FileChunk {
  bytes data = 1;
}

message UploadStatus {
  string message = 1;
}

 

🔹 서버 측 구현 (Java)

public class FileServiceImpl extends FileServiceGrpc.FileServiceImplBase {
    @Override
    public StreamObserver<FileChunk> uploadFile(StreamObserver<UploadStatus> responseObserver) {
        return new StreamObserver<>() {
            @Override
            public void onNext(FileChunk chunk) {
                // 파일 데이터를 처리
                System.out.println("Received chunk of size: " + chunk.getData().size());
            }

            @Override
            public void onCompleted() {
                UploadStatus status = UploadStatus.newBuilder().setMessage("Upload Completed!").build();
                responseObserver.onNext(status);
                responseObserver.onCompleted();
            }

            @Override
            public void onError(Throwable t) {
                t.printStackTrace();
            }
        };
    }
}

 

🔹 클라이언트 요청 (Java)

StreamObserver<FileChunk> requestObserver = fileServiceStub.uploadFile(new StreamObserver<UploadStatus>() {
    @Override
    public void onNext(UploadStatus status) {
        System.out.println(status.getMessage());
    }

    @Override
    public void onError(Throwable t) {}

    @Override
    public void onCompleted() {}
});

// 파일 데이터를 스트리밍 전송
requestObserver.onNext(FileChunk.newBuilder().setData(ByteString.copyFrom(bytesChunk)).build());
requestObserver.onCompleted();

 

결과: 클라이언트에서 파일 데이터를 여러 번 보내고, 전송이 끝나면 응답을 받는다.

 


4️⃣ Bidirectional Streaming RPC (양방향 스트리밍 요청)


 

Bidirectional Streaming RPC는 클라이언트와 서버가 동시에 데이터를 스트리밍하는 방식이다.

  • 클라이언트 -> 요청 스트리밍 전송
  • 서버 -> 응답 스트리밍 반환

 

💭 실시간 채팅, 스트리밍 게임 데이터 처리 등에 사용된다.

 

예시: 실시간 채팅 서비스

 

🔹 ProtoBuf 정의

service ChatService {
  rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}

message ChatMessage {
  string user = 1;
  string message = 2;
}

 

🔹 서버 측 구현 (Java)

public class ChatServiceImpl extends ChatServiceGrpc.ChatServiceImplBase {
    @Override
    public StreamObserver<ChatMessage> chat(StreamObserver<ChatMessage> responseObserver) {
        return new StreamObserver<>() {
            @Override
            public void onNext(ChatMessage message) {
                // 클라이언트 메시지 수신
                System.out.println("Received message from " + message.getUser());
                responseObserver.onNext(ChatMessage.newBuilder()
                    .setUser("Server")
                    .setMessage("Echo: " + message.getMessage())
                    .build());
            }

            @Override
            public void onCompleted() {
                responseObserver.onCompleted();
            }

            @Override
            public void onError(Throwable t) {
                t.printStackTrace();
            }
        };
    }
}

 

🔹 클라이언트 요청 (Java)

StreamObserver<ChatMessage> requestObserver = chatStub.chat(new StreamObserver<ChatMessage>() {
    @Override
    public void onNext(ChatMessage message) {
        System.out.println(message.getUser() + ": " + message.getMessage());
    }

    @Override
    public void onError(Throwable t) {}

    @Override
    public void onCompleted() {}
});

// 메시지 전송
requestObserver.onNext(ChatMessage.newBuilder().setUser("Client").setMessage("Hello!").build());

 

결과: 클라이언트와 서버가 동시에 메시지를 주고받으며 실시간 소통 가능

 

 


🔍  gRPC와 Protobuf의 상호작용


Protocol Buffers(Protobuf)는 gRPC에서 사용하는 기본 데이터 직렬화 포맷이다.

gRPC의 빠른 처리 속도와 네트워크 효율성은 대부분 이 Protobuf와의 결합에서 나온다.

 

 

1️⃣ 주요 인터페이스 정의 언어 (IDL)

gRPC는 Protobuf를 통해 서비스와 메시지의 구조를 정의한다. 이때 사용되는 언어가 바로 IDL(Interface Definition Language)이다.

 

예시: Protobuf를 사용한 IDL 정의
syntax = "proto3";

service UserService {
  rpc GetUser(UserRequest) returns (UserResponse);
}

message UserRequest {
  int32 user_id = 1;
}

message UserResponse {
  int32 user_id = 1;
  string name = 2;
  string email = 3;
}
  • service ➡️ 서비스의 인터페이스 정의
  • rpc ➡️ 원격 호출 메서드 정의
  • message ➡️ 데이터 구조 정의

 

2️⃣ 효율적인 데이터 직렬화

Protobuf는 텍스트 기반 JSON에 비해 훨씬 작고 빠른 바이너리 포맷을 사용한다.

  • 직렬화(Serialization) ➡️ 데이터를 바이너리 형태로 변환해 전송
  • 역직렬화(Deserialization) ➡️ 수신한 데이터를 원래 객체로 복원

 

3️⃣ 네트워크 효율성 향상

  • 작은 메시지 크기 ➡️ 대역폭 절감
  • 빠른 직렬화/역직렬화 ➡️ 빠른 요청 및 응답 처리

 


🔗 gRPC와 HTTP/2: HTTP/2 사용의 이점 및 주의 사항


gRPC는 HTTP/2 프로토콜을 기반으로 동작한다.

HTTP/1.1의 한계를 극복하며 다음과 같은 기능적 이점을 제공한다.

 

✅ 스트리밍 (Streaming)

  • 양방향으로 데이터를 실시간 주고받을 수 있는 기능
  • 실시간 채팅, 비디오 스트리밍 서비스에 활용 가능

 

✅ 헤더 압축 (Header Compression)

  • HTTP/2는 HPACK 압축 알고리즘을 사용해 헤더 크기를 줄여 네트워크 오버헤드 감소
  • 중복된 헤더 데이터를 줄여 빠른 요청-응답 처리 가능

 

✅ 멀티플렉싱 (Multiplexing)

  • 하나의 연결여러 요청/응답이 동시에 처리 가능
  • REST API의 HTTP/1.1에서는 동시 요청 시 헤드 오브 라인(Head-of-Line) 블로킹 문제가 발생하지만, gRPC는 이 문제를 해결하여 빠른 병렬 처리 지원

 

🚨 gRPC 스크리밍 사용 시 주의 사항

gRPC 스트리밍은 클라이언트와 서버 간에 데이터를 실시간으로 주고받을 수 있는 강력한 기능을 제공한다.

그러나 스트리밍을 올바르게 사용하지 않으면 리소스 낭비, 오류 처리 문제, 성능 저하 등의 문제가 발생할 수 있다.

 

1️⃣ 리소스 관리에 유의

  • gRPC 스트리밍은 연결을 지속적으로 유지하기 때문에, 불필요한 연결이 많아지면 서버 및 클라이언트 리소스를 과도하게 사용할 수 있다.
  • 스트리밍을 사용할 때는 불필요한 연결을 즉시 닫고, 적절한 타임아웃을 설정해야 한다.
예제 (Java - 스트리밍 종료)
responseObserver.onCompleted(); // 스트리밍 종료 후 리소스 해제
  • 연결 유지 시간이 너무 길어지지 않도록 관리해야 한다.

 

2️⃣ 다양한 오류 처리 필요성

  • 스트리밍 환경에서는 네트워크 장애, 서버 다운, 클라이언트 중단 등 다양한 오류가 발생할 수 있다.
  • 재시도(retry) 로직과 적절한 오류 코드 처리가 필요하다.
  • 특히, gRPC의 상태 코드(Status Code)를 활용하여 오류 원인을 파악하는 것이 중요하다.
예제 (Java - 오류 처리)
@Override
public void onError(Throwable t) {
    Status status = Status.fromThrowable(t);
    log.info("Error occurred: {}", status);
}
  • 예상치 못한 네트워크 문제나 서버 다운을 고려한 예외 처리가 필요하다.

 

3️⃣ Backpressure 관리의 필요성

  • Backpressure생산자(서버)와 소비자(클라이언트) 간의 처리 속도 차이로 인해 발생하는 문제를 의미한다.
  • 서버가 너무 빠르게 데이터를 보내면, 클라이언트가 이를 제대로 처리하지 못할 수 있다.
  • 이를 방지하려면 클라이언트에서 데이터를 수신할 수 있는 속도를 서버가 고려하여 조절해야 한다.
예제 (Java - Flow Control 적용)
responseObserver.request(10); // 클라이언트가 처리할 수 있는 개수만큼 요청
  • 클라이언트가 감당할 수 있는 만큼만 데이터를 수신하도록 조절해야 한다.

 


gRPC 서비스 요청 방식: HTTP/2와의 상호작용 예시


gRPC 요청은 HTTP/2의 스트림(Stream)을 통해 이루어진다.

각 RPC 호출은 고유의 스트림 ID를 가지며, 여러 스트림이 하나의 TCP 연결을 공유한다.

 

예시: 단일 요청-응답 흐름 (Unary RPC)

  1. 클라이언트 -> 서버로 요청 (HTTP/2 프레임 전송)
  2. 서버 -> 클라이언트로 응답 (헤더 + 데이터 프레임 전송)
  3. 스트림 종료 (서버와 클라이언트 간 통신 종료)

 

gRPC 서비스 요청 방식: 실제 요청 및 응답 프로토콜 분석

 

gRPC에서 요청과 응답은 HTTP/2 프레임을 통해 전송된다.

 

1️⃣ gRPC HTTP/2 요청 헤더

:method: POST
:scheme: http 또는 https
:path: 호출할 서비스 경로 (ex: /UserService/GetUser)
content-type: application/grpc

 

2️⃣ gRPC HTTP/2 응답 헤더

content-type: application/grpc
grpc-status: 요청 처리 상태 (0 → 성공, 1 → 에러 발생)
grpc-message: 에러 메시지 (에러 발생 시)

 

3️⃣ gRPC 요청 메시지 (Protobuf 직렬화)

\x00\x00\x00\x00\x10\x08\x01\x12\x05Alice\x1A\x11alice@example.com
  • 첫 번째 바이트 -> 메시지 압축 여부 (0 = 비압축)
  • 다음 4바이트 -> 메시지 길이 정보

 

4️⃣ gRPC 응답 메시지 (Protobuf 직렬화)

  • 바이너리 형식으로 응답 데이터 전송
  • 클라이언트는 이 데이터를 역직렬화해 객체로 복원

 

gRPC 서비스 요청 방식: 양방향 스트리밍 예시

1️⃣ 스트리밍 시작 요청 및 응답

  • 클라이언트와 서버가 스트림을 설정하고 연결

 

2️⃣ 헤더 프레임 전송

  • 클라이언트와 서버가 각각 필요한 정보를 HTTP/2 헤더에 담아 전송

 

3️⃣ 데이터 프레임 전송 (스트리밍 중)

  • 클라이언트 -> 여러 데이터 전송
  • 서버 -> 실시간으로 응답

 

4️⃣ 추가 헤더 전송 옵션

  • 스트리밍 중 메타데이터나 새로운 설정 정보를 실시간으로 전달 가능

 

5️⃣ 스트리밍 종료 요청 및 응답

  • 클라이언트나 서버에서 스트림 종료 신호 전송

 

gRPC 스트리밍 모범 사례

  • 스트림 관련 전략 수립: 타임아웃, 하트비트 또는 기타 커넥션 관리 구현 필요
  • 메시지 크기 최적화: 메시지 정의를 최적화하여 필드를 효율적으로 사용
  • 스트림 처리 로직 분리: 스트림 처리를 위한 별도의 서비스 또는 모듈 구성
  • 동시성과 병렬 처리 활용: 양방향 스트리밍에서는 서버가 동시에 여러 클라이언트부터 스트림을 처리 해야함
  • 오류 및 상태 모니터링: 스트리밍 서비스의 상태와 성능을 지속적으로 모니터링하고, 오류 발생 시 알림을 받을 수 있는 로깅 및 모니터링 시스템 구축

 

🔥 gRPC의 Stream과 Repeated의 차이

특징StreamRepeated

특징 Stream Repeated
데이터 처리 방식 스트리밍 데이터를 실시간으로 전송 모든 데이터를 한 번에 전송
네트워크 효율성 네트워크 효율적, 필요한 순간에 전송 데이터 양이 많을수록 비효율적
메모리 사용량 작은 메모리 사용 전체 데이터를 한 번에 저장해야 함
언제 사용해야 할까? 데이터가 실시간으로 전송되거나, 양이 많을 때 데이터가 비교적 작고 일괄 전송이 가능할 때
예시 실시간 채팅, 로그 스트리밍 사용자 목록 전체 반환