Gin과 gRPC-Web를 이용한 브라우저와 gRPC 연동
Takashi Yamamoto
Infrastructure Engineer · Leapcell

소개
현대 웹 개발에서 마이크로서비스 아키텍처는 확장 가능하고 유지 관리 가능한 애플리케이션을 구축하는 초석이 되었습니다. gRPC는 고성능, 강력한 타이핑 및 Protocol Buffers 기반의 효율적인 바이너리 프로토콜을 통해 서비스 간 통신에 탁월한 선택입니다. 그러나 gRPC 서비스를 웹 브라우저와 직접 통합하려고 할 때 상당한 문제가 발생합니다. 브라우저는 본질적으로 JSON이 아닌 HTTP/1.1 및 JSON을 이해하며 gRPC에서 사용하는 HTTP/2 및 바이너리 Protobuf 형식을 이해하지 못합니다. 이러한 불일치는 역사적으로 복잡한 중개자 또는 RESTful 프록시 계층을 필요로 했습니다. 이 문서는 Go용 고성능 HTTP 웹 프레임워크인 Gin이 gRPC-Web와 결합되어 이 간극을 효과적으로 연결하여 브라우저가 gRPC 서비스와 직접 통신할 수 있도록 하는 방법을 자세히 설명합니다. 이 접근 방식은 아키텍처를 단순화하고, gRPC의 이점을 엔드 투 엔드로 활용하며, 개발자가 보다 강력하고 효율적인 풀스택 애플리케이션을 구축할 수 있도록 지원합니다.
핵심 개념 및 구현
구현에 들어가기 전에 관련 핵심 기술을 정의해 보겠습니다.
주요 기술 이해
- gRPC (gRPC Remote Procedure Calls): Protocol Buffers를 인터페이스 정의 언어(IDL)로 사용하고 HTTP/2를 전송으로 사용하는 고성능 오픈 소스 범용 RPC 프레임워크입니다. 다양한 언어, 스트리밍 및 효율적인 직렬화를 지원하여 마이크로서비스 및 폴리글랏 환경에 이상적입니다.
- Protocol Buffers (Protobuf): 언어에 구애받지 않고 플랫폼에 구애받지 않는 확장 가능한 구조화된 데이터를 직렬화하는 메커니즘입니다. 특히 서비스 간 통신에서 데이터를 마샬링하는 데 XML 또는 JSON보다 작고 빠르며 간단합니다.
- gRPC-Web: 웹 애플리케이션이 브라우저에서 직접 gRPC 서비스와 상호 작용할 수 있도록 하는 사양 및 라이브러리 세트입니다. 브라우저와 호환되는 요청(HTTP/1.1, XHR/Fetch)을 gRPC 메시지로 변환하고 그 반대로 변환하는 브릿지 역할을 하며, 종종 gRPC와 gRPC-Web를 모두 이해하는 프록시가 필요합니다.
- Gin: Go를 위한 빠르고 가벼우며 확장 가능한 웹 프레임워크입니다. RESTful API 빌드에 자주 사용되며 gRPC-Web 요청을 처리하는 훌륭한 HTTP 서버 역할을 할 수 있습니다.
- Envoy Proxy: 클라우드 네이티브 애플리케이션을 위해 설계된 인기 있는 오픈 소스 엣지 및 서비스 프록시입니다. 역방향 프록시, 로드 밸런서 및 API 게이트웨이로 자주 사용됩니다. gRPC-Web의 맥락에서 Envoy는 브라우저의 gRPC-Web 요청을 백엔드 서비스의 네이티브 gRPC로 변환하는 중요한 중개자 역할을 할 수 있습니다.
작동 원리
핵심 아이디어는 네이티브 gRPC(HTTP/2)를 사용할 수 없는 웹 브라우저가 gRPC-Web(특정 헤더가 있는 HTTP/1.1)을 사용하여 프록시 서비스와 통신한다는 것입니다. 그런 다음 이 프록시는 이러한 gRPC-Web 요청을 표준 gRPC 호출(HTTP/2)로 변환하여 실제 gRPC 백엔드 서비스로 전달합니다. 응답은 역 경로를 따릅니다. Gin은 웹 애플리케이션을 호스팅하고 프록시 로직을 Go 애플리케이션에 내장하기로 선택한 경우 gRPC-Web 프록시 역할을 하는 표준 HTTP 서버 역할을 수행하며, 또는 더 일반적으로 gRPC-Web 변환을 처리하는 외부 Envoy 프록시와 상호 작용합니다.
Gin이 웹 애플리케이션을 호스팅하고 Envoy 프록시가 gRPC-Web 변환을 처리하는 일반적인 설정을 예로 들어 보겠습니다.
단계별 구현
간단한 "Greeter" 서비스를 빌드하여 프로세스를 설명하겠습니다.
1. Protocol Buffer 서비스 정의
먼저 Protocol Buffers를 사용하여 gRPC 서비스를 정의합니다. proto/greeter.proto
를 생성합니다.
syntax = "proto3"; option go_package = "github.com/your/repo/greetpb"; package greet; service Greeter { rpc SayHello (HelloRequest) returns (HelloResponse) {} } message HelloRequest { string name = 1; } message HelloResponse { string message = 1; }
gRPC 서비스 및 gRPC-Web 클라이언트 stub에 대한 Go 코드를 생성합니다.
# protoc 및 Go gRPC 플러그인 설치 go install google.golang.org/protobuf/cmd/protoc-gen-go@latest go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest go install github.com/grpc-ecosystem/grpc-web/protoc-gen-go-grpc-web@latest # Go gRPC 코드 생성 protoc --go_out=. --go_opt=paths=source_relative \ --go-grpc_out=. --go-grpc_opt=paths=source_relative \ proto/greeter.proto # gRPC-Web 클라이언트 stub 생성 (예: TypeScript용) protoc --js_out=import_style=commonjs,binary:. --grpc-web_out=import_style=typescript,mode=grpcwebtext:. \ proto/greeter.proto
이것은 서버용 greetpb/greeter_grpc.pb.go
, greetpb/greeter.pb.go
를 생성하고, 프런트엔드용 greeter_pb.js
, greeter_grpc_web_pb.d.ts
를 생성합니다.
2. Go에서 gRPC 서버 구현
생성된 Go 코드를 사용하여 gRPC 서버를 구현합니다. 파일: server/main.go
package main import ( "context" "fmt" "log" "net" "google.golang.org/grpc" "google.golang.org/grpc/reflection" // gRPC bloom 서비스 검색용 "github.com/your/repo/greetpb" // 실제 경로로 바꾸세요 ) type server struct { greetpb.UnimplementedGreeterServer } func (s *server) SayHello(ctx context.Context, in *greetpb.HelloRequest) (*greetpb.HelloResponse, error) { log.Printf("Received: %v", in.GetName()) return &greetpb.HelloResponse{Message: "Hello " + in.GetName()}, } func main() { lis, err := net.Listen("tcp", ":50051") if err != nil { log.Fatalf("failed to listen: %v", err) } ss := grpc.NewServer() greetpb.RegisterGreeterServer(s, &server{}) reflection.Register(s) // gRPCurl 등 반사를 활성화합니다. log.Printf("gRPC server listening at %v", lis.Addr()) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }
이 서버를 실행합니다: go run server/main.go
3. Envoy 프록시 설정
Envoy는 Gin 애플리케이션과 gRPC 서비스 모두에 프론트 역할을 하며 gRPC-Web 변환을 처리합니다. envoy.yaml
을 생성합니다.
static_resources: listeners: - name: listener_0 address: socket_address: protocol: TCP 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: ingress_http codec_type: AUTO route_config: name: local_route virtual_hosts: - name: backend domains: ["*"] routes: - match: { prefix: "/greet.Greeter" } # gRPC-Web 서비스 접두사 route: { cluster: greeter_service, timeout: 0s } typed_per_filter_config: envoy.filters.http.grpc_web: {} - match: { prefix: "/" } # Gin의 정적 파일/API용 캐치올 route: { cluster: gin_web_app } http_filters: - name: envoy.filters.http.grpc_web # gRPC-Web 필터 먼저 활성화 typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb - name: envoy.filters.http.cors # 브라우저 보안에 중요 typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors allow_origin_string_match: - exact: "*" # 프로덕션용으로 조정 allow_methods: GET, PUT, DELETE, POST, OPTIONS allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web max_age: "1728000" - name: envoy.filters.http.router clusters: - name: greeter_service # gRPC 백엔드 connect_timeout: 0.25s type: LOGICAL_DNS dns_lookup_family: V4_ONLY lb_policy: ROUND_ROBIN load_assignment: cluster_name: greeter_service endpoints: - lb_endpoints: - endpoint: address: socket_address: address: 127.0.0.1 # gRPC 서버 IP port_value: 50051 # gRPC 서버 포트 - name: gin_web_app # Gin 애플리케이션 connect_timeout: 0.25s type: LOGICAL_DNS dns_lookup_family: V4_ONLY lb_policy: ROUND_ROBIN load_assignment: cluster_name: gin_web_app endpoints: - lb_endpoints: - endpoint: address: socket_address: address: 127.0.0.1 # Gin 서버 IP port_value: 8081 # Gin 서버 포트
Envoy 실행 (Docker 설치 가정):
docker run --rm -it -p 8080:8080 -v $(pwd)/envoy.yaml:/etc/envoy/envoy.yaml envoyproxy/envoy:v1.27.0
이것은 포트 8080
에서 수신 대기하는 Envoy를 시작합니다. /greet.Greeter
를 gRPC 서버(포트 50051
)로 라우팅하고 다른 모든 요청을 Gin 앱(포트 8081
)으로 라우팅합니다.
4. Gin 웹 서버 생성
Gin은 정적 index.html
및 기타 기존 HTTP API 엔드포인트를 제공합니다. 파일: web/main.go
package main import ( "log" "net/http" "github.com/gin-gonic/gin" ) func main() { r := gin.Default() // 'public' 디렉터리에서 정적 파일 제공 r.Static("/static", "./public") r.LoadHTMLFiles("./public/index.html") r.GET("/", func(c *gin.Context) { c.HTML(http.StatusOK, "index.html", gin.H{}) }) log.Printf("Gin server listening on :8081") if err := r.Run(":8081"); err != nil { log.Fatalf("failed to run gin server: %v", err) } }
public
디렉토리를 만들고 index.html
을 그 안에 넣습니다.
public/index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>gRPC-Web Greeter</title> <script src="/static/main.js" defer></script> </head> <body> <h1>gRPC-Web Greeter Example</h1> <input type="text" id="nameInput" placeholder="Enter your name"> <button id="greetButton">Say Hello</button> <p id="response"></p> </body> </html>
Gin 서버를 실행합니다: go run web/main.go
5. 프런트엔드 애플리케이션 빌드
프런트엔드는 생성된 gRPC-Web 클라이언트를 사용하여 Envoy 프록시와 통신합니다. public/main.js
생성 (또는 TypeScript를 사용하고 컴파일):
import { GreeterClient } from '../greeter_grpc_web_pb.js'; import { HelloRequest } from '../greeter_pb.js'; const greeterClient = new GreeterClient('http://localhost:8080', null, null); // Envoy 주소 document.getElementById('greetButton').addEventListener('click', () => { const name = document.getElementById('nameInput').value; const request = new HelloRequest(); request.setName(name); greeterClient.sayHello(request, {}, (err, response) => { if (err) { console.error('Error calling SayHello:', err.code, err.message); document.getElementById('response').textContent = 'Error: ' + err.message; return; } document.getElementById('response').textContent = response.getMessage(); }); });
이 클라이언트 측 코드를 실행하려면 일반적으로 Webpack 또는 Parcel과 같은 모듈 번들러가 필요합니다. import
문을 사용하기 때문입니다. 빠른 테스트의 경우 간단한 http-server
또는 live-server
를 사용하고 import
경로를 일시적으로 조정하거나 번들링할 수 있습니다.
# Parcel 사용 예 (설치: npm install -g parcel-bundler) parcel build public/main.js --out-dir public --public-url /static
public/main.js
가 Gin에서 구성된 대로 /static/main.js
에서 액세스 가능한지 확인하십시오.
애플리케이션 시나리오
이 설정은 다음을 위해 이상적입니다.
- 단일 페이지 애플리케이션 (SPA): React, Vue, Angular 애플리케이션은 사용자 지정 REST API 계층 없이 gRPC 서비스를 직접 소비할 수 있습니다.
- 내부 대시보드/도구: 고성능으로 백엔드 마이크로서비스와 통신하는 관리 인터페이스 구축.
- 하이브리드 애플리케이션: 프런트엔드 일부가 고성능 gRPC 서비스와 통신해야 하고 다른 일부는 기존 HTTP API(Gin에서 제공)를 사용하는 경우.
- 보일러플레이트 감소: Protobuf 스키마를 직접 사용하여 프런트엔드는 자동으로 생성된 클라이언트 코드를 얻어 수동 API 클라이언트 개발을 줄입니다.
이 접근 방식의 이점
- 성능: gRPC의 바이너리 직렬화 및 HTTP/2(Envoy와 백엔드 간)를 활용하여 효율적인 데이터 전송.
- 강력한 타이핑: Protobuf는 백엔드에서 프런트엔드까지 강력한 타이핑을 제공하여 유지 관리성을 향상시키고 런타임 오류를 줄입니다.
- 통합 스키마: gRPC 및 gRPC-Web 클라이언트 모두에 대한 API 정의(Protobuf)의 단일 진실 공급원.
- 대기 시간 감소: REST API와 비교하여 JSON 구문 분석/직렬화 오버헤드 제거.
- 단순화된 아키텍처: API 게이트웨이 또는 특정 REST 어댑터에 대한 사용자 지정 코드가 적음.
결론
Gin을 웹 서버, gRPC-Web 변환을 위한 Envoy 프록시, gRPC 백엔드를 세심하게 조정함으로써 웹 브라우저가 고성능 gRPC 서비스와 직접 상호 작용할 수 있도록 성공적으로 지원했습니다. 이 강력한 조합은 강력한 타이핑, 향상된 성능 및 간소화된 개발 경험을 제공하는 풀스택 아키텍처에서 gRPC의 잠재력을 최대한 발휘합니다. 브라우저에서 gRPC를 말할 수 있다는 것은 현대 웹 애플리케이션을 설계하고 구축하는 방식을 근본적으로 변화시키며, 진정한 엔드 투 엔드 gRPC 생태계에 우리를 더 가깝게 만듭니다.