Files
grpccanary/examples/grpcentity

인터페이스 정의 언어(IDL) 파일 정의하기

인터페이스 정의 언어(IDL) 파일을 정의하는 방법을 배워보겠습니다.

우리가 개발할 gRPC 서비스는 다음 기능을 지원할 것입니다:

  • 서버는 클라이언트에게 현재 날짜와 시간을 반환해야 합니다.
  • 서버는 클라이언트에게 주어진 길이의 무작위로 생성된 비밀번호를 반환해야 합니다.
  • 서버는 클라이언트에게 무작위 정수를 반환해야 합니다.

gRPC 클라이언트와 서버 개발을 시작하기 전에, IDL 파일을 먼저 정의해야 합니다. IDL 정의는 이 저장소 루트에 위치한 protoapi.proto 파일을 사용합니다.

IDL 파일의 구조

다음은 protoapi.proto라는 IDL 파일의 내용입니다:

syntax = "proto3";

이 파일은 프로토콜 버퍼 언어의 proto3 버전을 사용합니다. 이전 버전인 proto2도 있으며, 약간의 문법적 차이가 있습니다. proto3를 명시하지 않으면, 프로토콜 버퍼 컴파일러는 proto2를 사용하는 것으로 간주합니다. 버전 정의는 .proto 파일의 첫 번째 비어있지 않은, 주석이 아닌 라인에 위치해야 합니다.

syntax = "proto3";

option go_package = "./protoapi/;protoapi";

gRPC 도구들은 이 .proto 파일로부터 Go 코드를 생성할 것입니다. 위 라인은 생성될 Go 패키지의 이름이 protoapi임을 명시합니다. ./protoapi/를 사용했기 때문에 출력 파일은 protoapi/ 하위 디렉토리에 생성됩니다.

service Random {
    rpc GetDate (RequestDateTime) returns (DateTime);
    rpc GetRandom (RandomParams) returns (RandomInt);
    rpc GetRandomPass (RequestPass) returns (RandomPass);
}

이 블록은 gRPC 서비스의 이름(Random)과 지원하는 메서드들을 명시합니다. 또한, 각 상호작용에 필요한 메시지들을 지정합니다. 예를 들어, GetDate의 경우 클라이언트는 RequestDateTime 메시지를 보내고 DateTime 메시지를 받기를 기대합니다.

이 메시지들은 동일한 .proto 파일에 정의되어 있습니다.

// For random number

모든 .proto 파일은 C와 C++ 스타일의 주석을 지원합니다. 즉, // text/* text */ 형식의 주석을 사용할 수 있습니다.

message RandomParams {
    int64 Seed = 1;
    int64 Place = 2;
}

난수 생성기는 시드(seed) 값으로 시작하며, 이 값은 클라이언트가 지정하여 RandomParams 메시지를 통해 서버로 전송됩니다. Place 필드는 무작위로 생성된 정수 시퀀스에서 반환될 난수의 위치를 지정합니다.

message RandomInt {
    int64 Value = 1;
}

앞선 두 메시지는 GetRandom 메서드와 관련이 있습니다. RandomParams는 요청의 매개변수를 설정하는 데 사용되고, RandomInt는 서버가 생성한 난수를 저장하는 데 사용됩니다. 모든 메시지 필드는 int64 데이터 타입을 가집니다.

message DateTime {
    string Value = 1;
}

message RequestDateTime {
    string Value = 2;
}

위 두 메시지는 GetDate 메서드의 동작을 지원하기 위한 것입니다. RequestDateTime 메시지는 실질적인 데이터를 담고 있지 않은 더미 메시지입니다. 단지 클라이언트가 서버로 보내는 메시지가 필요할 뿐이며, Value 필드에는 어떤 종류의 정보든 저장할 수 있습니다. 서버가 반환하는 정보는 DateTime 메시지에 string 값으로 저장됩니다.

참고: RequestDateTimeValue 필드 번호가 2로 지정되어 있습니다. 프로토콜 버퍼에서 필드 번호는 태그 번호로 사용되며 고유한 번호라면 임의의 값을 가질 수 있지만, 일반적으로는 첫 필드에 1을 사용하는 것이 관례입니다.

// For random password
message RequestPass {
    int64 Seed = 1;
    int64 Length = 8;
}

message RandomPass {
    string Password = 1;
}

마지막으로, 위 두 메시지는 GetRandomPass의 동작을 위한 것입니다.

요약하자면, IDL 파일은 다음을 수행합니다:

  • proto3를 사용함을 명시합니다.
  • 서비스의 이름이 Random임을 정의합니다.
  • 생성될 Go 패키지의 이름이 protoapi임을 명시합니다.
  • gRPC 서비스가 GetDate, GetRandom, GetRandomPass 세 가지 메서드를 지원함을 정의하고, 이 메서드 호출에서 교환될 메시지들의 이름을 정의합니다.
  • 데이터 교환에 사용될 여섯 가지 메시지의 형식을 정의합니다.

Go에서 IDL 파일 사용하기

다음 중요한 단계는 이 파일을 Go에서 사용할 수 있는 형식으로 변환하는 것입니다. protoapi.proto나 다른 .proto 파일을 처리하여 관련된 Go .pb.go 파일을 생성하기 위해 몇 가지 추가 도구를 다운로드해야 합니다. 프로토콜 버퍼 컴파일러 바이너리의 이름은 protoc입니다. macOS에서는 brew install protobuf 명령을 사용하여 protoc를 설치해야 합니다. 마찬가지로, Homebrew를 사용하여 protoc-gen-go-grpcprotoc-gen-go 패키지도 설치해야 합니다. 이 두 패키지는 Go와 관련이 있습니다.

