스크래치에서 Java gRPC

Java에서 gRPC를 구현하는 방법을 살펴보겠습니다.

gRPC(Google Remote Procedure Call): gRPC는 마이크로서비스 간의 고속 통신을 가능하게 하기 위해 Google에서 개발한 오픈 소스 RPC 아키텍처입니다. gRPC를 통해 개발자는 다른 언어로 작성된 서비스를 통합할 수 있습니다. gRPC는 구조화된 데이터를 직렬화하기 위한 매우 효율적이고 압축된 메시징 형식인 Protobuf 메시징 형식(프로토콜 버퍼)을 사용합니다.

일부 사용 사례의 경우 gRPC API가 REST API보다 더 효율적일 수 있습니다.

gRPC에 서버를 작성해 봅시다. 먼저 서비스 및 모델(DTO)을 설명하는 여러 .proto 파일을 작성해야 합니다. 간단한 서버의 경우 ProfileService 및 ProfileDescriptor를 사용합니다.

ProfileService는 다음과 같습니다.

syntax = "proto3";
package com.deft.grpc;
import "google/protobuf/empty.proto";
import "profile_descriptor.proto";
service ProfileService {
  rpc GetCurrentProfile (google.protobuf.Empty) returns (ProfileDescriptor) {}
  rpc clientStream (stream ProfileDescriptor) returns (google.protobuf.Empty) {}
  rpc serverStream (google.protobuf.Empty) returns (stream ProfileDescriptor) {}
  rpc biDirectionalStream (stream ProfileDescriptor) returns (stream 	ProfileDescriptor) {}
}

gRPC는 다양한 클라이언트-서버 통신 옵션을 지원합니다. 우리는 그것들을 모두 분해할 것입니다:

  • 일반 서버 호출 – 요청/응답.
  • 클라이언트에서 서버로 스트리밍.
  • 서버에서 클라이언트로 스트리밍.
  • 그리고 물론 양방향 스트림.

ProfileService 서비스는 가져오기 섹션에 지정된 ProfileDescriptor를 사용합니다.

syntax = "proto3";
package com.deft.grpc;
message ProfileDescriptor {
  int64 profile_id = 1;
  string name = 2;
}
  • int64는 Java의 Long입니다. 프로필 ID를 속하게 하십시오.
  • 문자열 – Java와 마찬가지로 문자열 변수입니다.

Gradle 또는 maven을 사용하여 프로젝트를 빌드할 수 있습니다. maven을 사용하는 것이 더 편리합니다. 그리고 더 나아가 maven을 사용하는 코드가 될 것입니다. Gradle의 경우 .proto의 미래 세대가 약간 다를 것이고 빌드 파일을 다르게 구성해야 하기 때문에 이것은 충분히 중요합니다. 간단한 gRPC 서버를 작성하려면 하나의 종속성만 필요합니다.

<dependency>
    <groupId>io.github.lognet</groupId>
    <artifactId>grpc-spring-boot-starter</artifactId>
    <version>4.5.4</version>
</dependency>

정말 놀랍습니다. 이 스타터는 우리에게 엄청난 양의 작업을 수행합니다.

우리가 만들 프로젝트는 다음과 같습니다.

Spring Boot 애플리케이션을 시작하려면 GrpcServerApplication이 필요합니다. 그리고 .proto 서비스의 메서드를 구현하는 GrpcProfileService. protoc을 사용하고 작성된 .proto 파일에서 클래스를 생성하려면 protobuf-maven-plugin을 pom.xml에 추가합니다. 빌드 섹션은 다음과 같습니다.

<build>
        <extensions>
            <extension>
                <groupId>kr.motd.maven</groupId>
                <artifactId>os-maven-plugin</artifactId>
                <version>1.6.2</version>
            </extension>
        </extensions>
        <plugins>
            <plugin>
                <groupId>org.xolstice.maven.plugins</groupId>
                <artifactId>protobuf-maven-plugin</artifactId>
                <version>0.6.1</version>
                <configuration>
                    <protoSourceRoot>${project.basedir}/src/main/proto</protoSourceRoot>
                    <outputDirectory>${basedir}/target/generated-sources/grpc-java</outputDirectory>
                    <protocArtifact>com.google.protobuf:protoc:3.12.0:exe:${os.detected.classifier}</protocArtifact>
                    <pluginId>grpc-java</pluginId>
                    <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.38.0:exe:${os.detected.classifier}</pluginArtifact>
                    <clearOutputDirectory>false</clearOutputDirectory>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>compile-custom</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
  • protoSourceRoot – .proto 파일이 있는 디렉터리를 지정합니다.
  • outputDirectory – 파일이 생성될 디렉토리를 선택합니다.
  • clearOutputDirectory – 생성된 파일을 지우지 않음을 나타내는 플래그입니다.
  우리 사이에서 안전하게 환기하는 방법

