go + gRPC

민정·2025년 7월 4일

gRPC

목록 보기
5/7

Basics tutorial를 기반으로 작성했다. 아래에서 gRPC 클라이언트 부분에 관해서만 작성한다.


.proto

  • proto 파일을 작성한 후 아래의 명령어로 컴파일한다.
  • 해당 명령어로 컴파일 시 grpc.pb.go와 pb.go 파일이 자동으로 생성된다.
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative 

pb.go

  • .proto 파일에서 정의한 message, enum 등 데이터 구조체 생성
  • 해당 구조체에 대한 직렬화/역직렬화, getter, setter 등의 메소드 생성

grpc.pb.go

  • gRPC의 클라이언트-서버 인터페이스, 서비스 stub 코드가 포함된 파일
  • gRPC 통신을 위해 반드시 필요한 핵심 파일로, 해당 파일을 참고해 클라이언트 코드를 구성한다.

.proto

  • 아래의 proto 파일을 기반으로 생성된 grpc.pb.go 파일의 클라이언트 코드를 분석한다.
syntax = "proto3";
package chat;
option go_package = "gRPC-based-chatting/chatProto;chatProto";
import "google/protobuf/timestamp.proto";

message ChatMessage {
  string channel = 1; // 채널 (채팅방 ID)
  string sender = 2;    // 송신자
  string receiver = 3;  // 수신자
  string content = 4; // 내용
  google.protobuf.Timestamp timestamp = 5; // 타임스탬프
}

service ChatService {
  rpc ChatStream(stream ChatMessage) returns (stream ChatMessage);
}

grpc.pb.go 분석

type ChatServiceClient interface { 
// NewChatServiceClient의 반환 타입
	ChatStream(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ChatMessage, ChatMessage], error)
}

type chatServiceClient struct { 
	cc grpc.ClientConnInterface
}

func NewChatServiceClient(cc grpc.ClientConnInterface) ChatServiceClient {
// gRPC 서버와의 연결 객체(ClientConnInterface)를 받아 chatServiceClient 구조체를 생성
// 생성된 구조체를 인터페이스 타입(ChatServiceClient)으로 반환
	return &chatServiceClient{cc}
}

func (c *chatServiceClient) ChatStream(
	ctx context.Context, 
	opts ...grpc.CallOption
) (grpc.BidiStreamingClient[ChatMessage, ChatMessage], error) {
// 양방향 스트림 생성 메소드
	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
	
	stream, err := c.cc.NewStream(ctx, &ChatService_ServiceDesc.Streams[0], ChatService_ChatStream_FullMethodName, cOpts...)
	
	if err != nil { // 에러 체크
		return nil, err
	}
	
	x := &grpc.GenericClientStream[ChatMessage, ChatMessage]{ClientStream: stream}
	
	return x, nil
}

type ChatService_ChatStreamClient = grpc.BidiStreamingClient[ChatMessage, ChatMessage]
// ChatStream의 반환 타입
  • go의 gRPC package 내 type과 메소드를 알아야 위의 코드를 제대로 이해할 수 있다.
  • NewChatServiceClient 메소드를 통해 gRPC 클라이언트를 생성
    • 해당 클라이언트를 통해 .proto 파일에서 정의한 service의 메소드를 호출
  • ChatStream을 통해 gRPC 클라이언트가 gRPC 서버와 통신할 스트림을 반환
    • ChatStream.proto 파일에서 정의한 service의 메소드
    • 매개변수로 적절한 context와 통신 옵션인 CallOption을 전달
  • 각 메소드의 반환 값은 type으로 정의
    • NewChatServiceClient의 반환 값 클라이언트는 ChatServiceClient type으로 정의
    • ChatStream의 반환 값 스트림은 ChatService_ChatStreamClient type으로 정의

client.go

  • .proto 코드를 컴파일해 자동 생성된 pb.gogrpc.pb.go를 참고해 사용자가 작성해야 하는 파일
type Client struct {
	conn           *grpc.ClientConn 
	client         pb.ChatServiceClient                      
	streams        map[string]pb.ChatService_ChatStreamClient
	ctx            context.Context
}

// gRPC 서버에 연결 및 새로운 클라이언트 생성
func NewClient() (*Client, error) {
	// gRPC 서버에 연결
	conn, err := grpc.NewClient("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Printf("에러 발생: %v", err)
		return nil, err
	}

	// 클라이언트 생성
	client := pb.NewChatServiceClient(conn)
	log.Printf("클라이언트 생성 완료")
	
    // 컨텍스트 생성
	ctx := context.Background()

	return &Client{
		conn:           conn,
		client:         client,
		streams:        make(map[string]pb.ChatService_ChatStreamClient),
		ctx:            ctx,
	}, nil
}

// 양방향 스트리밍 시작
func (c *Client) StartChat(chanId string) error {
	if c.streams[chanId] != nil {
		log.Printf("스트림이 이미 열려 있음")
		return nil
	}
	
    // 스트림 생성
	stream, err := c.client.ChatStream(c.ctx)
	if err != nil {
		log.Printf("스트림 생성 실패: %v", err)
		return err
	}

	c.streams[chanId] = stream
	log.Printf("스트림 생성 완료")

	// ...

	return nil
}
  • 연결 정보, gRPC 클라이언트, 스트림, 컨텍스트를 하나의 구조체 Client로 생성했다.
  • 이후 클라이언트의 메소드를 Client 구조체에 포함되도록 했다.
  • 위의 코드는 gRPC 클라이언트 연결 및 양방향 스트림 통신 시작 로직 코드이다.
profile
시스템 + 리눅스 + 클라우드

0개의 댓글