gRCP 사용해보기

dev-well-being·2023년 1월 10일
1
post-thumbnail

요즘 server to server 통신을 할 때 gRCP를 많이 사용한다고 들었다. 구글에서 만든 RPC 통신 기술이라고 하는데 어떤 기술인지 궁금하여 사용해보았다.

gRPC 공식사이트

아래는 공식사이트에서 퍼온 gRPC 소개글이다.

Introduction to gRPC
An introduction to gRPC and protocol buffers.

This page introduces you to gRPC and protocol buffers. gRPC can use protocol buffers as both its Interface Definition Language (IDL) and as its underlying message interchange format. If you’re new to gRPC and/or protocol buffers, read this! If you just want to dive in and see gRPC in action first, select a language and try its Quick start.

Overview
In gRPC, a client application can directly call a method on a server application on a different machine as if it were a local object, making it easier for you to create distributed applications and services. As in many RPC systems, gRPC is based around the idea of defining a service, specifying the methods that can be called remotely with their parameters and return types. On the server side, the server implements this interface and runs a gRPC server to handle client calls. On the client side, the client has a stub (referred to as just a client in some languages) that provides the same methods as the server.

gRPC clients and servers can run and talk to each other in a variety of environments - from servers inside Google to your own desktop - and can be written in any of gRPC’s supported languages. So, for example, you can easily create a gRPC server in Java with clients in Go, Python, or Ruby. In addition, the latest Google APIs will have gRPC versions of their interfaces, letting you easily build Google functionality into your applications

Protocol Buffer라는 IDL(Inteface Definition Language)를 사용해서 서버 간의 메세지를 주고받는데 사용한다고 한다. Protocol Buffer(구글에서 만듬)는 다양한 언어 환경을 지원해서 서버 간의 언어가 달라도 메세지를 주고 받을 수 있다.

C# / .NET
C++
Dart
Go
Java
Kotlin
Node
Objective-C
PHP
Python
Ruby

이 글은 공식사이트의 가이드를 참고하여 Java로 구현하고자 한다.


gradle

gradle에서 grpc 관련 라이브러리 dependency를 설정해준다. ('grpc' 주석 위치 참고)

plugins {
    id 'java'
    id 'com.google.protobuf' version '0.8.18' //grpc
}

group 'hello'
version '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    //grpc
    runtimeOnly 'io.grpc:grpc-netty-shaded:1.50.2'
    implementation 'io.grpc:grpc-stub:1.50.2'
    implementation 'io.grpc:grpc-protobuf:1.50.2'
    compileOnly 'org.apache.tomcat:annotations-api:6.0.53'
    implementation "com.google.protobuf:protobuf-java-util:3.21.8"

    //lombok
    compileOnly 'org.projectlombok:lombok:1.18.24'
    annotationProcessor 'org.projectlombok:lombok:1.18.24'

    testCompileOnly 'org.projectlombok:lombok:1.18.24'
    testAnnotationProcessor 'org.projectlombok:lombok:1.18.24'

    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0'
}

sourceSets {
    main {
        java {
            srcDirs 'build/generated/source/proto/main/grpc'
            srcDirs 'build/generated/source/proto/main/java'
        }
    }
}

//grpc
protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.21.7"
    }
    plugins {
        grpc {
            artifact = 'io.grpc:protoc-gen-grpc-java:1.50.2'
        }
    }
    generateProtoTasks {
        all()*.plugins {
            grpc {}
        }
    }
}

test {
    useJUnitPlatform()
}

Proto 파일 생성

main 폴더 밑에 proto라는 패키지를 만든 후 proto 파일을 생성한다.

proto 파일 안에는 아래와 같이 작성하였다.
service는 총 4가지 방식을 지원하는데 가장 심플한 rpc 방식만 선언했다.

syntax = "proto3";

// generated에 생성되는 패키지 경로
option java_package = "hello";
// proto에 정의한 service, message 등을 개별 파일들로 만들어준다.
option java_multiple_files = true;
// client에서 호출할 패키지명
package hello;

service HelloService {
  rpc HelloWorld(HelloRequest) returns (HelloResponse) {}
}

message HelloRequest {
  string first_name = 1;
  string last_name = 2;
}
message HelloResponse {
  string message = 1;
}

내용을 모두 작성하고 build를 하게 되면 아래처럼 proto 정의된 내용을 바탕으로 java에서 사용할 수 있도록 java 파일을 생성해준다.

아래는 gRPC 제공하는 4가지 rpc 타입에 대한 내용이다.

Then we define rpc methods inside our service definition, specifying their request and response types. gRPC lets you define four kinds of service methods, all of which are used in the RouteGuide service:

  • A simple RPC where the client sends a request to the server using the stub and waits for a response to come back, just like a normal function call.

// Obtains the feature at a given position.
rpc GetFeature(Point) returns (Feature) {}

  • A server-side streaming RPC where the client sends a request to the server and gets a stream to read a sequence of messages back. The client reads from the returned stream until there are no more messages. As you can see in our example, you specify a server-side streaming method by placing the stream keyword before the response type.