이 단계에서 프로젝트를 빌드할 수 있습니다. 다음으로 출력 디렉터리에 지정한 폴더로 이동해야 합니다. 생성된 파일이 있을 것입니다. 이제 점진적으로 GrpcProfileService를 구현할 수 있습니다.

클래스 선언은 다음과 같습니다.

@GRpcService
public class GrpcProfileService extends ProfileServiceGrpc.ProfileServiceImplBase

GRpcService 주석 – 클래스를 grpc-service bean으로 표시합니다.

ProfileServiceGrpc, ProfileServiceImplBase에서 서비스를 상속하므로 부모 클래스의 메서드를 재정의할 수 있습니다. 재정의할 첫 번째 메서드는 getCurrentProfile입니다.

    @Override
    public void getCurrentProfile(Empty request, StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> responseObserver) {
        System.out.println("getCurrentProfile");
        responseObserver.onNext(ProfileDescriptorOuterClass.ProfileDescriptor
                .newBuilder()
                .setProfileId(1)
                .setName("test")
                .build());
        responseObserver.onCompleted();
    }

클라이언트에 응답하려면 전달된 StreamObserver에서 onNext 메서드를 호출해야 합니다. 응답을 보낸 후 서버가 작업을 완료했다는 신호를 클라이언트에 보냅니다. getCurrentProfile 서버에 요청을 보낼 때 응답은 다음과 같습니다.

{
  "profile_id": "1",
  "name": "test"
}

다음으로 서버 스트림을 살펴보겠습니다. 이 메시징 접근 방식을 사용하면 클라이언트가 서버에 요청을 보내고 서버는 메시지 스트림으로 클라이언트에 응답합니다. 예를 들어 루프에서 5개의 요청을 보냅니다. 전송이 완료되면 서버는 스트림의 성공적인 완료에 대한 메시지를 클라이언트에 전송합니다.

재정의된 서버 스트림 방법은 다음과 같습니다.

@Override
    public void serverStream(Empty request, StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> responseObserver) {
        for (int i = 0; i < 5; i++) {
            responseObserver.onNext(ProfileDescriptorOuterClass.ProfileDescriptor
                    .newBuilder()
                    .setProfileId(i)
                    .build());
        }
        responseObserver.onCompleted();
    }

따라서 클라이언트는 응답 번호와 동일한 ProfileId가 있는 5개의 메시지를 받게 됩니다.

{
  "profile_id": "0",
  "name": ""
}
{
  "profile_id": "1",
  "name": ""
}
…
{
  "profile_id": "4",
  "name": ""
}

클라이언트 스트림은 서버 스트림과 매우 유사합니다. 이제 클라이언트는 메시지 스트림을 전송하고 서버는 이를 처리합니다. 서버는 메시지를 즉시 처리하거나 클라이언트의 모든 요청을 기다린 후 처리할 수 있습니다.

    @Override
    public StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> clientStream(StreamObserver<Empty> responseObserver) {
        return new StreamObserver<>() {

            @Override
            public void onNext(ProfileDescriptorOuterClass.ProfileDescriptor profileDescriptor) {
                log.info("ProfileDescriptor from client. Profile id: {}", profileDescriptor.getProfileId());
            }

            @Override
            public void onError(Throwable throwable) {

            }

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

클라이언트 스트림에서 StreamObserver를 서버가 메시지를 받을 클라이언트로 반환해야 합니다. 스트림에서 오류가 발생하면 onError 메서드가 호출됩니다. 예를 들어 잘못 종료되었습니다.

  최고의 밈을 만드는 7가지 딥페이크 앱

양방향 스트림을 구현하려면 서버와 클라이언트에서 스트림 생성을 결합해야 합니다.

@Override
    public StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> biDirectionalStream(
            StreamObserver<ProfileDescriptorOuterClass.ProfileDescriptor> responseObserver) {

        return new StreamObserver<>() {
            int pointCount = 0;
            @Override
            public void onNext(ProfileDescriptorOuterClass.ProfileDescriptor profileDescriptor) {
                log.info("biDirectionalStream, pointCount {}", pointCount);
                responseObserver.onNext(ProfileDescriptorOuterClass.ProfileDescriptor
                        .newBuilder()
                        .setProfileId(pointCount++)
                        .build());
            }

            @Override
            public void onError(Throwable throwable) {

            }

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

이 예에서 클라이언트의 메시지에 대한 응답으로 서버는 pointCount가 증가된 프로필을 반환합니다.

결론

구현된 서버 스트림, 클라이언트 스트림, 양방향 스트림과 같이 gRPC를 사용하여 클라이언트와 서버 간의 메시징을 위한 기본 옵션을 다루었습니다.

이 기사는 Sergey Golitsyn이 작성했습니다.