gRPC를 배워보자 3일차 - gRPC Unary RPC

gRPC

목록 보기
3/6

java <-> go의 gRPC 사용 예제

  1. client측 java code: https://github.com/grpc/grpc-java/blob/master/examples/src/main/java/io/grpc/examples/routeguide/RouteGuideUtil.java
  2. server 측 golang code: https://github.com/grpc/grpc-go/blob/master/examples/route_guide/server/server.go

gRPC java와 gRPC go를 만들어서 서로 통신하도록 하자.

먼저 project에 사용할 directory를 만들도록 하자.

mkdir routeguide && cd ./routeguide

protocol buffer 정의

protocol buffer는 api/proto directory에 저장하도록 하자.

mkdir -p ./api/proto && cd ./api/proto

tree로 확인하면 다음과 같다.

/routeguide$ tree
.
├── api
│   └── proto
│       └── route_guide.proto

다음의 protocol buffer를 적도록 하자.

  • route_guide.proto
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 관련 gRPC 만들기

먼저 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
  • grpc관련 go library 설치
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에 만들어주도록 하자.

  • protocol buffer 기반의 code 생성
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

Java 관련 gRPC 만들기

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 기준이다.

  • build.gradle
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}"
    ...
}
  1. javax.annotation:javax.annotation-api: jdk17 이상부터는 javax에서 jakarta로 옮겨가서 이걸 써주어야 할 수 있다.

나머지는 protocol buffer를 사용해서 stub을 만들고 grpc에 필요한 서버 측 라이브러리들을 기술한 라이브러리들이다.

다음으로 어떤 grpc protocol buffer을 읽고 stub을 만들 것이고, 만들어진 stub code 중 어떤 것을 읽을 것인지 알려줘야 한다.

  • build.gradle
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/javabuild/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.javaRouteGuideProto.java 코드가 있는 것을 볼 수 있는데, 이는 protocol buffer 생성 시 java_packagecom.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. sourceSetsproto 경로를 ../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. sourceSetsmain에서 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 통신 방식

gRPC의 통신 방식은 총 4개이다.

  1. 단항 RPC(Unary RPC): 단일 클라이언트 요청에 단일 응답을 보내는 것이다. 우리의 rpc GetFeature(Point) returns (Feature) {}이 여기에 해당한다.
  2. 서버 스트리밍 RPC(Server streaming RPC): 단일 클라이언트 요청에 대해서 여러 메시지 스트림 응답을 전달하는 것이다. rpc ListFeatures(Rectangle) returns (stream Feature) {} 이 여기에 해당한다.
  3. 클라이언트 스트리밍 RPC(Client streaming RPC): 여러 클라이언트 메시지 스트림 요청에 대해서 서버는 단일 응답을 보내는 것이다. rpc RecordRoute(stream Point) returns (RouteSummary) {}가 여기에 해당한다.
  4. 양방향 스트리밍 RPC(Bidirectional streaming RPC): 클라이언트 <-> 서버 간 독립적인 메시지 스트림이다. 실시간 양방향 통신이라고 생각하면 된다. rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}이 여기에 해당한다.

Unary RPC

먼저 단순한 RPC로 요청을 보내면 응답을 보내는 gRPC를 만들어보도록 하자.

  • route_guide.proto
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;
}

RouteGuideGetFeaturePoint라는 위도 경도 정보를 받으면 해당 위도 경도를 가진 지역의 이름인 Feature를 전달해준다.

GetFeature 처럼 한 개의 요청을 받으면 바로 한 개의 응답을 내주는 gRPC 요청에 대해서 '단항 RPC(unary RPC)' 이라고 한다.

go로 gRPC server를 만들고, java로 gRPC client를 만들어 사용해보도록 하자. 요청-응답을 보내보도록 하자.

Unary RPC golang 서버

먼저 go gRPC 서버를 만들도록 하자.

  • go-routeguide/server/routeguide_server.go
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
}
  1. exampleData는 예제 데이터로 초기 데이터 로딩할 때 사용한다.
  2. routeGuideServer: gRPC protocol buffer에 정의한 rpc service 메서드를 구현한 구현체이다.

routeGuideServer을 gRPC 서버를 만들 때 전달해주면 된다.

다음과 같이 만들어주도록 하자.

  • go-routeguide/main.go
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

다음의 로그가 나오면 성공이다.

Unary RPC java 클라이언트

java 클라이언트 code를 만들어주도록 하자. 단, grpc stub code가 이미 만들어져 있어야 하므로 build가 한 번은 실행되어 있어야 한다.

  • 디렉터리 구조
└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── grpc
    │   │           └── javarouteguide
    │   │               ├── JavaRouteguideApplication.java
    │   │               └── RouteGuideClient.java

golang gRPC 서버에 있는 GetFeature를 호출하는, RouteGuideClient.java를 구현해보도록 하자.

  • 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 관련 객체인 RouteGuideBlockingStubRouteGuideStub가 있다는 것이다.

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 객체로 만들어주어 RouteGuideClientgetFeature를 호출하면 된다. channelManagedChannel 객체로 shutdownNowawaitTermination 메서드를 가지고 있다. 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

요청을 받고 잘 처리한 것이 보인다.

0개의 댓글