263 lines
23 KiB
Markdown
263 lines
23 KiB
Markdown
# 3단계: gRPC 통신 구현 상세 가이드
|
|
|
|
⬅ [학습 로드맵으로 돌아가기](MANUSCRIPT.md)
|
|
|
|
이 문서는 `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`)
|
|
|
|
### 3.1 인터페이스 명세서의 개념 및 도입 목적
|
|
인터페이스 명세서(Interface Description Document)란 시스템을 구성하는 상이한 노드나 기기들이 데이터를 어떠한 규격과 규약으로 상호 교환할지 사전에 조율하고 합의하여 기술해 둔 설계 문서입니다. gRPC 환경에서는 이를 프로토콜 버퍼(Protocol Buffers)의 `.proto` 파일 형식을 활용해 데이터 명세와 서비스를 통합 기술하는 IDL(Interface Description Language)로 구체화합니다.
|
|
|
|
인터페이스 명세서를 강제하여 설계하는 목적은 다음과 같습니다:
|
|
1. **강력한 스키마 계약 강제(Schema Contract)**: 송수신할 데이터의 이름, 형태(타입) 및 지원 함수를 코드 레벨에서 선언적으로 봉인하여, 런타임 단계가 아닌 컴파일 단계에서 데이터 구조 불일치 오류를 완전히 원천 차단합니다.
|
|
2. **이종 스택/다국어 간의 원활한 호환성**: 하나의 명세서만을 공유하면, 번역 도구 체인을 통하여 Go, C++, Python, Java 등 서로 다른 소스 언어로 동작하는 클라이언트와 서버 기기들이 상호 간에 기술적 문맥 충돌 없이 원활하게 패킷 데이터를 해석하고 소통할 수 있게 유도합니다.
|
|
3. **효율적인 패킹을 통한 대역폭 절약**: JSON 등 텍스트 기반 통신 규약에 비해 바이너리 압축 포맷을 생성하는 메커니즘을 명세 단계에서 조율함으로써 패킷 용량을 극대화하여 낮추고, 분산 에이전트와 센싱 노드 간 통신 오버헤드를 경감합니다.
|
|
|
|
### 3.2 인터페이스 명세서 설계 가이드 (작성 요령)
|
|
* **사양 정의**: 파일 최상단에 `syntax = "proto3";` 스펙 버전을 선언하고, 대상 플랫폼에 맞는 패키지 출력 경로(`go_package` 옵션 등)를 제공합니다.
|
|
* **원격 프로시저 선언**: `service` 블록을 구성하여 외부 단말이 호출을 제기할 수 있는 진입 함수군(RPC 메서드)을 정의하며, 입력과 반환값으로 매핑될 데이터 규격을 선언합니다.
|
|
* **메시지 직렬화 필드 정의**: `message` 블록을 선언하여 구조체 데이터를 정의합니다. 이때 필드값 뒤에 정의하는 식별자 번호(예: `= 1`, `= 2`)는 값을 대입하는 대입 연산자가 아니라, 컴퓨터가 데이터 직렬화 및 역직렬화 시 순서를 식별하는 **필드 번호 태그(Field Number Tag)**입니다. 한 번 릴리즈된 인터페이스의 태그 번호는 과거 하위 호환성을 보장하기 위하여 임의로 수정하거나 재사용해서는 안 되며, 확장 시 새로운 번호를 꼬리에 덧붙이는 전방향 호환성 설계를 준수해야 합니다.
|
|
|
|
### 3.3 명세서 소스코드 및 구체적 분석
|
|
|
|
실습 디렉토리 내에 선언된 [protoapi.proto](../examples/grpcentity/protoapi.proto) 명세서 코드는 앞서 기획한 `IoTService` 통신 구조를 수립하기 위해 다음과 같이 사양을 기재해 둡니다.
|
|
|
|
```proto
|
|
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 직관적인 비유로 이해하는 명세서 동작 원리
|
|
|
|
인터페이스 명세서의 다소 생소한 기술적 개념들은 일상적인 요소에 빗대어 다음과 같이 쉽게 해석할 수 있습니다:
|
|
|
|
1. **인터페이스 명세서의 실체 (통합 약속 장부)**:
|
|
* 센서 단말기는 저수준 임베디드 언어(예: C언어)를 사용하고, 중앙 서버는 고수준 언어(예: Go 언어)를 사용할 때, 두 기기는 첫인사부터 말이 통하지 않습니다. 이때 통신 규격이 적힌 약속 장부(`.proto` 파일)를 가운데에 공유함으로써, 기기와 서버가 동일한 기준으로 대화할 수 있는 소통 채널을 구축하게 됩니다.
|
|
2. **명세서를 선언해야 하는 이유 (오타 예방과 자동 코드 생성)**:
|
|
* **데이터 이름 불일치 차단**: 송신 측은 기기 ID를 `device_id`로 보내고, 수신 측은 `deviceId`로 처리하여 시스템이 오작동하는 사소한 오타 버그를 컴파일(빌드) 단계에서 완전히 차단합니다.
|
|
* **자동 코드 생성**: 명세서 장부 하나만 기재해 두면 Go, Python 등 원하는 언어의 네트워크 연동 소스코드를 번역기(`protoc`)가 스스로 작성해 줍니다. 개발자가 일일이 수백 줄의 네트워크 통신 코드를 손으로 직접 짤 필요가 없어집니다.
|
|
3. **메시지(Message)와 서비스(Service)의 구분 (규격 포장 상자와 기능 메뉴판)**:
|
|
* **`service` (기능 메뉴판)**: "본 서버 관제소에서는 시간 조회(`GetDate`)와 센서 보고(`UpdateSensingData`) 두 가지 기능 메뉴를 접수합니다" 하고 처리 가능한 통신 API 목록을 공시하는 것과 같습니다.
|
|
* **`message` (규격 포장 상자)**: 전송할 데이터를 알맞게 담는 전용 포장 박스입니다. 상자 안에는 기기 ID, 온도, 습도라는 세 종류의 알맹이가 들어가도록 크기와 형식을 고정해 둡니다.
|
|
4. **`= 1`, `= 2` 식별자의 의미 (데이터의 고유 번호표)**:
|
|
* 프로토콜 버퍼에서 필드 이름 뒤에 붙는 `= 1`, `= 2`는 변수에 특정 값을 집어넣는 대입 연산자가 아닙니다.
|
|
* 통신 시 패킷에 매번 `DeviceId`나 `Temperature` 같은 구구절절한 문자열 이름표를 전부 태워 보내면 네트워크 대역폭이 낭비됩니다. 대신 데이터를 보낼 때 **"1번 칸에는 기기 ID가 들어있고, 2번 칸에는 온도가 들어있다"** 하고 짧은 번호표만 달아서 이진화(바이너리)하여 보냅니다. 수신 측 컴퓨터는 이 번호표만 보고 순서대로 데이터를 꺼내 해석하기 때문에 통신 오버헤드가 극적으로 줄어듭니다. 이미 가동 중인 이전 단말 기기들이 헷갈리지 않게, **한 번 지정한 번호표는 절대로 바꾸지 않고 유지**하는 것이 설계의 절대 법칙입니다.
|
|
|
|
---
|
|
|
|
## 4. 말귀를 알아듣는 코드 변환기 준비 (Go Stub 컴파일 및 도구 체인)
|
|
|
|
우리가 열심히 기획해서 적은 `.proto` 약속 파일을 컴퓨터(Go 언어)가 알아듣는 소스코드로 변환해 줄 **번역기(`protoc`)**와 **Go 전용 번역 플러그인**들을 로컬 개발 환경에 설치하고 구동하는 방법입니다.
|
|
|
|
### 4.1 핵심 도구 체인 삼각 편대 이해하기
|
|
gRPC 개발 환경을 구축하기 위해 설치하는 세 가지 프로그램의 구체적인 역할과 설치 목적은 다음과 같습니다:
|
|
|
|
1. **`protobuf-compiler` (또는 `protoc` 본체)**:
|
|
* **역할**: 프로토콜 버퍼의 코어 컴파일러 엔진 (**"번역기 본체"**)
|
|
* **목적**: `.proto` 문법 구조를 해석하고 분석하는 핵심 코어 본체입니다. 이 엔진 자체는 Go, Python 등 특정 프로그래밍 언어의 구체적인 소스 변환 지식은 없으며, 파싱한 데이터를 아래 기술되는 전용 플러그인(부품)에 전달하는 번역 지휘소 역할을 합니다.
|
|
2. **`protoc-gen-go` (Go 데이터 구조 금형틀)**:
|
|
* **역할**: Go 언어 전용 데이터 구조체 생성 플러그인 (**"구조체 조립 부품"**)
|
|
* **목적**: 명세서에 정의된 `message` 규격(예: `SensingData`)을 Go 프로그래밍 언어에서 바로 활용할 수 있도록 `struct`(구조체) 파일로 완전 자동 번역해 주는 부품입니다. (결과물: `*.pb.go` 파일 생성)
|
|
3. **`protoc-gen-go-grpc` (Go gRPC 통신망 금형틀)**:
|
|
* **역할**: Go 언어 전용 gRPC 통신 인터페이스 생성 플러그인 (**"네트워크 안테나 부착 부품"**)
|
|
* **목적**: 명세서에 선언된 `service` 규격(예: `IoTService` RPC)을 해석하여 기기와 서버가 실제로 네트워크 채널을 열고 연결하여 핑퐁 대화를 나눌 수 있게 통신 모듈 코드를 제공합니다. (결과물: `*_grpc.pb.go` 파일 생성)
|
|
|
|
### 4.2 번역 도구 설치하기
|
|
* **macOS (Homebrew 사용)**:
|
|
터미널에 아래 명령어를 입력해 컴파일러와 Go 언어 통신용 변환 플러그인을 설치합니다.
|
|
```bash
|
|
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 플러그인을 환경에 바인딩합니다.
|
|
```bash
|
|
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.3 실제로 번역하기 (컴파일 명령어)
|
|
아래 명령어는 컴퓨터에게 작성한 명세 문서를 바탕으로 실제 Go 컴파일 결과 코드를 얻어내기 위해 기동하는 명령어입니다:
|
|
```bash
|
|
# 실습 디렉터리로 이동한 후 컴파일 실행
|
|
cd examples/grpcentity
|
|
protoc --go_out=. --go-grpc_out=. protoapi.proto
|
|
```
|
|
|
|
#### 컴파일 옵션 세부 작동 원리
|
|
컴파일 시 사용하는 옵션들은 각 결과물 파일이 생성될 **개별 기준 디렉터리(Base Directory)**를 선언하는 지시자입니다:
|
|
* **`--go_out=.`**: 데이터 규격(Message) 번역본인 `protoapi.pb.go` 파일이 출력될 기준 위치를 현재 명령을 실행 중인 경로(`.`)로 지정합니다.
|
|
* **`--go-grpc_out=.`**: 네트워크 원격 함수(Service) 번역본인 `protoapi_grpc.pb.go` 파일이 출력될 기준 위치를 현재 실행 경로(`.`)로 지정합니다.
|
|
|
|
**🚨 Go 패키지 옵션과의 상호 결합 메커니즘**:
|
|
이 옵션들은 단독으로 최종 목적지를 결정하지 않습니다. 컴파일러는 지정된 기준 경로(예: `--go_out=.`)를 바탕으로, `.proto` 스키마 장부 내부에 선언된 `option go_package = "./protoapi;protoapi"` 의 세부 경로 정보를 읽어 들여 최종 병합합니다.
|
|
따라서 **기준 경로(`.`) + 상세 상대경로(`./protoapi`)**가 결합하여 현재 실행 디렉터리 하위에 `protoapi/` 폴더가 자동 생성되고, 그 안에 번역된 다음 두 핵심 Go 파일이 정상적으로 놓이게 됩니다:
|
|
* `protoapi.pb.go`: 약속 문서에 적은 데이터(Message) 규격들을 Go 구조체로 변환해 놓은 파일입니다.
|
|
* `protoapi_grpc.pb.go`: 기기와 서버가 실제로 요청을 주고받을 수 있게 하는 통신 창구(Service) 함수가 자동 완성된 파일입니다.
|
|
|
|
---
|
|
|
|
## 5. 실습 소스코드의 속살 들여다보기 (구현 상세 분석)
|
|
|
|
번역기가 뼈대 코드를 만들어 주었으니, 이제 기기와 서버가 나눌 구체적인 대화의 내용을 채워 넣어 봅시다.
|
|
|
|
### 5.1 요청에 대답하는 서버 구현 ([server.go](../examples/grpcentity/server.go))
|
|
|
|
* **대답 행동 대장 구조체 (`IoTServer`)**:
|
|
```go
|
|
type IoTServer struct {
|
|
protoapi.UnimplementedIoTServiceServer
|
|
}
|
|
```
|
|
`UnimplementedIoTServiceServer`를 품에 안은 구조체를 만듭니다. 이 친구는 **"혹시 기기가 아직 구현되지 않은 통신 창구를 두드리더라도 서버가 뻗지 않고 조용히 '아직 준비 중입니다' 에러 대답을 돌려주도록"** 든든하게 받쳐주는 안전 보디가드 역할을 해 줍니다.
|
|
* **대답 채워 넣기 (`UpdateSensingData`)**:
|
|
```go
|
|
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`)**:
|
|
```go
|
|
listen, _ := net.Listen("tcp", port)
|
|
server.Serve(listen)
|
|
```
|
|
지정된 문 번호(포트 `:8080`)의 문을 활짝 열고, 기기들의 접속 요청을 다정히 기다리는 시작 지점입니다.
|
|
|
|
### 5.2 요청을 보내는 클라이언트 구현 ([client.go](../examples/grpcentity/client.go))
|
|
|
|
* **전송 전용 기기 만들기 (`NewIoTServiceClient`)**:
|
|
```go
|
|
conn, _ := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
|
client := protoapi.NewIoTServiceClient(conn)
|
|
```
|
|
서버로 통하는 통로(`conn`)를 안전하게 확보하고, 그 길을 타고 데이터를 실어 보낼 **전송 전용 클라이언트 기기**를 조립해 내는 핵심 과정입니다.
|
|
* **데이터 포장해서 보내기 (`AskUpdateSensingData`)**:
|
|
```go
|
|
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.go`의 `port` 변수값과 [server.go](../examples/grpcentity/server.go)의 `port` 전역 변수값을 동시에 다른 번호(예: `:9090`)로 변경하고 다시 실행해 보십시오.
|
|
- **🚨 미묘한 함정**: `server.go` 내부의 `ServerRun(addr string)` 함수는 외부 진입점으로부터 인자 `addr`을 인가받지만, 실제 포트 리슨 코드에서는 이를 슬쩍 무시하고 자체 패키지 전역 변수 `port = ":8080"`를 직접 읽어 처리하도록 하드코딩되어 있습니다. 따라서 정상적으로 포트를 바꾸기 위해서는 반드시 `server.go` 내부 전역 변수인 `port` 값을 수정해 주어야 포트 바인딩이 성공합니다.
|
|
|
|
---
|
|
|
|
## 7. 한 걸음 더 나아가기 (다음 단계)
|
|
|
|
본 기초 실습을 끝마치셨다면, 함께 학습하는 후배나 동료분들에게 아래와 같은 도전 과제들을 제안해 보십시오:
|
|
1. **약속 스펙 확장해 보기**: [protoapi.proto](../examples/grpcentity/protoapi.proto) 파일에 새로운 환경 데이터(예: 미세먼지 수치 `double Dust = 4;`)를 슬쩍 얹어본 뒤, 직접 번역기를 새로 돌리고 Go 소스코드를 고치며 확장해 봅니다.
|
|
2. **실전 분산 환경 상상하기**: 수많은 자율 에이전트나 IoT 센서 단말이 하나의 gRPC 중앙 관제 서버로 동시에 데이터를 주고받는 분산 AIoT 멀티 에이전트 인프라로의 아이디어를 고민해 봅니다.
|
|
|
|
---
|
|
|
|
## 8. 참고 자료
|
|
|
|
* [gRPC와 REST의 차이점 (AWS)](https://aws.amazon.com/ko/compare/the-difference-between-grpc-and-rest/): 두 방식의 특징과 언제 어떤 기술을 선택해야 하는지 친절하게 정리된 공식 블로그 자료입니다.
|