Linux에서는 선호하는 패키지 관리자를 사용하여 protobuf를 설치하고, go install google.golang.org/protobuf/cmd/protoc-gen-go@latest 명령을 사용하여 protoc-gen-go를 설치해야 합니다. 마찬가지로, go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest를 실행하여 protoc-gen-go-grpc 실행 파일을 설치해야 합니다.

참고: Go 1.16부터는 모듈 모드에서 패키지를 빌드하고 설치하는 데 go install을 사용하는 것이 권장됩니다. go get의 사용은 더 이상 사용되지 않습니다. go install을 사용할 때는 최신 버전을 설치하기 위해 패키지 이름 뒤에 @latest를 추가하는 것을 잊지 마세요.

  • protoc
  • protoc-gen-go
  • protoc-gen-go-grpc

변환 과정은 다음 단계를 필요로 합니다:

protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. \
--go-grpc_opt=paths=source_relative protoapi.proto

이 명령을 실행하면, 리포지토리 루트 하위의 protoapi 디렉토리에 protoapi_grpc.pb.goprotoapi.pb.go라는 두 개의 파일이 생성됩니다. protoapi.pb.go 소스 코드 파일에는 메시지가 포함되어 있고, protoapi_grpc.pb.go에는 서비스가 포함되어 있습니다.

protoapi_grpc.pb.go의 첫 열 줄은 다음과 같습니다:

// Code generated by protoc-gen-go-grpc. DO NOT EDIT.

package protoapi

앞서 논의했듯이, 패키지 이름은 protoapi입니다.

import (
	context "context"
	grpc "google.golang.org/grpc"

codes "google.golang.org/grpc/codes"
	status "google.golang.org/grpc/status"
)

이것은 import 블록입니다. context "context"가 있는 이유는 context가 예전에는 표준 Go 라이브러리의 일부가 아닌 외부 Go 패키지였기 때문입니다.

protoapi.pb.go의 첫 줄은 다음과 같습니다:

// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// 	protoc-gen-go v1.33.0
// 	protoc        v3.21.12
// source: protoapi.proto

package protoapi

protoapi_grpc.pb.goprotoapi.pb.go는 모두 protoapi Go 패키지의 일부이므로, 코드에서 한 번만 포함하면 됩니다.

gRPC 서버 개발

IDL을 통해 생성된 Go 코드를 기반으로, 실제 비즈니스 로직을 수행할 gRPC 서버(server.go)를 구현합니다.

1. 서비스 인터페이스 구현

우리가 .proto 파일에 정의한 Random 서비스의 메서드들은 RandomServer 구조체 타입을 통해 구현됩니다. 이 구조체는 stub 코드의 UnimplementedRandomServer를 임베딩하여 기본 호환성을 확보합니다.

type RandomServer struct {
	protoapi.UnimplementedRandomServer
}

2. 서비스 메서드 작성

서버는 IDL에 기술된 세 가지 RPC 메서드인 GetDate, GetRandom, GetRandomPass를 각각 실제 동작 코드로 구현합니다.

  • GetDate: 현재 날짜와 시간 정보를 반환합니다.
  • GetRandom: 전달받은 시드(Seed)와 위치(Place) 매개변수를 이용해 의사 난수를 생성하고 반환합니다.
  • GetRandomPass: 지정된 길이(Length)의 무작위 문자열 비밀번호를 빌드하여 반환합니다.

3. gRPC 서버 시작 (ServerRun)

네트워크 포트 청취를 개시하고, gRPC 서버 객체를 인스턴스화한 후 서비스를 등록하여 대기 상태에 들어갑니다.

func ServerRun(addr string) {
	server := grpc.NewServer()
	var randomServer RandomServer
	protoapi.RegisterRandomServer(server, randomServer)

	// 외부 CLI 도구(예: grpcurl)의 디버깅을 위해 리플렉션 등록
	reflection.Register(server)

	listen, err := net.Listen("tcp", port)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println("Serving requests...")
	server.Serve(listen)
}

gRPC 클라이언트 개발

서버로 RPC 요청을 전송하고 결과를 출력하는 gRPC 클라이언트(client.go)의 흐름은 다음과 같습니다.

1. 서버 접속 채널 구축

클라이언트는 보안 자격 증명 옵션을 지정하여 서버 네트워크 주소로 커넥션을 생성합니다. (본 예제에서는 로컬 테스트용으로 insecure 자격 증명을 이용해 평문 채널을 구축합니다.)

conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))

2. 클라이언트 인스턴스 및 호출 함수 정의

채널 연결 완료 후 protoapi.NewRandomClient(conn)를 통해 클라이언트 객체를 생성하고, 개별 RPC 메서드들을 호출하는 래퍼 함수들을 정의해 서버에 값을 질의합니다.

  • AskingDateTime -> client.GetDate() 호출
  • AskPass -> client.GetRandomPass() 호출
  • AskRandom -> client.GetRandom() 호출

3. 실행 엔트리포인트 (ClientRun)

각 RPC 메서드를 호출하여 서버로부터 전달받은 시간 값, 난수 비밀번호, 무작위 난수들을 터미널 표준 출력(fmt.Println)으로 출력해 줍니다.