gRPC java와 gRPC go를 만들어서 서로 통신하도록 하자.
먼저 project에 사용할 directory를 만들도록 하자.
mkdir routeguide && cd ./routeguide
protocol buffer는 api/proto directory에 저장하도록 하자.
mkdir -p ./api/proto && cd ./api/proto
tree로 확인하면 다음과 같다.
/routeguide$ tree
.
├── api
│ └── proto
│ └── route_guide.proto
다음의 protocol buffer를 적도록 하자.
syntax = "proto3";
option go_package = "github.com/test/gorouteguide";
option java_multiple_files = true;
option java_package = "com.grpc.javarouteguide.grpc.routeguide";
option java_outer_classname = "RouteGuideProto";
package routeguide;
// Interface exported by the server.
service RouteGuide {
// A simple RPC.
//
// Obtains the feature at a given position.
//
// A feature with an empty name is returned if there's no feature at the given
// position.
rpc GetFeature(Point) returns (Feature) {}
// A server-to-client streaming RPC.
//
// 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-to-server streaming RPC.
//
// 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.
//
// 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) {}
}
// Points are represented as latitude-longitude pairs in the E7 representation
// (degrees multiplied by 10**7 and rounded to the nearest integer).
// Latitudes should be in the range +/- 90 degrees and longitude should be in
// the range +/- 180 degrees (inclusive).
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
// A latitude-longitude rectangle, represented as two diagonally opposite
// points "lo" and "hi".
message Rectangle {
// One corner of the rectangle.
Point lo = 1;
// The other corner of the rectangle.
Point hi = 2;
}
// A feature names something at a given point.
//
// If a feature could not be named, the name is empty.
message Feature {
// The name of the feature.
string name = 1;
// The point where the feature is detected.
Point location = 2;
}
// A RouteNote is a message sent while at a given point.
message RouteNote {
// The location from which the message is sent.
Point location = 1;
// The message to be sent.
string message = 2;
}
// A RouteSummary is received in response to a RecordRoute rpc.
//
// It contains the number of individual points received, the number of
// detected features, and the total distance covered as the cumulative sum of
// the distance between each point.
message RouteSummary {
// The number of points received.
int32 point_count = 1;
// The number of known features passed while traversing the route.
int32 feature_count = 2;
// The distance covered in metres.
int32 distance = 3;
// The duration of the traversal in seconds.
int32 elapsed_time = 4;
}
단순 RPC부터 단방향 stream, 양방향 stream 통신에 대한 예제를 배우기 좋다.
먼저 golang project를 만들기로 하자. routeguide로 가서 golang project을 만들도록 하자.
mkdir ./go-routeguide && cd ./go-routeguide
go mod init github.com/test/go-routeguide
mkdir gorouteguide
tree로 확인하면 다음과 같다.
.
├── api
│ └── proto
│ └── route_guide.proto
├── go-routeguide
| ├── gorouteguide
│ ├── go.mod
│ ├── go.sum
│ └── main.go
go get github.com/gofrs/uuid/v5
go get google.golang.org/protobuf
go get github.com/google/uuid
go get google.golang.org/grpc
다시 routeguide로 가서 protocol buffer의 stub을 go-routeguide에 만들어주도록 하자.
protoc -I=api/proto --go_out=go-routeguide/gorouteguide --go_opt=paths=source_relative --go-grpc_out=go-routeguide/gorouteguide --go-grpc_opt=paths=source_relative api/proto/route_guide.proto
잘 생성되었다면 다음과 같이 보일 것이다.
routeguide$ tree
.
├── api
│ └── proto
│ └── route_guide.proto
├── go-routeguide
│ ├── gorouteguide
│ │ ├── route_guide_grpc.pb.go
│ │ └── route_guide.pb.go
│ ├── go.mod
│ ├── go.sum
│ └── main.go
golang과 달리 java는 조금 방식이 특이하다.
먼저 routeguide에서 intellij로 프로젝트를 만들어주도록 하자. directory는 java-routeguide로 만들었고, 패키지는 com.grpc.javarouteguide로 만들었다. tree로 구조를 확인하면 다음과 같다.
├── api
│ └── proto
│ └── route_guide.proto
├── go-routeguide
│ ├── gorouteguide
│ │ ├── route_guide_grpc.pb.go
│ │ └── route_guide.pb.go
│ ├── go.mod
│ ├── go.sum
│ └── main.go
└── java-routeguide
├── build.gradle
├── gradle
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── HELP.md
├── settings.gradle
└── src
├── main
│ ├── java
│ │ └── com
│ │ └── grpc
│ │ └── javarouteguide
│ │ └── JavaRouteguideApplication.java
│ └── resources
│ ├── application.properties
│ ├── static
│ └── templates
└── test
└── java
└── com
└── grpc
└── javarouteguide
└── JavaRouteguideApplicationTests.java
이제 dependency들을 넣어주도록 하자. 참고로 jdk 21 기준이다.
def protobufVersion = '3.25.8'
def grpcVersion = '1.69.0' // CURRENT_GRPC_VERSION
dependencies {
implementation 'javax.annotation:javax.annotation-api:1.3.2'
runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" // 네티 기반 통신
implementation "io.grpc:grpc-services:${grpcVersion}" // gRPC 서버 서비스
implementation "io.grpc:grpc-protobuf:${grpcVersion}" // protobuf 지원
implementation "io.grpc:grpc-stub:${grpcVersion}" // 서비스 스텁
implementation "com.google.protobuf:protobuf-java-util:${protobufVersion}"
...
}
javax.annotation:javax.annotation-api: jdk17 이상부터는 javax에서 jakarta로 옮겨가서 이걸 써주어야 할 수 있다.나머지는 protocol buffer를 사용해서 stub을 만들고 grpc에 필요한 서버 측 라이브러리들을 기술한 라이브러리들이다.
다음으로 어떤 grpc protocol buffer을 읽고 stub을 만들 것이고, 만들어진 stub code 중 어떤 것을 읽을 것인지 알려줘야 한다.
protobuf {
protoc { artifact = "com.google.protobuf:protoc:${protobufVersion}" }
plugins {
grpc { artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" }
}
generateProtoTasks {
all()*.plugins {
grpc {} // 모든 proto 태스크에 gRPC 플러그인 적용
}
}
}
sourceSets {
main {
java {
srcDirs 'build/generated/source/proto/main/java'
srcDirs 'build/generated/source/proto/main/grpc'
}
proto {
srcDirs '../api/proto'
}
}
}
protobuf 부분은 옵션을 추가하지 않는 한 크게 건드릴 것이 없다.
sourceSets에는 크게 두 가지 부분이 있다.
1. proto: 어디에 있는 protocol buffer를 쓸 것인가
2. srcDirs: protocol buffer를 읽고 자동으로 만들어진 java stub code는 build/generated/source/proto/main/java과 build/generated/source/proto/main/grpc에 만들어지는데, 이를 실제 우리가 개발할 때 코드에 적용하도록 하기 위해서는 srcDirs에 해당 path를 명시해주어야 한다.
gradle을 통해서 clean과 build를 해주면 build directory에 자동으로 java grpc stub code를 만들었을 것이다.
./gradlew clean build
build/generated를 확인해보도록 하자.
├── build
| ....
│ ├── generated
│ │ ├── source
│ │ │ └── proto
│ │ │ └── main
│ │ │ ├── grpc
│ │ │ │ └── com
│ │ │ │ └── grpc
│ │ │ │ └── javarouteguide
│ │ │ │ └── grpc
│ │ │ │ └── routeguide
│ │ │ │ └── RouteGuideGrpc.java
│ │ │ └── java
│ │ │ └── com
│ │ │ └── grpc
│ │ │ └── javarouteguide
│ │ │ └── grpc
│ │ │ └── routeguide
│ │ │ ├── Feature.java
│ │ │ ├── FeatureOrBuilder.java
│ │ │ ├── Point.java
│ │ │ ├── PointOrBuilder.java
│ │ │ ├── Rectangle.java
│ │ │ ├── RectangleOrBuilder.java
│ │ │ ├── RouteGuideProto.java
│ │ │ ├── RouteNote.java
│ │ │ ├── RouteNoteOrBuilder.java
│ │ │ ├── RouteSummary.java
│ │ │ └── RouteSummaryOrBuilder.java
이전에 gradle에서 설정한 sourceSets의 'build/generated/source/proto/main/java'와 'build/generated/source/proto/main/grpc' 경로가 있는 것을 볼 수 있다. 그런데 해당 경로 다음으로 com.grpc.javarouteguide.grpc.routeguide 패키지에 RouteGuideGrpc.java와 RouteGuideProto.java 코드가 있는 것을 볼 수 있는데, 이는 protocol buffer 생성 시 java_package를 com.grpc.javarouteguide.grpc.routeguide으로 설정했기 때문이다.
이 generated 하위에 있는 grpc stub을 srcDirs으로 지정했기 때문에 build/classes를 타고 들어가면 protocol buffer에 java_package로 지정한 패키지 경로로 java stub code들이 있는 것을 볼 수 있다.
└── java-routeguide
├── build
│ ├── classes
│ │ └── java
│ │ ├── main
│ │ │ └── com
│ │ │ └── grpc
│ │ │ └── javarouteguide
│ │ │ ├── grpc
│ │ │ │ ├── routeguide
│ │ │ │ │ ├── Feature$1.class
│ │ │ │ │ ├── Feature$Builder.class
│ │ │ │ │ ├── Feature.class
│ │ │ │ │ ├── FeatureOrBuilder.class
│ │ │ │ │ ├── Point$1.class
│ │ │ │ │ ├── Point$Builder.class
│ │ │ │ │ ├── Point.class
│ │ │ │ │ ├── PointOrBuilder.class
│ │ │ │ │ ├── Rectangle$1.class
│ │ │ │ │ ├── Rectangle$Builder.class
│ │ │ │ │ ├── Rectangle.class
│ │ │ │ │ ├── RectangleOrBuilder.class
│ │ │ │ │ ├── RouteGuideGrpc$1.class
│ │ │ │ │ ├── RouteGuideGrpc$2.class
│ │ │ │ │ ├── RouteGuideGrpc$3.class
│ │ │ │ │ ├── RouteGuideGrpc$AsyncService.class
│ │ │ │ │ ├── RouteGuideGrpc$MethodHandlers.class
│ │ │ │ │ ├── RouteGuideGrpc$RouteGuideBaseDescriptorSupplier.class
│ │ │ │ │ ├── RouteGuideGrpc$RouteGuideBlockingStub.class
│ │ │ │ │ ├── RouteGuideGrpc$RouteGuideFileDescriptorSupplier.class
│ │ │ │ │ ├── RouteGuideGrpc$RouteGuideFutureStub.class
│ │ │ │ │ ├── RouteGuideGrpc$RouteGuideImplBase.class
│ │ │ │ │ ├── RouteGuideGrpc$RouteGuideMethodDescriptorSupplier.class
│ │ │ │ │ ├── RouteGuideGrpc$RouteGuideStub.class
│ │ │ │ │ ├── RouteGuideGrpc.class
│ │ │ │ │ ├── RouteGuideProto.class
│ │ │ │ │ ├── RouteNote$1.class
│ │ │ │ │ ├── RouteNote$Builder.class
│ │ │ │ │ ├── RouteNote.class
│ │ │ │ │ ├── RouteNoteOrBuilder.class
│ │ │ │ │ ├── RouteSummary$1.class
│ │ │ │ │ ├── RouteSummary$Builder.class
│ │ │ │ │ ├── RouteSummary.class
│ │ │ │ │ └── RouteSummaryOrBuilder.class
정리하자면 다음과 같다.
1. gradle build 시에 protocol buffer를 읽고 stub을 만드는 동작이 dependency 덕분에 자동으로 실행된다.
2. sourceSets의 proto 경로를 ../api/proto로 지정했기 때문에 해당 directory에 있는 protocol buffer를 읽게 된다.
3. protocol buffer를 읽고 stub을 자동 생성하면 'build/generated/source/proto/main/java'와 'build/generated/source/proto/main/grpc' 경로에 stub code가 생긴다.
4. 이때 protocol buffer에 지정한 java_package의 option을 읽고 해당 경로 하위에 패키지들을 만든다.
5. sourceSets의 main에서 java 파일을 읽어 build 디렉터리의 classes에 적재하겠다는 의미로 srcDirs를 'build/generated/source/proto/main/java'와 'build/generated/source/proto/main/grpc'로 지정하면 build/classes에 적재된다.
6. build/classes에 grpc stub code들이 적재되었음으로, 이제 main code에서 import가 가능해지는 것이다.
golang이랑 비교하면 설정도 복잡하고 과정도 요상하다.
gRPC의 통신 방식은 총 4개이다.
rpc GetFeature(Point) returns (Feature) {}이 여기에 해당한다.rpc ListFeatures(Rectangle) returns (stream Feature) {} 이 여기에 해당한다.rpc RecordRoute(stream Point) returns (RouteSummary) {}가 여기에 해당한다.rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}이 여기에 해당한다.먼저 단순한 RPC로 요청을 보내면 응답을 보내는 gRPC를 만들어보도록 하자.
service RouteGuide {
...
rpc GetFeature(Point) returns (Feature) {}
...
}
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
message Feature {
// The name of the feature.
string name = 1;
// The point where the feature is detected.
Point location = 2;
}
RouteGuide의 GetFeature은 Point라는 위도 경도 정보를 받으면 해당 위도 경도를 가진 지역의 이름인 Feature를 전달해준다.
GetFeature 처럼 한 개의 요청을 받으면 바로 한 개의 응답을 내주는 gRPC 요청에 대해서 '단항 RPC(unary RPC)' 이라고 한다.
go로 gRPC server를 만들고, java로 gRPC client를 만들어 사용해보도록 하자. 요청-응답을 보내보도록 하자.
먼저 go gRPC 서버를 만들도록 하자.
package server
import (
"context"
"encoding/json"
"log"
"sync"
pb "github.com/test/go-routeguide/gorouteguide"
"google.golang.org/protobuf/proto"
)
var exampleData = []byte(`[{
"location": {
"latitude": 4078383,
"longitude": -7461437
},
"name": "Patriots Path, Mendham, NJ 07945, USA"
}, {
"location": {
"latitude": 4081228,
"longitude": -7439991
},
"name": "101 New Jersey 10, Whippany, NJ 07981, USA"
}]`)
type routeGuideServer struct {
pb.UnimplementedRouteGuideServer
savedFeatures []*pb.Feature
mu sync.Mutex
routeNotes map[string][]*pb.RouteNote
}
func (r *routeGuideServer) loadExampleData() {
if err := json.Unmarshal(exampleData, &r.savedFeatures); err != nil {
log.Fatalf("failed to load default features: %v", err)
}
}
func (s *routeGuideServer) GetFeature(_ context.Context, point *pb.Point) (*pb.Feature, error) {
log.Println("requests comming: " + point.String())
for _, feature := range s.savedFeatures {
if proto.Equal(feature.Location, point) {
return feature, nil
}
}
return &pb.Feature{Location: point}, nil
}
func NewRouteGuideServer() *routeGuideServer {
rgs := &routeGuideServer{
routeNotes: map[string][]*pb.RouteNote{},
}
rgs.loadExampleData()
return rgs
}
exampleData는 예제 데이터로 초기 데이터 로딩할 때 사용한다.routeGuideServer: gRPC protocol buffer에 정의한 rpc service 메서드를 구현한 구현체이다. routeGuideServer을 gRPC 서버를 만들 때 전달해주면 된다.
다음과 같이 만들어주도록 하자.
package main
import (
"log"
"net"
pb "github.com/test/go-routeguide/gorouteguide"
"github.com/test/go-routeguide/server"
"google.golang.org/grpc"
)
var (
port = ":50051"
)
func main() {
s, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
gs := grpc.NewServer()
pb.RegisterRouteGuideServer(gs, server.NewRouteGuideServer())
log.Println("server starting... " + port)
if err := gs.Serve(s); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
tcp connection을 열고 grpc server에 우리가 정의한 protocol buffer 구현체를 넣어주면 된다. 이제 상대 측에서 GetFeature을 호출하면 GetFeature 메서드가 실행될 것이다.
먼저 golang gRPC 서버를 실행해보도록 하자.
go run ./...
2025/08/08 14:38:32 server starting... :50051
다음의 로그가 나오면 성공이다.
java 클라이언트 code를 만들어주도록 하자. 단, grpc stub code가 이미 만들어져 있어야 하므로 build가 한 번은 실행되어 있어야 한다.
└── src
├── main
│ ├── java
│ │ └── com
│ │ └── grpc
│ │ └── javarouteguide
│ │ ├── JavaRouteguideApplication.java
│ │ └── RouteGuideClient.java
golang gRPC 서버에 있는 GetFeature를 호출하는, RouteGuideClient.java를 구현해보도록 하자.
public class RouteGuideClient {
private final RouteGuideGrpc.RouteGuideBlockingStub blockingStub;
private final RouteGuideGrpc.RouteGuideStub asyncStub;
private Random random = new Random();
private TestHelper testHelper;
public RouteGuideClient(Channel channel) {
blockingStub = RouteGuideGrpc.newBlockingStub(channel);
asyncStub = RouteGuideGrpc.newStub(channel);
}
public void getFeature(int lat, int lon) {
System.out.println("*** GetFeature: lat=" + lat + ", lon=" + lon);
Point request = Point.newBuilder().setLatitude(lat).setLongitude(lon).build();
Feature feature;
try {
feature = blockingStub.getFeature(request);
if (testHelper != null) {
testHelper.onMessage(feature);
}
} catch (StatusRuntimeException e) {
System.out.println("RPC failed" + e.getStatus());
if (testHelper != null) {
testHelper.onRpcError(e);
}
return;
}
if (feature != null && !feature.getName().isEmpty()) {
System.out.printf("Found feature called \"{%s}\" at {%s}, {%s} \n",
feature.getName(),
feature.getLocation().getLatitude(),
feature.getLocation().getLongitude());
} else {
System.out.printf("Found no feature at {%s}, {%s}\n",
feature.getLocation().getLatitude(),
feature.getLocation().getLongitude());
}
}
/**
* Only used for unit test, as we do not want to introduce randomness in unit test.
*/
@VisibleForTesting
void setRandom(Random random) {
this.random = random;
}
/**
* Only used for helping unit test.
*/
@VisibleForTesting
interface TestHelper {
/**
* Used for verify/inspect message received from server.
*/
void onMessage(Message message);
/**
* Used for verify/inspect error received from server.
*/
void onRpcError(Throwable exception);
}
@VisibleForTesting
void setTestHelper(TestHelper testHelper) {
this.testHelper = testHelper;
}
}
눈 여겨 볼 것은 두 개의 grpc stub 관련 객체인 RouteGuideBlockingStub와 RouteGuideStub가 있다는 것이다.
public class RouteGuideClient {
private final RouteGuideGrpc.RouteGuideBlockingStub blockingStub;
private final RouteGuideGrpc.RouteGuideStub asyncStub;
...
public RouteGuideClient(Channel channel) {
blockingStub = RouteGuideGrpc.newBlockingStub(channel);
asyncStub = RouteGuideGrpc.newStub(channel);
}
...
}
RouteGuideBlockingStub은 요청을 보내고 응답이 올 때까지 기다려야 할 때 사용하고 RouteGuideStub은 요청을 보내고 응답을 바로 받지 않아도 될 때 사용한다. 둘 다 Channel이라는 gRPC channel 객체를 받는데, 이는 target server의 host와 port를 의미한다.
우리의 경우는 단순한 gRPC로 동기식 요청-응답 구조를 갖으므로 RouteGuideBlockingStub을 사용하도록 하자.
java main 코드를 만들어보도록 하자. 필자는 버릇처럼 Spring boot로 실행했지만 굳이 spring으로 만들 필요는 없다.
@SpringBootApplication
public class JavaRouteguideApplication {
public static void main(String[] args) throws InterruptedException {
SpringApplication.run(JavaRouteguideApplication.class, args);
String target = "localhost:50051";
List<Feature> features;
ManagedChannel channel = Grpc.newChannelBuilder(target, InsecureChannelCredentials.create()).build();
try {
RouteGuideClient client = new RouteGuideClient(channel);
client.getFeature(4078383, -7461437);
client.getFeature(111111, 111111);
} finally {
channel.shutdownNow().awaitTermination(5, TimeUnit.SECONDS);
}
}
}
target server의 주소인 target을 만들고 Channel 객체로 만들어주어 RouteGuideClient의 getFeature를 호출하면 된다. channel은 ManagedChannel 객체로 shutdownNow와 awaitTermination 메서드를 가지고 있다. java에서 thread를 썼던 사람들에게는 아주 익숙한 메서드로 timeout 설정할 때 아주 좋다.
현재 getFeature는 두 번의 요청을 하고 있는 것을 볼 수 있다. 첫번째는 실제 golang gRPC server에 있는 데이터이고 두 번째는 아니다. 따라서 첫번째 요청은 실제로 찾았다는 응답이 와야하고, 두번째는 없다는 응답이 와야 한다.
현재는 5초 간의 timeout이 설정된 것을 볼 수 있다.
이제 실행시켜보도록 하자.
*** GetFeature: lat=4078383, lon=-7461437
Found feature called "{Patriots Path, Mendham, NJ 07945, USA}" at {4078383}, {-7461437}
*** GetFeature: lat=111111, lon=111111
Found no feature at {111111}, {111111}
응답이 잘 온 것을 볼 수 있다.
서버 측에서도 확인하면 다음과 같다.
2025/08/08 14:38:32 server starting... :50051
2025/08/08 14:38:49 requests comming: latitude:4078383 longitude:-7461437
2025/08/08 14:38:49 requests comming: latitude:111111 longitude:111111
요청을 받고 잘 처리한 것이 보인다.