Go와 gRPC를 이용한 고성능 마이크로서비스 구축
Lukas Schneider
DevOps Engineer · Leapcell

소개
오늘날 빠르게 발전하는 소프트웨어 환경에서 마이크로서비스 아키텍처는 확장 가능하고, 탄력적이며, 독립적으로 배포 가능한 애플리케이션을 구축하기 위한 지배적인 패러다임으로 부상했습니다. 조직이 이 아키텍처 스타일을 채택함에 따라 서비스 간 통신 프로토콜의 선택이 매우 중요해집니다. 전통적인 REST API는 유연하지만 텍스트 기반 특성과 종종 중복되는 데이터 직렬화로 인해 오버헤드가 발생할 수 있습니다. 여기서 바로 고성능 오픈 소스 범용 RPC 프레임워크인 gRPC가 빛을 발합니다. HTTP/2를 기반으로 하고 효율적인 데이터 직렬화를 위해 프로토콜 버퍼를 활용하는 gRPC는 특히 다국어 환경에서 마이크로서비스 간의 더 빠르고 효율적인 통신을 달성하기 위한 매력적인 대안을 제공합니다. 이 글은 Go와 함께 gRPC의 세계를 탐구하며 강력하고 성능이 뛰어난 마이크로서비스 통신을 구축하기 위해 gRPC의 힘을 활용하는 방법을 시연합니다.
핵심 개념 이해
실질적인 구현에 들어가기 전에 gRPC의 핵심이 되는 몇 가지 기본 개념을 명확히 해봅시다.
- RPC (Remote Procedure Call): 기본적으로 RPC는 프로그램이 다른 주소 공간(일반적으로 원격 컴퓨터)에 있는 프로시저(또는 함수)를 로컬 프로시저 호출처럼 호출할 수 있도록 합니다. RPC의 마법은 백그라운드에서 네트워크 통신, 데이터 직렬화 및 역직렬화를 처리하는 것입니다.
- Protocol Buffers (protobuf): 이것은 구조화된 데이터를 직렬화하기 위한 Google의 언어 중립적, 플랫폼 중립적, 확장 가능한 메커니즘입니다. XML이나 JSON과 달리 Protocol Buffers는 바이너리 형식으로, 훨씬 작고 구문 분석 속도가 빠릅니다.
.proto
파일에 데이터 구조를 정의하면protoc
(Protocol Buffers 컴파일러)이 다양한 언어로 코드를 생성하여 구조화된 데이터를 쉽게 읽고 쓸 수 있습니다. - IDL (Interface Definition Language): Protocol Buffers는 gRPC의 IDL 역할을 합니다. 서비스 인터페이스와 클라이언트 및 서버 간에 교환되는 메시지 구조를 정의하는 데 사용됩니다.
- HTTP/2: gRPC는 HTTP/2를 기본 전송 프로토콜로 사용합니다. HTTP/2는 멀티플렉싱(단일 TCP 연결을 통한 여러 요청/응답), 헤더 압축 및 서버 푸시와 같이 HTTP/1.1에 비해 여러 가지 이점을 제공하며, 이 모든 것이 gRPC의 고성능에 기여합니다.
- Stub/Client:
.proto
파일에서 생성된 클라이언트 측 라이브러리로, 애플리케이션이 원격 gRPC 서비스에 대한 호출을 할 수 있도록 합니다. - Server:
.proto
파일에 정의된 서비스의 구현으로, 클라이언트로부터 요청을 받고 응답을 다시 보냅니다.
Go gRPC 서비스 구현
실질적인 예제를 통해 gRPC의 힘을 설명해 봅시다. 간단한 제품 카탈로그 마이크로서비스를 구축하는 것입니다.
1. Protocol Buffers를 사용하여 서비스 정의
먼저 .proto
파일에 서비스와 메시지 유형을 정의합니다. product.proto
라는 파일을 만듭니다.
syntax = "proto3"; option go_package = "./pb"; // 생성된 코드에 대한 Go 패키지를 지정합니다. package product_service; // Product는 카탈로그의 단일 제품을 나타냅니다. message Product { string id = 1; string name = 2; string description = 3; float price = 4; } // ID로 제품을 가져오는 요청 message GetProductRequest { string id = 1; } // 새 제품을 생성하는 요청 message CreateProductRequest { string name = 1; string description = 2; float price = 3; } // 제품 생성 후 응답 message CreateProductResponse { Product product = 1; } // 서비스 정의 service ProductService { rpc GetProduct (GetProductRequest) returns (Product); rpc CreateProduct (CreateProductRequest) returns (CreateProductResponse); }
2. Protobuf에서 Go 코드 생성
다음으로 protoc
컴파일러를 사용하여 .proto
파일에서 Go 코드를 생성합니다. protoc
및 Go gRPC 플러그인을 설치해야 합니다.
# protoc 설치 (아직 설치되지 않은 경우) # 공식 지침: https://grpc.io/docs/protoc-installation/ # macOS의 경우: brew install protobuf # 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 코드를 생성합니다.
protoc --go_out=./pb --go_opt=paths=source_relative \ --go-grpc_out=./pb --go-grpc_opt=paths=source_relative \ product.proto
이 명령은 pb
디렉터리를 생성하며, 여기에는 product.pb.go
(메시지 정의)와 product_grpc.pb.go
(서비스 인터페이스 및 stub)가 포함됩니다.
3. gRPC 서버 구현
이제 Go에서 ProductService
서버를 구현해 봅시다.
// server/main.go package main import ( "context" "fmt" "log" "net" "sync" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" pb "your-module-path/pb" // 실제 모듈 경로로 바꾸세요 ) // server는 pb.ProductServiceServer를 구현합니다. type server struct { pb.UnimplementedProductServiceServer // 이전 호환성을 위해 반드시 포함해야 합니다. products map[string]*pb.Product mu sync.RWMutex nextID int } func newServer() *server { return &server{ products: make(map[string]*pb.Product), nextID: 1, } } func (s *server) GetProduct(ctx context.Context, req *pb.GetProductRequest) (*pb.Product, error) { s.mu.RLock() defer s.mu.RUnlock() product, ok := s.products[req.GetId()] if !ok { return nil, status.Errorf(codes.NotFound, "ID %s를 가진 제품을 찾을 수 없습니다", req.GetId()) } log.Printf("제품을 가져왔습니다: %v", product) return product, nil } func (s *server) CreateProduct(ctx context.Context, req *pb.CreateProductRequest) (*pb.CreateProductResponse, error) { s.mu.Lock() defer s.mu.Unlock() productID := fmt.Sprintf("prod-%d", s.nextID) s.nextID++ newProduct := &pb.Product{ Id: productID, Name: req.GetName(), Description: req.GetDescription(), Price: req.GetPrice(), } s.products[productID] = newProduct log.Printf("새 제품을 만들었습니다: %v", newProduct) return &pb.CreateProductResponse{Product: newProduct}, nil } func main() { lis, err := net.Listen("tcp", ":50051") if err != nil { log.Fatalf("listen 실패: %v", err) } ss := grpc.NewServer() pb.RegisterProductServiceServer(ss, newServer()) // 서비스 구현 등록 log.Printf("server가 %v에서 listen 중입니다", lis.Addr()) if err := ss.Serve(lis); err != nil { log.Fatalf("serve 실패: %v", err) } }
4. gRPC 클라이언트 빌드
이제 ProductService
와 상호 작용할 클라이언트를 만들어 보겠습니다.
// client/main.go package main import ( "context" "log" "time" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" pb "your-module-path/pb" // 실제 모듈 경로로 바꾸세요 ) func main() { conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Fatalf("연결되지 않았습니다: %v", err) } defer conn.Close() c := pb.NewProductServiceClient(conn) // 새 클라이언트 생성 ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() // 제품 생성 createRes, err := c.CreateProduct(ctx, &pb.CreateProductRequest{ Name: "Laptop Pro", Description: "전문가를 위한 고성능 노트북", Price: 1200.00, }) if err != nil { log.Fatalf("제품을 생성할 수 없습니다: %v", err) } log.Printf("생성된 제품: %s", createRes.GetProduct().GetId()) // 생성된 제품 가져오기 getProductRes, err := c.GetProduct(ctx, &pb.GetProductRequest{Id: createRes.GetProduct().GetId()}) if err != nil { log.Fatalf("제품을 가져올 수 없습니다: %v", err) } log.Printf("검색된 제품: %s - %s (%.2f)", getProductRes.GetId(), getProductRes.GetName(), getProductRes.GetPrice()) // 존재하지 않는 제품 가져오기 시도 _, err = c.GetProduct(ctx, &pb.GetProductRequest{Id: "non-existent-id"}) if err != nil { log.Printf("존재하지 않는 제품 가져오기 오류 (예상됨): %v", err) } }
예제 실행
- Go 모듈 초기화:
go mod init your-module-path # 예: go mod init example.com/grpc-demo go mod tidy
- protobuf 코드 생성 (위에서 보인 대로).
- 서버 시작:
go run server/main.go
- 다른 터미널에서 클라이언트 실행:
go run client/main.go
그러면 서버는 제품 생성 및 검색을 로깅하고 클라이언트는 성공적인 작업과 존재하지 않는 제품에 대한 예상된 오류를 로깅합니다.
적용 시나리오
gRPC는 다양한 마이크로서비스 통신 패턴에 특히 적합합니다:
- 내부 마이크로서비스 통신: 같은 조직 내에서 개발된 여러 서비스로 구성된 시스템을 구축할 때 gRPC는 매우 효율적이고 강력한 형식의 통신 메커니즘을 제공합니다.
- 다국어 환경: Protocol Buffers를 통해 달성되는 언어 불가지론적 특성은 다양한 프로그래밍 언어(예: Go 백엔드, Python 머신러닝 서비스, Java 결제 서비스)로 작성된 서비스에 탁월한 선택입니다.
- 실시간 및 저지연 애플리케이션: HTTP/2의 멀티플렉싱 및 바이너리 직렬화 덕분에 gRPC는 IoT 장치, 게임 백엔드 또는 금융 거래 시스템과 같이 높은 처리량과 낮은 지연 시간이 필요한 시나리오에서 뛰어납니다.
- 스트리밍 데이터: gRPC는 서버 측, 클라이언트 측 및 양방향 스트리밍과 같은 다양한 스트리밍 유형을 기본적으로 지원하므로 실시간 업데이트, 채팅 애플리케이션 또는 데이터 파이프라인과 같은 시나리오에 이상적입니다.
- 모바일 클라이언트: 공개 API에 HTTP/JSON만큼 일반적이지는 않지만, gRPC는 모바일 앱과 백엔드 서비스 간의 통신에 사용될 수 있으며, 특히 네트워크 대역폭이 문제가 될 때 성능 이점을 제공합니다.
결론
Go와 gRPC는 고성능, 강력하고 확장 가능한 마이크로서비스를 구축하기 위한 강력한 조합을 형성합니다. 효율적인 데이터 직렬화를 위해 Protocol Buffers를 사용하고 전송을 위해 HTTP/2를 활용함으로써 gRPC는 통신 오버헤드를 크게 줄이고 강력한 형식 검사 및 코드 생성을 통해 개발자 생산성을 향상시킵니다. gRPC를 채택하면 더 효율적인 서비스 간 통신과 더 탄력적인 마이크로서비스 아키텍처를 얻을 수 있습니다. 개발자는 다음 마이크로서비스 프로젝트에 이 프레임워크를 채택함으로써 상당한 성능 향상과 간소화된 개발 경험을 얻을 수 있습니다.