Files
grpccanary/docs/GRPC.md
T

14 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)를 수행합니다:

  1. 서버 시간 및 날짜 조회 (GetDate): 기기가 접속 상태를 확인하며 동기화를 위해 서버의 현재 날짜와 시간 포맷 문자열을 반환받습니다.
  2. 센싱 데이터 업데이트 (UpdateSensingData): 센서 노드가 주기적으로 수집한 물리 데이터(온도, 습도) 및 기기 식별자(Device ID)를 전달하면, 서버는 데이터 정합성을 검증한 후 성공 여부를 반환합니다.
  3. 일회성 보안 패스워드 발급 (GetRandomPass): 기기가 임시 통신 세션 수립을 위해 난수 생성 시드와 보안 문자열 길이를 전달하면, 무작위 ASCII 임시 패스워드를 연산하여 응답받습니다.

2.2 학습 목표 및 진행 방법

이 유기적인 IoT 데이터 통신 모듈을 구축하는 실습을 통해 우리는 다음과 같은 gRPC의 핵심 개발 과정을 아주 쉽게 단계별로 마스터하게 됩니다:

  • 1단계 - 통신 약속 문서 작성하기 (스키마 설계): 기기와 서버가 서로 어떤 형태로 데이터를 주고받을지 .proto라는 명세서 파일에 미리 정의해 둡니다. 이 약속을 바탕으로 데이터의 이름과 형태(예: 기기 ID는 문자열, 온도는 실수 등)를 컴파일 전에 확실하게 강제합니다.
  • 2단계 - 말을 알아듣는 코드 자동으로 만들기 (Stub 컴파일): 작성한 약속 문서를 컴파일러(protoc)에 넣어주면, 네트워크 통신과 데이터 변환 처리를 담당하는 Go 언어 코드를 컴퓨터가 자동으로 만들어 줍니다. 이를 통해 개발자가 직접 복잡한 JSON 변환이나 소켓 통신 코드를 일일이 짤 필요가 없어집니다.
  • 3단계 - 실제로 서버와 기기를 연결하여 대화하기 (네트워크 구현): 자동으로 만들어진 코드를 바탕으로 실제 gRPC 서버를 실행하고, 클라이언트 기기가 서버에 접속하여 데이터(날짜 요청, 센싱 값 전송 등)를 실시간으로 주고받는 엔드투엔드(End-to-End) 통신 흐름을 완성합니다.

3. 인터페이스 명세서 (protoapi.proto)

저장소 루트의 protoapi.proto 파일은 앞서 설계한 IoTService를 구축하기 위해 아래와 같이 사양을 선언해 둡니다.

syntax = "proto3";

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

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;
}

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 기기와 서버의 핑퐁 대화 흐름

실습 예제를 실행하면 서버와 클라이언트가 다음과 같이 대화를 나눕니다:

  1. 서버 시간 물어보기: 기기가 "지금 몇 시인가요?" 하고 문을 두드리면, 서버는 시스템의 현재 날짜와 시간 정보를 보기 좋게 반환해 줍니다.
  2. 비밀번호 생성: 기기가 시드값과 길이를 주면, 서버는 불규칙하게 글자들을 마구 섞어 일회용 보안 패스워드를 발급해 줍니다.
  3. 온습도 데이터 전송: 기기가 "현재 방 안 온도는 24.50°C이고 습도는 52.30%입니다!" 하고 소리치면, 서버는 이를 받아 화면에 출력하고 "데이터가 무사히 갱신되었습니다" 라고 기분 좋게 응답해 줍니다.

6. 개발하다 막혔을 때 찾아보는 해결사 가이드 (트러블슈팅)

실습을 진행하다가 갑작스레 에러를 마주했을 때 당황하지 않고 해결할 수 있는 가이드입니다.

6.1 bind: address already in use (문 번호가 꽉 막혔을 때)

  • 원인: gRPC 서버를 켜려고 하는데 이미 다른 백그라운드 프로그램(혹은 덜 꺼진 이전 실습 서버)이 포트 번호 :8080을 꽉 쥐고 있어 문을 열지 못하는 상황입니다.
  • 해결 방법:
    • examples/main.goport 변수값과 server.goport 전역 변수값을 동시에 다른 번호(예: :9090)로 변경하고 다시 실행해 보십시오.
    • 🚨 미묘한 함정: server.go 내부의 ServerRun(addr string) 함수는 외부 진입점으로부터 인자 addr을 인가받지만, 실제 포트 리슨 코드에서는 이를 슬쩍 무시하고 자체 패키지 전역 변수 port = ":8080"를 직접 읽어 처리하도록 하드코딩되어 있습니다. 따라서 정상적으로 포트를 바꾸기 위해서는 반드시 server.go 내부 전역 변수인 port 값을 수정해 주어야 포트 바인딩이 성공합니다.

7. 한 걸음 더 나아가기 (다음 단계)

본 기초 실습을 끝마치셨다면, 함께 학습하는 후배나 동료분들에게 아래와 같은 도전 과제들을 제안해 보십시오:

  1. 약속 스펙 확장해 보기: protoapi.proto 파일에 새로운 환경 데이터(예: 미세먼지 수치 double Dust = 4;)를 슬쩍 얹어본 뒤, 직접 번역기를 새로 돌리고 Go 소스코드를 고치며 확장해 봅니다.
  2. 실전 분산 환경 상상하기: 수많은 자율 에이전트나 IoT 센서 단말이 하나의 gRPC 중앙 관제 서버로 동시에 데이터를 주고받는 분산 AIoT 멀티 에이전트 인프라로의 아이디어를 고민해 봅니다.

8. 참고 자료

  • gRPC와 REST의 차이점 (AWS): 두 방식의 특징과 언제 어떤 기술을 선택해야 하는지 친절하게 정리된 공식 블로그 자료입니다.