187 lines
11 KiB
Markdown
187 lines
11 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 지식 체계를 단계별로 마스터하게 됩니다:
|
|
* **스키마 설계**: `.proto` IDL 문법을 활용해 데이터 형식(Message)과 원격 함수(Service RPC) 계약을 강제하는 법을 습득합니다.
|
|
* **Stub 컴파일**: `protoc` 도구 체인을 가동하여 Go 프로그래밍 언어 소스코드를 안전하게 생성하고 프로젝트 빌드에 바인딩하는 기법을 배웁니다.
|
|
* **네트워크 구현**: 실제로 TCP 소켓을 확보하여 gRPC 서버를 실행하고, 클라이언트가 평문 커넥션을 수립하여 실시간 동기식 원격 프로시저를 직접 기동하는 엔드투엔드 구동 구조를 체득합니다.
|
|
|
|
---
|
|
|
|
## 3. 인터페이스 명세서 (`protoapi.proto`)
|
|
|
|
저장소 루트의 [protoapi.proto](../protoapi.proto) 파일은 앞서 설계한 `IoTService`를 구축하기 위해 아래와 같이 사양을 선언해 둡니다.
|
|
|
|
```proto
|
|
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`)와 플러그인이 로컬에 갖추어져야 합니다.
|
|
|
|
### 4.1 OS별 설치 가이드
|
|
* **macOS**:
|
|
```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**:
|
|
```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.2 컴파일 실행 명령어
|
|
```bash
|
|
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. \
|
|
--go-grpc_opt=paths=source_relative protoapi.proto
|
|
```
|
|
실행 결과로 `protoapi/` 아래에 `protoapi.pb.go`(메시지 정의)와 `protoapi_grpc.pb.go`(인터페이스 및 원격 호출)가 자동 생성됩니다.
|
|
|
|
---
|
|
|
|
## 5. 실습 코드 구현 상세 분석
|
|
|
|
### 5.1 gRPC 서버 구현 ([server.go](../examples/grpcentity/server.go))
|
|
|
|
* **구조체 정의**:
|
|
```go
|
|
type IoTServer struct {
|
|
protoapi.UnimplementedIoTServiceServer
|
|
}
|
|
```
|
|
`UnimplementedIoTServiceServer`를 임베딩하여, 향후 메서드가 새로 추가되더라도 기존 서버가 빌드 에러 없이 최소한의 호환(unimplemented 에러 응답)을 가지게 강제합니다.
|
|
* **서버 기동 흐름**:
|
|
```go
|
|
server := grpc.NewServer()
|
|
var iotServer IoTServer
|
|
protoapi.RegisterIoTServiceServer(server, iotServer)
|
|
reflection.Register(server) // grpcurl 등 외부 디버깅 목적
|
|
listen, _ := net.Listen("tcp", port)
|
|
server.Serve(listen)
|
|
```
|
|
|
|
### 5.2 gRPC 클라이언트 구현 ([client.go](../examples/grpcentity/client.go))
|
|
|
|
* **연결 수립**:
|
|
```go
|
|
conn, _ := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
|
defer conn.Close()
|
|
client := protoapi.NewIoTServiceClient(conn)
|
|
```
|
|
`insecure.NewCredentials()`를 전달하여 TLS를 건너뛴 채 평문으로 빠르고 간단한 로컬 테스트 환경을 구축합니다.
|
|
* **원격 호출**:
|
|
`client.GetDate()`, `client.GetRandomPass()`, `client.UpdateSensingData()`를 차례로 호출하여 매개변수와 결과를 콘솔로 확인합니다.
|
|
|
|
### 5.3 gRPC 실습 예제 동작 흐름
|
|
예제가 구동되면 서버와 클라이언트 간에 다음과 같은 호출 시퀀스가 순차적으로 실행됩니다:
|
|
1. **날짜 조회 (`GetDate`)**: 클라이언트가 서버에 날짜 조회를 요청하고, 서버는 자체의 현재 날짜와 시간 문자열을 포맷하여 반환합니다.
|
|
2. **비밀번호 생성 (`GetRandomPass`)**: 클라이언트가 생성할 무작위 비밀번호의 길이(기본 8자)와 난수 생성 시드값을 전달하면, 서버는 지정된 사양의 임의 문자열을 작성해 반환합니다.
|
|
3. **센싱 데이터 업데이트 (`UpdateSensingData`)**: 클라이언트가 기기 ID("sensor-room-01")와 수집한 온/습도 환경 변수를 전달하면, 서버는 이를 수신하여 화면에 상세 센싱 값을 출력하고 업데이트 완료 성공 응답을 전송합니다.
|
|
|
|
---
|
|
|
|
## 6. 트러블슈팅 (Troubleshooting)
|
|
|
|
gRPC 서버 및 클라이언트 실습 과정에서 직면할 수 있는 대표적인 문제와 해결 방안입니다.
|
|
|
|
### 6.1 `listen tcp :8080: bind: address already in use`
|
|
* **원인**: 포트 `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. 다음 단계 (Next Steps)
|
|
|
|
gRPC 통신 방식을 한층 더 깊이 탐구해 보려면 다음과 같은 후속 실습을 추천합니다:
|
|
1. **메시지 스펙 확장**: 루트 디렉토리의 [protoapi.proto](../protoapi.proto)에 새로운 필드를 추가하거나 메서드를 정의한 뒤, Stub을 재컴파일([examples/grpcentity/README.md](../examples/grpcentity/README.md) 컴파일 가이드 참고)해 보십시오.
|
|
2. **인터페이스 모듈 연계**: 생성된 gRPC 클라이언트 및 서버 stub 인터페이스를 활용하여, 향후 분산 AIoT 환경에서의 센서 데이터 수집이나 에이전트 간 제어 메시지 전송 로직을 설계해 보십시오.
|
|
|
|
---
|
|
|
|
## 8. 참고 자료
|
|
|
|
* [gRPC와 REST의 차이점 (AWS)](https://aws.amazon.com/ko/compare/the-difference-between-grpc-and-rest/)
|