Protocol Buffers proto3 ๋ฌธ๋ฒ, 4๊ฐ์ง RPC ํต์ ํจํด(Unary/Streaming), protoc ์ปดํ์ผ, Spring Boot/Go/Node.js ์ฐ๋, ์ธํฐ์
ํฐ, ์๋ฌ ๋ชจ๋ธ ๋ฐ grpcurl ๋๋ฒ๊น
๊ฐ์ด๋
## 1. gRPC vs REST ์ํคํ
์ฒ ๋น๊ต & ํต์ฌ ํน์ง
Google์ด ๊ฐ๋ฐํ ์คํ์์ค ๊ณ ์ฑ๋ฅ ์๊ฒฉ ํ๋ก์์ ํธ์ถ(RPC) ํ๋ ์์ํฌ๋ก, ๋ง์ดํฌ๋ก์๋น์ค ๊ฐ ํต์ ๊ณผ ์ค์๊ฐ ์คํธ๋ฆฌ๋ฐ์ ์ต์ ํ๋์ด ์์ต๋๋ค.
```text
[gRPC vs REST API ํต์ฌ ๋น๊ต]
โข ํ๋กํ ์ฝ : HTTP/2 ์ ์ฉ (๋ฐ์ด๋๋ฆฌ ํ๋ ์ด๋ฐ, ๋ฉํฐํ๋ ์ฑ, ์๋ฐฉํฅ ์คํธ๋ฆฌ๋ฐ) vs HTTP/1.1 or 2
โข ํ์ด๋ก๋ ํฌ๋งท: Protocol Buffers (๋ฐ์ด๋๋ฆฌ ์ง๋ ฌํ, JSON ๋๋น 3~10๋ฐฐ ๋น ๋ฅด๊ณ ์ฉ๋ 70% ์ ๊ฐ) vs JSON / XML
โข ๊ณ์ฝ ๋ฐฉ์ : ์๊ฒฉํ ํ์
์ .proto ์คํค๋ง ๊ธฐ๋ฐ IDL vs OpenAPI / Swagger (์ ํ ์ฌํญ)
โข ํต์ ํจํด : Unary, Server Stream, Client Stream, Bi-directional Stream vs ๋จ๋ฐฉํฅ Request-Response
โข ๋ธ๋ผ์ฐ์ ์ฐ๋: gRPC-Web ๋ฐ Envoy ํ๋ก์ ํ์ vs ๋ธ๋ผ์ฐ์ ๋ค์ดํฐ๋ธ ์ง์ ์ง์ (fetch, XHR)
โข ์ฃผ ์ฌ์ฉ์ฒ : MSA ๋ด๋ถ ๊ณ ์ ํต์ , ๋์ฉ๋ ์ค์๊ฐ IoT/์คํธ๋ฆฌ๋ฐ, ๋ชจ๋ฐ์ผ ๋ฐฑ์๋ vs ๊ณต๊ฐ Public API
```
## 2. Protocol Buffers v3 (`proto3`) ๋ฌธ๋ฒ ์๋ฒฝ ๊ฐ์ด๋
์คํค๋ง ํ์ผ(`.proto`)์ ๋ฉ์์ง ๊ตฌ์กฐ์ ๋ฐ์ดํฐ ํ์
์ ์๊ฒฉํ๊ฒ ์ ์ํฉ๋๋ค.
```protobuf
syntax = "proto3"; // proto3 ๋ฌธ๋ฒ ๋ช
์ (ํ์)
package user.service.v1; // ํจํค์ง ๋ค์์คํ์ด์ค
// โ ์ธ์ด๋ณ ์์ฑ ์ต์
option java_multiple_files = true;
option java_package = "com.lucky.grpc.user.v1";
option go_package = "github.com/lucky/service/user/v1;userv1";
// ๐ฆ Google ๋ด์ฅ ๊ณต์ฉ ํ์
์ํฌํธ
import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";
// ๐ค [1] ๊ธฐ๋ณธ ๋ฉ์์ง ์ ์
message User {
// ํ๋ ๋ฒํธ(1~15)๋ 1๋ฐ์ดํธ๋ก ์ธ์ฝ๋ฉ๋๋ฏ๋ก ์์ฃผ ์ฌ์ฉํ๋ ํต์ฌ ํ๋์ ํ ๋น
int64 id = 1;
string username = 2;
string email = 3;
bool is_active = 4;
Role role = 5;
// ๐ ๋ฐฐ์ด/๋ฆฌ์คํธ (repeated)
repeated string tags = 6;
// ๐บ๏ธ Key-Value ๋งต (map<KeyType, ValueType>)
map<string, string> metadata = 7;
// โฐ ํ์์คํฌํ
google.protobuf.Timestamp created_at = 8;
// ๐ oneof: ์ฌ๋ฌ ํ๋ ์ค ๋จ ํ๋๋ง ๊ฐ ์ค์ ๊ฐ๋ฅ (๋ฉ๋ชจ๋ฆฌ ์ ์ฝ)
oneof auth_method {
string password_hash = 9;
string oauth_token = 10;
}
// ๐ซ reserved: ์ญ์ ๋ ํ๋ ๋ฒํธ๋ ์ด๋ฆ์ ์ฌ์ฌ์ฉํ์ง ๋ชปํ๋๋ก ์๊ตฌ ๊ณ ์ (ํ์ ํธํ์ฑ)
reserved 11, 12, 20 to 30;
reserved "temp_token", "legacy_id";
}
// ๐ท๏ธ [2] ์ด๊ฑฐํ(Enum) ์ ์
enum Role {
// proto3์ Enum 0๋ฒ ์ธ๋ฑ์ค๋ ๋ฐ๋์ ๊ธฐ๋ณธ๊ฐ(UNSPECIFIED)์ด์ด์ผ ํฉ๋๋ค.
ROLE_UNSPECIFIED = 0;
ROLE_USER = 1;
ROLE_ADMIN = 2;
ROLE_MANAGER = 3;
}
```
## 3. gRPC ์๋น์ค ์ ์ & 4๊ฐ์ง RPC ํต์ ํจํด
ํ๋์ ์๋น์ค ๋ด์์ ๋จํญ ๋ฐ 3๊ฐ์ง ํํ์ ์คํธ๋ฆฌ๋ฐ ํต์ ๋ฐฉ์์ ์ ์ธํฉ๋๋ค.
```protobuf
service UserService {
// 1๏ธโฃ [๋จํญ RPC (Unary RPC)]
// 1๊ฐ์ ์์ฒญ์ ๋ณด๋ด๊ณ 1๊ฐ์ ์๋ต์ ๋ฐ๋ ๊ฐ์ฅ ์ผ๋ฐ์ ์ธ ํต์ ๋ฐฉ์ (REST ๋์ฒด)
rpc GetUser (UserRequest) returns (UserResponse);
// 2๏ธโฃ [์๋ฒ ์คํธ๋ฆฌ๋ฐ RPC (Server Streaming)]
// ํด๋ผ์ด์ธํธ์ 1๊ฐ ์์ฒญ์ ๋ํด ์๋ฒ๊ฐ ์ฌ๋ฌ ๊ฐ์ ์๋ต ๋ฉ์์ง๋ฅผ ์ง์์ ์ผ๋ก ์คํธ๋ฆฌ๋ฐ (๋ค์ด๋ก๋/ํผ๋)
rpc StreamUserLogs (LogRequest) returns (stream LogEntry);
// 3๏ธโฃ [ํด๋ผ์ด์ธํธ ์คํธ๋ฆฌ๋ฐ RPC (Client Streaming)]
// ํด๋ผ์ด์ธํธ๊ฐ ๋์ฉ๋ ๋ฐ์ดํฐ ์กฐ๊ฐ๋ค์ ์ฐ์ ์ ์กํ๊ณ , ์๋ฒ๊ฐ ์ฒ๋ฆฌ๋ฅผ ๋ง์น ๋ค ์ต์ข
1ํ ์๋ต (ํ์ผ ์
๋ก๋)
rpc UploadAuditLogs (stream AuditLog) returns (UploadSummary);
// 4๏ธโฃ [์๋ฐฉํฅ ์คํธ๋ฆฌ๋ฐ RPC (Bidirectional Streaming)]
// ํด๋ผ์ด์ธํธ์ ์๋ฒ๊ฐ ๋
๋ฆฝ์ ์ธ ์คํธ๋ฆผ์ ์ด์ด ์ธ์ ๋ ๋น๋๊ธฐ๋ก ๋ฉ์์ง๋ฅผ ์ฃผ๊ณ ๋ฐ์ (์ค์๊ฐ ์ฑํ
/IoT)
rpc ChatRoom (stream ChatMessage) returns (stream ChatMessage);
}
message UserRequest { int64 id = 1; }
message UserResponse { User user = 1; }
message LogRequest { int64 user_id = 1; }
message LogEntry { string text = 1; int64 timestamp = 2; }
message AuditLog { string action = 1; }
message UploadSummary { int32 count = 1; bool success = 2; }
message ChatMessage { string sender = 1; string message = 2; }
```
## 4. ์ฝ๋ ์์ฑ (protoc ์ปดํ์ผ๋ฌ & ํ๋ฌ๊ทธ์ธ)
`.proto` ํ์ผ๋ก๋ถํฐ ํ๊ฒ ํ๋ก๊ทธ๋๋ฐ ์ธ์ด์ ๋ชจ๋ธ ํด๋์ค์ gRPC ํด๋ผ์ด์ธํธ/์๋ฒ ์ค์ผ๋ ํค ์ฝ๋๋ฅผ ์๋ ์์ฑํฉ๋๋ค.
```bash
# ๐ ๏ธ [1] protoc CLI ๊ธฐ๋ณธ ์ปดํ์ผ ๋ช
๋ น์ด
# Go ์ฝ๋ ์์ฑ (protoc-gen-go, protoc-gen-go-grpc ํ๋ฌ๊ทธ์ธ ํ์)
protoc --proto_path=. \
--go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
user.proto
# Python ์ฝ๋ ์์ฑ
python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. user.proto
# ๐ฆ [2] Go ํ๋ฌ๊ทธ์ธ ์ค์น (์ฌ์ ์ค๋น)
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
```
### Gradle ๋น๋ ์ค์ (`build.gradle.kts` - Java/Kotlin)
```kotlin
plugins {
id("com.google.protobuf") version "0.9.4"
}
protobuf {
protoc { artifact = "com.google.protobuf:protoc:3.25.1" }
plugins {
create("grpc") { artifact = "io.grpc:protoc-gen-grpc-java:1.60.0" }
}
generateProtoTasks {
all().forEach { task ->
task.plugins { create("grpc") }
}
}
}
```
## 5. Spring Boot & Java ์ฐ๋ (`@GrpcService` & `@GrpcClient`)
Spring Boot 3 ํ๊ฒฝ์์ `net.devh:grpc-spring-boot-starter`๋ฅผ ์ด์ฉํ ์๋ฒ ๋ฐ ํด๋ผ์ด์ธํธ ๊ตฌํ์
๋๋ค.
```java
// ๐ [1] gRPC ์๋ฒ ๊ตฌํ์ฒด (Spring Boot)
package com.lucky.grpc.server;
import com.lucky.grpc.user.v1.*;
import io.grpc.stub.StreamObserver;
import net.devh.boot.grpc.server.service.GrpcService;
@GrpcService // Spring Bean ๋ฑ๋ก ๋ฐ gRPC ์๋ฒ์ ์๋น์ค ์๋ ๋
ธ์ถ
public class UserGrpcServiceImpl extends UserServiceGrpc.UserServiceImplBase {
@Override
public void getUser(UserRequest request, StreamObserver<UserResponse> responseObserver) {
long userId = request.getId();
User user = User.newBuilder()
.setId(userId)
.setUsername("lucky")
.setEmail("lucky@example.com")
.setIsActive(true)
.build();
UserResponse response = UserResponse.newBuilder().setUser(user).build();
// ๊ฒฐ๊ณผ ์ ์ก ๋ฐ ์๋ฃ ์๋ฆผ
responseObserver.onNext(response);
responseObserver.onCompleted();
}
// ์๋ฒ ์คํธ๋ฆฌ๋ฐ ์์
@Override
public void streamUserLogs(LogRequest request, StreamObserver<LogEntry> responseObserver) {
for (int i = 1; i <= 5; i++) {
LogEntry entry = LogEntry.newBuilder()
.setText("๋ก๊ทธ ์ด๋ฒคํธ #" + i + " for User " + request.getUserId())
.setTimestamp(System.currentTimeMillis())
.build();
responseObserver.onNext(entry); // ์ฌ๋ฌ ๋ฒ ํธ์ถ
}
responseObserver.onCompleted(); // ์คํธ๋ฆผ ์ข
๋ฃ ์๋ฆผ
}
}
```
```java
// ๐ป [2] Spring Boot ํด๋ผ์ด์ธํธ ํธ์ถ (Stub ์ฃผ์
)
package com.lucky.grpc.client;
import com.lucky.grpc.user.v1.*;
import net.devh.boot.grpc.client.inject.GrpcClient;
import org.springframework.stereotype.Service;
@Service
public class UserClientService {
// ๋๊ธฐ ๋ธ๋กํน Stub (๊ฐ์ฅ ํํ๊ฒ ์ฌ์ฉ)
@GrpcClient("user-service")
private UserServiceGrpc.UserServiceBlockingStub userBlockingStub;
public User fetchUser(long id) {
UserRequest request = UserRequest.newBuilder().setId(id).build();
UserResponse response = userBlockingStub.getUser(request);
return response.getUser();
}
}
```
### `application.yml` ์ค์
```yaml
grpc:
server:
port: 9090 # gRPC ์๋ฒ ๋ฆฌ์ค๋ ํฌํธ
client:
user-service:
address: 'static://localhost:9090'
negotiation-type: plaintext # ๋ก์ปฌ ๊ฐ๋ฐ ์ TLS ๋ฏธ์ ์ฉ
```
## 6. Go (Golang) ์๋ฒ & ํด๋ผ์ด์ธํธ ๊ตฌํ
Go๋ gRPC์ ๋ค์ดํฐ๋ธ 1๋ฑ ์๋ฏผ ์ธ์ด๋ก ์ด๊ฒฝ๋ ๊ณ ์ฑ๋ฅ ํต์ ์ ์ฃผ๋ก ์ฌ์ฉ๋ฉ๋๋ค.
```go
// ๐ [1] Go gRPC ์๋ฒ ๊ตฌํ (main.go)
package main
import (
"context"
"log"
"net"
pb "myproject/user/v1"
"google.golang.org/grpc"
)
type server struct {
pb.UnimplementedUserServiceServer
}
func (s *server) GetUser(ctx context.Context, in *pb.UserRequest) (*pb.UserResponse, error) {
log.Printf("์์ฒญ ์์ User ID: %d", in.GetId())
return &pb.UserResponse{
User: &pb.User{
Id: in.GetId(),
Username: "lucky_go",
Email: "go@example.com",
IsActive: true,
},
}, nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("ํฌํธ ๋ฐ์ธ๋ฉ ์คํจ: %v", err)
}
s := grpc.NewServer()
pb.RegisterUserServiceServer(s, &server{})
log.Println("gRPC Go Server ๋ฆฌ์ค๋ ์์ :50051")
if err := s.Serve(lis); err != nil {
log.Fatalf("์๋ฒ ์คํ ์คํจ: %v", err)
}
}
```
```go
// ๐ป [2] Go gRPC ํด๋ผ์ด์ธํธ ํธ์ถ
package main
import (
"context"
"log"
"time"
pb "myproject/user/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
// gRPC ์ฑ๋ ์ฐ๊ฒฐ ์์ฑ
conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("์ฐ๊ฒฐ ์คํจ: %v", err)
}
defer conn.Close()
client := pb.NewUserServiceClient(conn)
// ์ปจํ
์คํธ์ 2์ด ํ์์์ ์ง์ (ํ์ ๊ถ์ฅ!)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
resp, err := client.GetUser(ctx, &pb.UserRequest{Id: 42})
if err != nil {
log.Fatalf("RPC ํธ์ถ ์คํจ: %v", err)
}
log.Printf("์กฐํ๋ ์ ์ ์ด๋ฆ: %s", resp.GetUser().GetUsername())
}
```
## 7. Node.js / TypeScript ๊ตฌํ (`@grpc/grpc-js`)
Node.js์์ proto ํ์ผ์ ๋์ ์ผ๋ก ๋ก๋ํ๊ฑฐ๋ ์ปดํ์ผ๋ ์ฝ๋๋ก ํต์ ํฉ๋๋ค.
```typescript
import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
// ๐ [1] .proto ํ์ผ ๋์ ๋ก๋
const packageDefinition = protoLoader.loadSync('user.proto', {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const userProto: any = grpc.loadPackageDefinition(packageDefinition).user.service.v1;
// ๐ [2] ์๋ฒ ๊ตฌ๋
const server = new grpc.Server();
server.addService(userProto.UserService.service, {
GetUser: (call: any, callback: any) => {
const userId = call.request.id;
callback(null, {
user: { id: userId, username: 'lucky_node', email: 'node@test.com' },
});
},
});
server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => {
console.log('Node.js gRPC Server Running on port 50051');
server.start();
});
// ๐ป [3] ํด๋ผ์ด์ธํธ ํธ์ถ
const client = new userProto.UserService('localhost:50051', grpc.credentials.createInsecure());
client.GetUser({ id: 10 }, (err: any, response: any) => {
if (err) console.error(err);
else console.log('์๋ต:', response.user);
});
```
## 8. ๋ฉํ๋ฐ์ดํฐ(Metadata) & ์ธํฐ์
ํฐ(Interceptor)
์ธ์ฆ ํ ํฐ(JWT) ์ ๋ฌ, ๋ถ์ฐ ์ถ์ (Tracing), ๊ณตํต ๋ก๊น
์ HTTP/2 ํค๋ ๋ฉํ๋ฐ์ดํฐ์ ์ธํฐ์
ํฐ๋ก ์ฒ๋ฆฌํฉ๋๋ค.
```go
// ๐ [1] ํด๋ผ์ด์ธํธ: ์์๋ฐ์ด๋ ํค๋์ Bearer ํ ํฐ ์ฃผ์
(Go)
import "google.golang.org/grpc/metadata"
// ๋ฉํ๋ฐ์ดํฐ ์์ฑ
md := metadata.Pairs("authorization", "Bearer my-jwt-token", "x-request-id", "uuid-1234")
ctx := metadata.NewOutgoingContext(context.Background(), md)
// ์์ฒญ ์ ๋ฉํ๋ฐ์ดํฐ ์ปจํ
์คํธ ์ ๋ฌ
resp, err := client.GetUser(ctx, &pb.UserRequest{Id: 1})
```
```java
// ๐ก๏ธ [2] ์๋ฒ: ์ธํฐ์
ํฐ๋ก ์ธ์ฆ ํ ํฐ ๊ฒ์ฆ (Java)
package com.lucky.grpc.interceptor;
import io.grpc.*;
import net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor;
@GrpcGlobalServerInterceptor
public class AuthServerInterceptor implements ServerInterceptor {
private static final Metadata.Key<String> AUTH_KEY =
Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER);
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) {
String authHeader = headers.get(AUTH_KEY);
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
// ์ธ์ฆ ์คํจ ์ ์ฆ์ UNAUTHENTICATED ์๋ฌ ๋ฆฌํด
call.close(Status.UNAUTHENTICATED.withDescription("์ธ์ฆ ํ ํฐ์ด ๋๋ฝ๋์์ต๋๋ค."), headers);
return new ServerCall.Listener<ReqT>() {};
}
// ์ ์ ์์ฒญ ๊ณ์ ์งํ
return next.startCall(call, headers);
}
}
```
## 9. ์๋ฌ ํธ๋ค๋ง & gRPC ํ์ค ์ํ ์ฝ๋
REST HTTP ์ํ ์ฝ๋(404, 500 ๋ฑ)์ 1:1๋ก ๋์๋๋ gRPC ํ์ค ์ํ ์ฝ๋ ์ฒด๊ณ์
๋๋ค.
```text
[gRPC ํ์ค ์ํ ์ฝ๋ (Status Codes)]
โข OK (0) : ์ฑ๊ณต (HTTP 200)
โข CANCELLED (1) : ํด๋ผ์ด์ธํธ๊ฐ ์์
์ทจ์ (HTTP 499)
โข UNKNOWN (2) : ์ ์ ์๋ ์๋ฒ ๋ด๋ถ ์ค๋ฅ
โข INVALID_ARGUMENT (3) : ํ๋ผ๋ฏธํฐ/์
๋ ฅ๊ฐ ์ ํจ์ฑ ๊ฒ์ฆ ์คํจ (HTTP 400)
โข DEADLINE_EXCEEDED (4) : ํ์์์ ์ด๊ณผ (HTTP 504)
โข NOT_FOUND (5) : ๋์ ๋ฆฌ์์ค๋ฅผ ์ฐพ์ ์ ์์ (HTTP 404)
โข ALREADY_EXISTS (6) : ์ด๋ฏธ ์กด์ฌํ๋ ๋ฆฌ์์ค ์์ฑ ์๋ (HTTP 409)
โข PERMISSION_DENIED (7) : ๊ถํ ๋ถ์กฑ (HTTP 403)
โข RESOURCE_EXHAUSTED (8) : Rate Limit / ์ฟผํฐ ์ด๊ณผ (HTTP 429)
โข UNAUTHENTICATED (16) : ์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์/ํ ํฐ ๋ง๋ฃ (HTTP 401)
```
```java
// โ ๏ธ ์๋ฒ์์ ๋น์ฆ๋์ค ์์ธ ๋์ง๊ธฐ ์์ (Java)
if (user == null) {
responseObserver.onError(
Status.NOT_FOUND
.withDescription("ID " + userId + "์ ํด๋นํ๋ ์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.")
.asRuntimeException()
);
return;
}
```
## 10. CLI ๋๋ฒ๊น
& ํ
์คํธ ๋๊ตฌ (`grpcurl`)
Postman GUI ๋๋ ํฐ๋ฏธ๋์ `cURL`์ฒ๋ผ gRPC ์๋ฒ๋ฅผ ์์ฝ๊ฒ ํธ์ถํ๊ณ ํ
์คํธํ๋ ํ์ CLI ๋๊ตฌ์
๋๋ค.
```bash
# ๐ ๏ธ [1] grpcurl ์ค์น (macOS: brew install grpcurl / Go: go install ... / Windows: winget)
winget install fullstorydev.grpcurl
# ๐ [2] ์๋ฒ์์ ์ ๊ณตํ๋ ์๋น์ค ๋ชฉ๋ก ์กฐํ (์๋ฒ ๋ฆฌํ๋ ์
ํ์ฑํ ํ์)
grpcurl -plaintext localhost:9090 list
# ๐ [3] ํน์ ์๋น์ค์ ์์ธ ๋ฉ์๋ ๋ฐ ์คํค๋ง ๊ตฌ์กฐ ํ์ธ
grpcurl -plaintext localhost:9090 describe user.service.v1.UserService
# โก [4] ๋จํญ RPC ํธ์ถ (JSON ๋ฌธ์์ด ํ์ด๋ก๋ ์ ๋ฌ)
grpcurl -plaintext \
-d '{"id": 42}' \
localhost:9090 \
user.service.v1.UserService/GetUser
# ๐ [5] ๋ฉํ๋ฐ์ดํฐ(ํค๋) ํฌํจ ํธ์ถ
grpcurl -plaintext \
-H 'authorization: Bearer my-secret-token' \
-d '{"id": 100}' \
localhost:9090 \
user.service.v1.UserService/GetUser
```
## 11. ๋ธ๋ผ์ฐ์ ์ฐ๋: gRPC-Web & Envoy Proxy
์น ๋ธ๋ผ์ฐ์ ๋ HTTP/2 ๋ฐ์ด๋๋ฆฌ ํ๋ ์์ ์ง์ ์กฐ์ํ ์ ์์ผ๋ฏ๋ก ์ค๊ฐ์ **Envoy Proxy**๋ฅผ ๋์ด gRPC-Web ํ๋กํ ์ฝ์ ๋ค์ดํฐ๋ธ gRPC๋ก ๋ณํํฉ๋๋ค.
```yaml
# ๐ Envoy Proxy ๋ผ์ฐํ
์ค์ ํ์ผ (envoy.yaml)
static_resources:
listeners:
- name: grpc_web_listener
address:
socket_address: { address: 0.0.0.0, port_value: 8080 } # ๋ธ๋ผ์ฐ์ ๊ฐ ์ ์ํ ํฌํธ
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: grpc_web
http_filters:
- name: envoy.filters.http.grpc_web # โ
gRPC-Web ๋ณํ ํํฐ
- name: envoy.filters.http.cors # โ
๋ธ๋ผ์ฐ์ CORS ํ์ฉ ํํฐ
- name: envoy.filters.http.router
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
cors:
allow_origin_string_match: [{ prefix: "*" }]
allow_methods: GET, PUT, DELETE, POST, OPTIONS
allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout
routes:
- match: { prefix: "/" }
route:
cluster: backend_grpc_service
timeout: 0s
clusters:
- name: backend_grpc_service
connect_timeout: 0.25s
type: logical_dns
http2_protocol_options: {} # ๋ฐฑ์๋์ HTTP/2 ํต์
load_assignment:
cluster_name: backend_grpc_service
endpoints:
- lb_endpoints:
- endpoint:
socket_address: { address: 127.0.0.1, port_value: 9090 } # ์ค์ ๋ฐฑ์๋ gRPC ํฌํธ
```
์๊ฒฌ ๋ฐ ์ง๋ฌธ
0์์ง ๋ฑ๋ก๋ ์๊ฒฌ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋๊ธ์ ๋จ๊ฒจ๋ณด์ธ์!
๋๊ธ ์์
๋๊ธ ์ญ์