// Obtains the Features available within the given Rectangle. Results are
// streamed rather than returned at once (e.g. in a response message with a
// repeated field), as the rectangle may cover a large area and contain a
// huge number of features.
rpc ListFeatures(Rectangle) returns (stream Feature) {}

  • A client-side streaming RPC where the client writes a sequence of messages and sends them to the server, again using a provided stream. Once the client has finished writing the messages, it waits for the server to read them all and return its response. You specify a client-side streaming method by placing the stream keyword before the request type.

// Accepts a stream of Points on a route being traversed, returning a
// RouteSummary when traversal is completed.
rpc RecordRoute(stream Point) returns (RouteSummary) {}

  • A bidirectional streaming RPC where both sides send a sequence of messages using a read-write stream. The two streams operate independently, so clients and servers can read and write in whatever order they like: for example, the server could wait to receive all the client messages before writing its responses, or it could alternately read a message then write a message, or some other combination of reads and writes. The order of messages in each stream is preserved. You specify this type of method by placing the stream keyword before both the request and the response.

// Accepts a stream of RouteNotes sent while a route is being traversed,
// while receiving other RouteNotes (e.g. from other users).
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}


Service 구현

어떤 메세지를 주고 받을지 서비스를 만들어보자. proto에 정의하여 만들어진 HelloServiceGrpc.HelloServiceImplBase를 상속받아 내용을 구현한다. 실제 제품을 구현한다면 비즈니스 로직을 여기다 구현해야 할 것이다.

public class HelloServiceImpl extends HelloServiceGrpc.HelloServiceImplBase {
    @Override
    public void helloWorld(HelloRequest request, StreamObserver<HelloResponse> responseObserver) {

        try {
            String firstName = request.getFirstName();
            String lastName = request.getLastName();

            System.out.println(request);

            HelloResponse helloResponse = HelloResponse.newBuilder()
                    .setMessage("hello! " + lastName + " " + firstName + "!")
                    .build();

            responseObserver.onNext(helloResponse);

        } catch (Exception e){
            responseObserver.onError(e);
        } finally {
            responseObserver.onCompleted();
        }
    }
}

Server & Client

server와 Client를 구현한다. gRPC는 동기식 비동기식 모두 지원하기 때문에 두 가지 방식 모두 구현해보았다.

  • server
@Log
public class HelloServer {
    public static void main(String[] args) throws IOException, InterruptedException {
        Server server = ServerBuilder
                .forPort(11570)
                .addService(new HelloServiceImpl()).build();
        log.info("==server start============================================================");
        server.start();
        server.awaitTermination();
    }
}
  • client
@Log
public class HelloClient {
    public static void main(String[] args) throws InterruptedException {
        ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 11570)
                .usePlaintext()
                .build();
        
        // 동기식
        HelloServiceGrpc.HelloServiceBlockingStub stub = HelloServiceGrpc.newBlockingStub(channel);

        HelloRequest request = HelloRequest.newBuilder()
                .setFirstName("Velnova")
                .setLastName("Jung")
                .build();

        HelloResponse helloResponse = stub.helloWorld(request);

        log.log(Level.INFO, "response : {0}", helloResponse);

        // 비동기식
        HelloServiceGrpc.HelloServiceStub asyncStub = HelloServiceGrpc.newStub(channel);

        HelloRequest aSyncRequest = HelloRequest.newBuilder()
                .setFirstName("Kildong")
                .setLastName("Hong")
                .build();

        StreamObserver<HelloResponse> responseObserver = new StreamObserver<>(){

            @Override
            public void onNext(HelloResponse value) {
                log.log(Level.INFO, "response Async : {0}", value);
            }

            @Override
            public void onError(Throwable t) {
                log.log(Level.WARNING, "response Async error", t);
            }

            @Override
            public void onCompleted() {
                log.log(Level.INFO, "Complete!!!!!!!!!!!!!!");
            }
        };

        asyncStub.helloWorld(aSyncRequest, responseObserver);

        channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);

    }
}

실행

이제는 실행할 수 있는 준비는 다 됐다. 클라이언트가 호출하기에 앞서 서버를 먼저 띄어준다.

1월 10, 2023 4:40:24 오후 hello.HelloServer main
INFO: ==server start============================================================

서버가 정상적으로 실행되었으니 클라이언트를 실행하여 서버와 통신을 한다.

1월 10, 2023 4:41:35 오후 hello.HelloClient main
INFO: response : message: "hello! Jung Velnova!"

1월 10, 2023 4:41:35 오후 hello.HelloClient$1 onNext
INFO: response Async : message: "hello! Hong Kildong!"

1월 10, 2023 4:41:35 오후 hello.HelloClient$1 onCompleted
INFO: Complete!!!!!!!!!!!!!!

정상적으로 로그가 호출된 것을 확인하였다.

profile
안녕하세요!! 좋은 개발 문화를 위해 노력하는 dev-well-being 입니다.

0개의 댓글