21 KiB
3단계: gRPC 통신 구현 상세 가이드
이 문서는 grpccanary 프로젝트의 3단계: gRPC 통신 구현에 대한 이론적 배경, 스키마 명세, 컴파일 기법 및 구체적인 소스코드 분석을 설명합니다.
gRPC는 HTTP/2를 기반으로 구축된 구글의 고성능 오픈소스 원격 프로시저 호출(RPC) 시스템입니다. 사물인터넷(IoT) 장비나 에이전트 간의 데이터 통신 시 가볍고 구조화된 데이터 통신을 유지하는 데에 가장 적합한 프레임워크입니다.
1. gRPC 개요 및 기술 배경
1.1 gRPC의 핵심 차별점
- 강력한 스키마 계약:
.proto파일 하나로 서비스 통신 규약을 명확히 선언하고, 컴파일 단계에서 이를 바탕으로 여러 언어의 클라이언트/서버 코드를 자동 생성합니다. 따라서 런타임 단계에서의 통신 필드 누락이나 타입 불일치 버그를 완벽하게 방지합니다. - 이진 프로토콜 (바이너리 포맷): 텍스트가 아닌 컴팩트한 이진 형식을 사용하므로 데이터 크기가 매우 작고 네트워크 대역폭 리소스 효율이 뛰어납니다.
- HTTP/2 기반: 하나의 네트워크 커넥션을 재사용해 다중화(Multiplexing) 전송이 가능하고, 실시간 스트리밍(양방향 스트리밍 포함) 서비스에 탁월한 환경을 제공합니다.
1.2 Protobuf (프로토콜 버퍼)의 장단점
-
장점
- 높은 전송 효율성: 데이터 교환 시 텍스트가 아닌 바이너리 인코딩 형식을 사용하므로 JSON에 비해 직렬화/역직렬화 속도가 매우 빠르고 크기도 가볍습니다.
- 일관성 있는 코드 생성 (Stub): 동일한 정의서로부터 다국어 API 클라이언트를 빌드하여 언어별 클라이언트 코드를 중복 작성해야 하는 오버헤드를 제거합니다.
- 하위 호환성: 고유 필드 번호 매핑 방식을 사용하므로 스키마가 개정되어도 이전 시스템과의 통신 호환을 보장합니다.
-
단점
- 가독성 부재: 패킷이 암호화는 아니지만 바이너리로 전달되므로 사람이 브라우저 개발자 도구 등으로 바로 읽어 디버깅하기 곤란합니다.
- 빌드 종속성: 명세 변경 시마다 Stub 코드를 컴파일하여 빌드에 바인딩하는 과정이 요구됩니다.
2. 실습 프로젝트 소개: IoT 센싱 데이터 수집 서비스
본 튜토리얼에서는 gRPC 분산 통신 기법을 실증적으로 학습하기 위해 현업에서 가장 범용적으로 쓰이는 가상의 IoT 센싱 데이터 수집 및 기기 관리 서비스 프로젝트를 직접 설계하고 구현해 나갑니다.
2.1 프로젝트 시나리오
사물인터넷(IoT) 센서 노드나 분산 멀티 에이전트 환경에서는 엣지 기기들이 중앙 서버에 접속해 통신 가능 여부를 검증하고 상태 정보(날짜/시간)를 수집하거나, 임시 보안 인증을 위한 비밀번호 발급을 요청하고, 실시간으로 센싱한 환경 정보(온도, 습도 등)를 지속적으로 업데이트해야 하는 현실적인 시나리오가 요구됩니다.
우리가 개발할 IoTService는 이에 대응하는 다음 3가지 핵심 원격 프로시저(RPC)를 수행합니다:
- 서버 시간 및 날짜 조회 (
GetDate): 기기가 접속 상태를 확인하며 동기화를 위해 서버의 현재 날짜와 시간 포맷 문자열을 반환받습니다. - 센싱 데이터 업데이트 (
UpdateSensingData): 센서 노드가 주기적으로 수집한 물리 데이터(온도, 습도) 및 기기 식별자(Device ID)를 전달하면, 서버는 데이터 정합성을 검증한 후 성공 여부를 반환합니다. - 일회성 보안 패스워드 발급 (
GetRandomPass): 기기가 임시 통신 세션 수립을 위해 난수 생성 시드와 보안 문자열 길이를 전달하면, 무작위 ASCII 임시 패스워드를 연산하여 응답받습니다.
2.2 학습 목표 및 진행 방법
이 유기적인 IoT 데이터 통신 모듈을 구축하는 실습을 통해 우리는 다음과 같은 gRPC의 핵심 개발 과정을 아주 쉽게 단계별로 마스터하게 됩니다:
- 1단계 - 통신 약속 문서 작성하기 (스키마 설계): 기기와 서버가 서로 어떤 형태로 데이터를 주고받을지
.proto라는 명세서 파일에 미리 정의해 둡니다. 이 약속을 바탕으로 데이터의 이름과 형태(예: 기기 ID는 문자열, 온도는 실수 등)를 컴파일 전에 확실하게 강제합니다. - 2단계 - 말을 알아듣는 코드 자동으로 만들기 (Stub 컴파일): 작성한 약속 문서를 컴파일러(
protoc)에 넣어주면, 네트워크 통신과 데이터 변환 처리를 담당하는 Go 언어 코드를 컴퓨터가 자동으로 만들어 줍니다. 이를 통해 개발자가 직접 복잡한 JSON 변환이나 소켓 통신 코드를 일일이 짤 필요가 없어집니다. - 3단계 - 실제로 서버와 기기를 연결하여 대화하기 (네트워크 구현): 자동으로 만들어진 코드를 바탕으로 실제 gRPC 서버를 실행하고, 클라이언트 기기가 서버에 접속하여 데이터(날짜 요청, 센싱 값 전송 등)를 실시간으로 주고받는 엔드투엔드(End-to-End) 통신 흐름을 완성합니다.
3. 인터페이스 명세서 (protoapi.proto)
3.1 인터페이스 명세서의 개념 및 도입 목적
인터페이스 명세서(Interface Description Document)란 시스템을 구성하는 상이한 노드나 기기들이 데이터를 어떠한 규격과 규약으로 상호 교환할지 사전에 조율하고 합의하여 기술해 둔 설계 문서입니다. gRPC 환경에서는 이를 프로토콜 버퍼(Protocol Buffers)의 .proto 파일 형식을 활용해 데이터 명세와 서비스를 통합 기술하는 IDL(Interface Description Language)로 구체화합니다.
인터페이스 명세서를 강제하여 설계하는 목적은 다음과 같습니다:
- 강력한 스키마 계약 강제(Schema Contract): 송수신할 데이터의 이름, 형태(타입) 및 지원 함수를 코드 레벨에서 선언적으로 봉인하여, 런타임 단계가 아닌 컴파일 단계에서 데이터 구조 불일치 오류를 완전히 원천 차단합니다.
- 이종 스택/다국어 간의 원활한 호환성: 하나의 명세서만을 공유하면, 번역 도구 체인을 통하여 Go, C++, Python, Java 등 서로 다른 소스 언어로 동작하는 클라이언트와 서버 기기들이 상호 간에 기술적 문맥 충돌 없이 원활하게 패킷 데이터를 해석하고 소통할 수 있게 유도합니다.
- 효율적인 패킹을 통한 대역폭 절약: JSON 등 텍스트 기반 통신 규약에 비해 바이너리 압축 포맷을 생성하는 메커니즘을 명세 단계에서 조율함으로써 패킷 용량을 극대화하여 낮추고, 분산 에이전트와 센싱 노드 간 통신 오버헤드를 경감합니다.
3.2 인터페이스 명세서 설계 가이드 (작성 요령)
- 사양 정의: 파일 최상단에
syntax = "proto3";스펙 버전을 선언하고, 대상 플랫폼에 맞는 패키지 출력 경로(go_package옵션 등)를 제공합니다. - 원격 프로시저 선언:
service블록을 구성하여 외부 단말이 호출을 제기할 수 있는 진입 함수군(RPC 메서드)을 정의하며, 입력과 반환값으로 매핑될 데이터 규격을 선언합니다. - 메시지 직렬화 필드 정의:
message블록을 선언하여 구조체 데이터를 정의합니다. 이때 필드값 뒤에 정의하는 식별자 번호(예:= 1,= 2)는 값을 대입하는 대입 연산자가 아니라, 컴퓨터가 데이터 직렬화 및 역직렬화 시 순서를 식별하는 **필드 번호 태그(Field Number Tag)**입니다. 한 번 릴리즈된 인터페이스의 태그 번호는 과거 하위 호환성을 보장하기 위하여 임의로 수정하거나 재사용해서는 안 되며, 확장 시 새로운 번호를 꼬리에 덧붙이는 전방향 호환성 설계를 준수해야 합니다.
3.3 명세서 소스코드 및 구체적 분석
저장소 루트에 선언된 protoapi.proto 명세서 코드는 앞서 기획한 IoTService 통신 구조를 수립하기 위해 다음과 같이 사양을 기재해 둡니다.
syntax = "proto3";
option go_package = "./protoapi/;protoapi";
// 엣지 센싱 단말과 중앙 관리 서버가 연동할 원격 호출(RPC) 창구 정의
service IoTService {
rpc GetDate (RequestDateTime) returns (DateTime);
rpc UpdateSensingData (SensingData) returns (SensingResponse);
rpc GetRandomPass (RequestPass) returns (RandomPass);
}
// 센싱 디바이스가 측정하여 전송할 환경 데이터 구조
message SensingData {
string DeviceId = 1; // 기기 식별 고유 일련번호
double Temperature = 2; // 수집된 대기 온도 센싱값
double Humidity = 3; // 수집된 상대 습도 센싱값
}
// 데이터 수신 완료 및 가공 결과를 통보하는 응답 영수증 구조
message SensingResponse {
bool Success = 1; // 트랜잭션 정상 반영 성공 여부
string Message = 2; // 상태 상세 정보 안내 메시지
}
message DateTime {
string Value = 1;
}
message RequestDateTime {
string Value = 2;
}
message RequestPass {
int64 Seed = 1;
int64 Length = 8;
}
message RandomPass {
string Password = 1;
}
명세 구조의 구성 분석
IoTService: 센서 단말이 중앙 관제소(서버)에 접근하여 실행 가능한 세 가지 원격 서비스의 인터페이스 스펙을 나타냅니다. 기기는 서버 시계를 연계 조회(GetDate)하거나, 측정값을 안전하게 전달(UpdateSensingData)하고, 암호 세션 비밀번호 발급(GetRandomPass)을 동기식으로 호출할 수 있습니다.SensingData: 실제 환경에 노출된 IoT 단말 기기의 정보를 포장하여 송신하기 위한 데이터 모델입니다. 기기 식별을 위한 식별자(DeviceId) 및 물리 센서 측정 변수(Temperature,Humidity)를 순서 번호 태그 1, 2, 3으로 매핑하여 순서가 흐트러지지 않도록 보장합니다.SensingResponse: 서버가 수신 데이터를 트랜잭션 처리한 결과를 다시 단말로 되돌려주기 위한 수신 회신 메커니즘입니다. 통신 처리 및 갱신의 안전한 성패 지표(Success) 및 원격 디버깅을 위한 가시적인 스트링 로그(Message)를 캡슐화해 줍니다.
3.4 직관적인 비유로 이해하는 명세서 동작 원리
인터페이스 명세서의 다소 생소한 기술적 개념들은 일상적인 요소에 빗대어 다음과 같이 쉽게 해석할 수 있습니다:
- 인터페이스 명세서의 실체 (통합 약속 장부):
- 센서 단말기는 저수준 임베디드 언어(예: C언어)를 사용하고, 중앙 서버는 고수준 언어(예: Go 언어)를 사용할 때, 두 기기는 첫인사부터 말이 통하지 않습니다. 이때 통신 규격이 적힌 약속 장부(
.proto파일)를 가운데에 공유함으로써, 기기와 서버가 동일한 기준으로 대화할 수 있는 소통 채널을 구축하게 됩니다.
- 센서 단말기는 저수준 임베디드 언어(예: C언어)를 사용하고, 중앙 서버는 고수준 언어(예: Go 언어)를 사용할 때, 두 기기는 첫인사부터 말이 통하지 않습니다. 이때 통신 규격이 적힌 약속 장부(
- 명세서를 선언해야 하는 이유 (오타 예방과 자동 코드 생성):
- 데이터 이름 불일치 차단: 송신 측은 기기 ID를
device_id로 보내고, 수신 측은deviceId로 처리하여 시스템이 오작동하는 사소한 오타 버그를 컴파일(빌드) 단계에서 완전히 차단합니다. - 자동 코드 생성: 명세서 장부 하나만 기재해 두면 Go, Python 등 원하는 언어의 네트워크 연동 소스코드를 번역기(
protoc)가 스스로 작성해 줍니다. 개발자가 일일이 수백 줄의 네트워크 통신 코드를 손으로 직접 짤 필요가 없어집니다.
- 데이터 이름 불일치 차단: 송신 측은 기기 ID를
- 메시지(Message)와 서비스(Service)의 구분 (규격 포장 상자와 기능 메뉴판):
service(기능 메뉴판): "본 서버 관제소에서는 시간 조회(GetDate)와 센서 보고(UpdateSensingData) 두 가지 기능 메뉴를 접수합니다" 하고 처리 가능한 통신 API 목록을 공시하는 것과 같습니다.message(규격 포장 상자): 전송할 데이터를 알맞게 담는 전용 포장 박스입니다. 상자 안에는 기기 ID, 온도, 습도라는 세 종류의 알맹이가 들어가도록 크기와 형식을 고정해 둡니다.
= 1,= 2식별자의 의미 (데이터의 고유 번호표):- 프로토콜 버퍼에서 필드 이름 뒤에 붙는
= 1,= 2는 변수에 특정 값을 집어넣는 대입 연산자가 아닙니다. - 통신 시 패킷에 매번
DeviceId나Temperature같은 구구절절한 문자열 이름표를 전부 태워 보내면 네트워크 대역폭이 낭비됩니다. 대신 데이터를 보낼 때 "1번 칸에는 기기 ID가 들어있고, 2번 칸에는 온도가 들어있다" 하고 짧은 번호표만 달아서 이진화(바이너리)하여 보냅니다. 수신 측 컴퓨터는 이 번호표만 보고 순서대로 데이터를 꺼내 해석하기 때문에 통신 오버헤드가 극적으로 줄어듭니다. 이미 가동 중인 이전 단말 기기들이 헷갈리지 않게, 한 번 지정한 번호표는 절대로 바꾸지 않고 유지하는 것이 설계의 절대 법칙입니다.
- 프로토콜 버퍼에서 필드 이름 뒤에 붙는
4. 말귀를 알아듣는 코드 변환기 준비 (Go Stub 컴파일 및 도구 체인)
우리가 열심히 기획해서 적은 .proto 약속 파일을 컴퓨터(Go 언어)가 알아듣는 소스코드로 변환해 줄 **번역기(protoc)**와 Go 전용 번역 플러그인들을 로컬 개발 환경에 설치하고 구동하는 방법입니다.
4.1 번역 도구 설치하기
- macOS (Homebrew 사용):
터미널에 아래 명령어를 입력해 컴파일러와 Go 언어 통신용 변환 플러그인을 설치합니다.
brew install protobuf go install google.golang.org/protobuf/cmd/protoc-gen-go@latest go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest - Linux (Ubuntu 기준):
패키지 관리자를 통해 컴파일러를 다운로드하고 마찬가지로 Go 플러그인을 환경에 바인딩합니다.
sudo apt install -y protobuf-compiler go install google.golang.org/protobuf/cmd/protoc-gen-go@latest go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
4.2 실제로 번역하기 (컴파일 명령어)
아래 명령어는 컴퓨터에게 "내가 작성한 protoapi.proto 약속 장부를 Go 언어로 번역해서 protoapi/ 폴더에 가지런히 넣어줘!" 하고 지시하는 마법의 명령어입니다:
protoc --go_out=. --go-grpc_out=. protoapi.proto
명령을 실행하고 나면 protoapi/ 폴더 하위에 다음 두 파일이 기분 좋게 생성됩니다:
protoapi.pb.go: 약속 문서에 적은 데이터(Message) 규격들을 Go 구조체로 변환해 놓은 파일입니다.protoapi_grpc.pb.go: 기기와 서버가 실제로 요청을 주고받을 수 있게 하는 통신 창구(Service) 함수가 자동 완성된 파일입니다.
5. 실습 소스코드의 속살 들여다보기 (구현 상세 분석)
번역기가 뼈대 코드를 만들어 주었으니, 이제 기기와 서버가 나눌 구체적인 대화의 내용을 채워 넣어 봅시다.
5.1 요청에 대답하는 서버 구현 (server.go)
- 대답 행동 대장 구조체 (
IoTServer):type IoTServer struct { protoapi.UnimplementedIoTServiceServer }UnimplementedIoTServiceServer를 품에 안은 구조체를 만듭니다. 이 친구는 "혹시 기기가 아직 구현되지 않은 통신 창구를 두드리더라도 서버가 뻗지 않고 조용히 '아직 준비 중입니다' 에러 대답을 돌려주도록" 든든하게 받쳐주는 안전 보디가드 역할을 해 줍니다. - 대답 채워 넣기 (
UpdateSensingData):기기가 온/습도 패킷을 들고 찾아오면, 서버 화면에 그 정보를 정답게 출력한 뒤 "이상 없이 잘 받았습니다!"라는 성공 영수증(func (IoTServer) UpdateSensingData(ctx context.Context, r *protoapi.SensingData) (*protoapi.SensingResponse, error) { fmt.Printf("Received sensing data - Device: %s, Temp: %.2f°C, Humid: %.2f%%\n", r.GetDeviceId(), r.GetTemperature(), r.GetHumidity()) return &protoapi.SensingResponse{Success: true, Message: "Sensing data updated successfully!"}, nil }SensingResponse)을 발급해 주는 역할을 기특하게 해내고 있습니다. - 서버 문 열기 (
ServerRun):지정된 문 번호(포트listen, _ := net.Listen("tcp", port) server.Serve(listen):8080)의 문을 활짝 열고, 기기들의 접속 요청을 다정히 기다리는 시작 지점입니다.
5.2 요청을 보내는 클라이언트 구현 (client.go)
- 전송 전용 기기 만들기 (
NewIoTServiceClient):서버로 통하는 통로(conn, _ := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials())) client := protoapi.NewIoTServiceClient(conn)conn)를 안전하게 확보하고, 그 길을 타고 데이터를 실어 보낼 전송 전용 클라이언트 기기를 조립해 내는 핵심 과정입니다. - 데이터 포장해서 보내기 (
AskUpdateSensingData):센서가 수집한 온/습도 정보를 예쁘게 상자에 담아 포장한 뒤 서버의request := &protoapi.SensingData{DeviceId: deviceId, Temperature: temp, Humidity: humid} return m.UpdateSensingData(ctx, request)UpdateSensingData창구로 쏘아 올립니다.
5.3 기기와 서버의 핑퐁 대화 흐름
실습 예제를 실행하면 서버와 클라이언트가 다음과 같이 대화를 나눕니다:
- 서버 시간 물어보기: 기기가 "지금 몇 시인가요?" 하고 문을 두드리면, 서버는 시스템의 현재 날짜와 시간 정보를 보기 좋게 반환해 줍니다.
- 비밀번호 생성: 기기가 시드값과 길이를 주면, 서버는 불규칙하게 글자들을 마구 섞어 일회용 보안 패스워드를 발급해 줍니다.
- 온습도 데이터 전송: 기기가 "현재 방 안 온도는 24.50°C이고 습도는 52.30%입니다!" 하고 소리치면, 서버는 이를 받아 화면에 출력하고 "데이터가 무사히 갱신되었습니다" 라고 기분 좋게 응답해 줍니다.
6. 개발하다 막혔을 때 찾아보는 해결사 가이드 (트러블슈팅)
실습을 진행하다가 갑작스레 에러를 마주했을 때 당황하지 않고 해결할 수 있는 가이드입니다.
6.1 bind: address already in use (문 번호가 꽉 막혔을 때)
- 원인: gRPC 서버를 켜려고 하는데 이미 다른 백그라운드 프로그램(혹은 덜 꺼진 이전 실습 서버)이 포트 번호
:8080을 꽉 쥐고 있어 문을 열지 못하는 상황입니다. - 해결 방법:
examples/main.go의port변수값과 server.go의port전역 변수값을 동시에 다른 번호(예::9090)로 변경하고 다시 실행해 보십시오.- 🚨 미묘한 함정:
server.go내부의ServerRun(addr string)함수는 외부 진입점으로부터 인자addr을 인가받지만, 실제 포트 리슨 코드에서는 이를 슬쩍 무시하고 자체 패키지 전역 변수port = ":8080"를 직접 읽어 처리하도록 하드코딩되어 있습니다. 따라서 정상적으로 포트를 바꾸기 위해서는 반드시server.go내부 전역 변수인port값을 수정해 주어야 포트 바인딩이 성공합니다.
7. 한 걸음 더 나아가기 (다음 단계)
본 기초 실습을 끝마치셨다면, 함께 학습하는 후배나 동료분들에게 아래와 같은 도전 과제들을 제안해 보십시오:
- 약속 스펙 확장해 보기: protoapi.proto 파일에 새로운 환경 데이터(예: 미세먼지 수치
double Dust = 4;)를 슬쩍 얹어본 뒤, 직접 번역기를 새로 돌리고 Go 소스코드를 고치며 확장해 봅니다. - 실전 분산 환경 상상하기: 수많은 자율 에이전트나 IoT 센서 단말이 하나의 gRPC 중앙 관제 서버로 동시에 데이터를 주고받는 분산 AIoT 멀티 에이전트 인프라로의 아이디어를 고민해 봅니다.
8. 참고 자료
- gRPC와 REST의 차이점 (AWS): 두 방식의 특징과 언제 어떤 기술을 선택해야 하는지 친절하게 정리된 공식 블로그 자료입니다.