트래픽이 많은 내부 트래픽에 gRPC 를 적용하면 Rest API 에 비해 얼마나 빠를까?
K6가 10명 / 100명의 가상 사용자를 생성하여 60초 동안 지속적으로 서버에 요청을 보내도록 한다.
항목 | gRPC 결과 | HTTP 결과 |
---|---|---|
수신된 데이터 | 141 kB | 134 kB |
전송된 데이터 | 239 kB | 147 kB |
평균 응답시간 | 3.19ms | 6.31ms |
최소 응답시간 | 455.45µs | 328µs |
중앙값 응답시간 | 1.95ms | 4.95ms |
최대 응답시간 | 56.57ms | 84.35ms |
p(90) 응답시간 | 3.75ms | 8.13m |
p(95) 응답시간 | 5.6ms | 12.28ms |
항목 | gRPC 결과 | HTTP 결과 |
---|---|---|
수신된 데이터 | 2.8 MB | 2.7 MB |
전송된 데이터 | 4.7 MB | 2.9 MB |
평균 응답시간 | 3.25ms | 6.61ms |
최소 응답시간 | 165.45µs | 175µs |
중앙값 응답시간 | 2.36ms | 2.26ms |
최대 응답시간 | 23.96ms | 143.18ms |
p(90) 응답시간 | 5.86ms | 8.68ms |
p(95) 응답시간 | 8.64ms | 16.22ms |
gRPC는 HTTP/2를 사용하고, REST API는 주로 HTTP/1.1을 사용한다. HTTP/2는 멀티플렉싱, 헤더 압축, 서버 푸시 등의 기능을 지원해서 더 빠르게 데이터를 주고받을 수 있다.
HTTP/1.1에서는 동시에 여러 요청을 보낼 수 있지만, 이 요청들은 하나씩 처리된다. 이를 헤드 오브 라인 블로킹 문제라고 한다.
HTTP/1.1에서는 지속 연결(Persistent Connection)을 사용할 수 있지만, 기본적으로 요청/응답마다 새로운 TCP 연결을 설정하는 방식이다. 이로 인해 연결 설정과 해제 과정에서 시간이 소요된다.
멀티플렉싱
HTTP/2는 멀티플렉싱을 통해 하나의 TCP 연결에서 여러 요청과 응답을 동시에 주고받을 수 있다. 이는 헤드 오브 라인 블로킹 문제를 해결해준다.
연결 설정 및 유지
HTTP/2는 하나의 연결을 지속적으로 사용하며, 이 연결에서 여러 스트림으로 데이터를 주고받을 수 있다.
헤더 압축
HTTP/2는 HPACK이라는 헤더 압축 방식을 사용한다. 이는 헤더를 효율적으로 압축하고, 중복된 헤더 정보를 줄여 데이터 전송량을 줄여준다.
gRPC는 Protobuf를 사용해서 데이터를 직렬화한다. Protobuf는 바이너리 형식이라 데이터 크기가 작고, 처리 속도가 빠르다. 반면에 REST API는 JSON을 사용한다. JSON은 사람이 읽기 쉽지만 텍스트 형식이라 데이터 크기가 크고, 파싱 속도가 느리다.
gRPC는 상태 유지 방식이고, 양방향 스트리밍을 지원한다. 그래서 클라이언트와 서버가 지속적으로 연결된 상태에서 데이터를 주고받을 수 있다. 반면에 REST API는 상태 비유지 방식이라, 각 요청마다 새로운 연결을 설정하고 해제해야 하는데, 이 과정에서 오버헤드가 발생한다.
grpc-spring-boot-starter
의 최신버전은 3.1.0.RELEASE이고, spring-boot 3.2.4와 spring-cloud 2023.0.0 과 호환이 된다.
fyi; https://github.com/grpc-ecosystem/grpc-spring?tab=readme-ov-file
다른 버전과의 호환성이 궁금하다면 해당 문서를 참고하자.
project(':grpc-interface') {
apply plugin: 'java'
apply plugin: 'com.google.protobuf'
dependencies {
implementation "io.grpc:grpc-netty-shaded:$grpcVersion"
implementation "io.grpc:grpc-protobuf:$grpcVersion"
implementation "io.grpc:grpc-stub:$grpcVersion"
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
compileOnly 'jakarta.annotation:jakarta.annotation-api:1.3.5'
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:${protobufVersion}"
}
plugins {
grpc {
artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}"
}
}
generatedFilesBaseDir = "$projectDir/src/generated"
generateProtoTasks {
all().each { task ->
task.plugins {
grpc {}
php {}
}
}
}
}
tasks.clean {
delete "$projectDir/src/generated"
}
}
project(':grpc-client') {
apply plugin: 'org.springframework.boot'
dependencies {
implementation project(':grpc-interface')
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'net.devh:grpc-client-spring-boot-starter:2.15.0.RELEASE'
implementation 'org.springframework.boot:spring-boot-starter'
}
bootJar.enabled = true
jar.enabled = false
}
project(':grpc-server') {
apply plugin: 'org.springframework.boot'
dependencies {
implementation project(':grpc-interface')
implementation 'net.devh:grpc-server-spring-boot-starter:2.15.0.RELEASE'
implementation 'org.springframework.boot:spring-boot-starter'
}
bootJar.enabled = true
jar.enabled = false
}
프로토콜 버퍼(Protocol Buffers)라는 직렬화 포맷을 정의한 스키마 파일로, .proto 파일이 컴파일되어 gRPC 및 프로토콜 버퍼 관련 코드를 생성하는 과정을 살펴보자.
먼저, 서비스와 메시지 구조를 정의한 .proto 파일을 작성한다.
syntax = "proto3";
option java_package = "com.example.grpc";
option java_outer_classname = "ExampleProto";
message Request {
string name = 1;
}
message Response {
string message = 1;
}
service ExampleService {
rpc SayHello (Request) returns (Response);
}
프로젝트의 build.gradle 파일에서 protobuf 플러그인을 설정한다. 이 설정은 프로토콜 버퍼 컴파일러(protoc)와 gRPC 플러그인을 사용해 Java 코드를 생성한다.
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:${protobufVersion}"
}
plugins {
grpc {
artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}"
}
}
generateProtoTasks {
all().each { task ->
task.builtins {
java {}
}
task.plugins {
grpc {}
}
}
}
}
generateProto 작업을 실행하면, .proto 파일을 찾아서 protoc 컴파일러를 사용해 Java 소스 파일을 생성하고 gRPC 서비스 인터페이스 및 메시지 클래스가 포함된 Java 파일이 생성된다.
생성된 Java 코드를 프로젝트에서 사용해보자.
@Override
public void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
var reply = HelloReply.newBuilder().setMessage("Hello ==> " + request.getName()).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
https://github.com/hocaron/spring-study/tree/main/spring-grpc)