docs: revise GRPC.md and ASKING_DATETIME_WORKFLOW.md through multi-agent-mux-loop

This commit is contained in:
2026-07-17 18:00:55 +09:00
parent 4697f7e069
commit 558f17eeed
3 changed files with 56 additions and 30 deletions
+46 -30
View File
@@ -10,6 +10,8 @@ gRPC는 HTTP/2를 기반으로 구축된 구글의 고성능 오픈소스 원격
## 1. gRPC 개요 및 기술 배경
쉽게 말해 gRPC는 **다른 컴퓨터에 있는 함수를, 마치 내 코드 안에 있는 함수처럼 그냥 호출할 수 있게 해주는 기술**입니다. 평소 `sum(1, 2)`처럼 내 프로그램 안의 함수를 부르듯, `client.GetDate()`라고 쓰면 실제로는 저 멀리 다른 컴퓨터(서버)의 함수가 실행되고 그 결과를 돌려받는 것입니다. 이런 '원격에 있는 함수를 부르는 방식'을 원격 프로시저 호출(RPC)이라고 부릅니다.
### 1.1 gRPC의 핵심 차별점
* **강력한 스키마 계약**: `.proto` 파일 하나로 서비스 통신 규약을 명확히 선언하고, 컴파일 단계에서 이를 바탕으로 여러 언어의 클라이언트/서버 코드를 자동 생성합니다. 따라서 런타임 단계에서의 통신 필드 누락이나 타입 불일치 버그를 완벽하게 방지합니다.
* **이진 프로토콜 (바이너리 포맷)**: 텍스트가 아닌 컴팩트한 이진 형식을 사용하므로 데이터 크기가 매우 작고 네트워크 대역폭 리소스 효율이 뛰어납니다.
@@ -49,7 +51,23 @@ gRPC는 HTTP/2를 기반으로 구축된 구글의 고성능 오픈소스 원격
## 3. 인터페이스 명세서 (`protoapi.proto`)
### 3.1 인터페이스 명세서의 개념 및 도입 목적
### 3.1 직관적인 비유로 이해하는 명세서 동작 원리
인터페이스 명세서의 다소 생소한 기술적 개념들은 일상적인 요소에 빗대어 다음과 같이 쉽게 해석할 수 있습니다:
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번 칸에는 온도가 들어있다"** 하고 짧은 번호표만 달아서 이진화(바이너리)하여 보냅니다. 수신 측 컴퓨터는 이 번호표만 보고 순서대로 데이터를 꺼내 해석하기 때문에 통신 오버헤드가 극적으로 줄어듭니다. 이미 가동 중인 이전 단말 기기들이 헷갈리지 않게, **한 번 지정한 번호표는 절대로 바꾸지 않고 유지**하는 것이 설계의 절대 법칙입니다.
### 3.2 인터페이스 명세서의 개념 및 도입 목적
인터페이스 명세서(Interface Description Document)란 시스템을 구성하는 상이한 노드나 기기들이 데이터를 어떠한 규격과 규약으로 상호 교환할지 사전에 조율하고 합의하여 기술해 둔 설계 문서입니다. gRPC 환경에서는 이를 프로토콜 버퍼(Protocol Buffers)의 `.proto` 파일 형식을 활용해 데이터 명세와 서비스를 통합 기술하는 IDL(Interface Description Language)로 구체화합니다.
인터페이스 명세서를 강제하여 설계하는 목적은 다음과 같습니다:
@@ -57,12 +75,12 @@ gRPC는 HTTP/2를 기반으로 구축된 구글의 고성능 오픈소스 원격
2. **이종 스택/다국어 간의 원활한 호환성**: 하나의 명세서만을 공유하면, 번역 도구 체인을 통하여 Go, C++, Python, Java 등 서로 다른 소스 언어로 동작하는 클라이언트와 서버 기기들이 상호 간에 기술적 문맥 충돌 없이 원활하게 패킷 데이터를 해석하고 소통할 수 있게 유도합니다.
3. **효율적인 패킹을 통한 대역폭 절약**: JSON 등 텍스트 기반 통신 규약에 비해 바이너리 압축 포맷을 생성하는 메커니즘을 명세 단계에서 조율함으로써 패킷 용량을 극대화하여 낮추고, 분산 에이전트와 센싱 노드 간 통신 오버헤드를 경감합니다.
### 3.2 인터페이스 명세서 설계 가이드 (작성 요령)
### 3.3 인터페이스 명세서 설계 가이드 (작성 요령)
* **사양 정의**: 파일 최상단에 `syntax = "proto3";` 스펙 버전을 선언하고, 대상 플랫폼에 맞는 패키지 출력 경로(`go_package` 옵션 등)를 제공합니다.
* **원격 프로시저 선언**: `service` 블록을 구성하여 외부 단말이 호출을 제기할 수 있는 진입 함수군(RPC 메서드)을 정의하며, 입력과 반환값으로 매핑될 데이터 규격을 선언합니다.
* **메시지 직렬화 필드 정의**: `message` 블록을 선언하여 구조체 데이터를 정의합니다. 이때 필드값 뒤에 정의하는 식별자 번호(예: `= 1`, `= 2`)는 값을 대입하는 대입 연산자가 아니라, 컴퓨터가 데이터 직렬화 및 역직렬화 시 순서를 식별하는 **필드 번호 태그(Field Number Tag)**입니다. 한 번 릴리즈된 인터페이스의 태그 번호는 과거 하위 호환성을 보장하기 위하여 임의로 수정하거나 재사용해서는 안 되며, 확장 시 새로운 번호를 꼬리에 덧붙이는 전방향 호환성 설계를 준수해야 합니다.
### 3.3 명세서 소스코드 및 구체적 분석
### 3.4 명세서 소스코드 및 구체적 분석
실습 디렉토리 내에 선언된 [protoapi.proto](../examples/grpcentity/protoapi.proto) 명세서 코드는 앞서 기획한 `IoTService` 통신 구조를 수립하기 위해 다음과 같이 사양을 기재해 둡니다.
@@ -114,22 +132,6 @@ message RandomPass {
* **`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 컴파일 및 도구 체인
@@ -137,6 +139,9 @@ message RandomPass {
작성된 `.proto` 명세 파일을 Go 언어 소스코드로 변환하기 위해 프로토콜 버퍼 컴파일러(`protoc`)와 Go 전용 플러그인을 로컬 개발 환경에 구성하는 가이드라인입니다.
### 4.1 핵심 도구 체인 구성과 기능 개요
`.proto` 파일이 '약속 장부'라면, 이 3가지 도구는 그 장부를 실제 프로그램이 알아듣는 Go 언어 코드로 옮겨주는 '번역팀'입니다. `protoc`은 장부를 읽고 이해하는 통역사, `protoc-gen-go`는 장부 속 데이터 모양(message)을 Go 구조체로 옮겨 적는 필경사, `protoc-gen-go-grpc`는 장부 속 기능 목록(service)을 실제로 호출 가능한 Go 함수 뼈대로 옮겨 적는 필경사입니다.
gRPC 빌드 및 코드 생성 환경을 구축하기 위해 사용되는 세 가지 핵심 바이너리의 세부 역할은 다음과 같습니다:
1. **`protobuf-compiler` (또는 `protoc` 본체)**:
@@ -199,7 +204,15 @@ protoc --go_out=. --go-grpc_out=. protoapi.proto
protoapi.UnimplementedIoTServiceServer
}
```
`UnimplementedIoTServiceServer`를 임베딩하여 정의합니다. 이는 향후 새로운 메서드가 프로토콜 스펙에 추가되더라도 기존 서버가 빌드 오류 없이 구동 호환성(미구현 메서드 호출 시 unimplemented 에러 반환)을 안전하게 유지하도록 제약하기 위한 설정입니다.
* **쉬운 설명**: 이 구조체는 '나는 IoTService가 약속한 기능들을 구현하는 서버입니다'라고 선언하는 역할을 합니다. **Go 언어의 gRPC 규칙상 이 구절(`Unimplemented...`)을 빼놓으면 서버가 아예 컴파일(빌드)되지 않고 에러가 발생하므로, '있으면 좋은 것'이 아니라 반드시 그대로 넣어주어야 하는 필수 구성 요소입니다.** (이렇게 넣어두면 부수적으로, 나중에 약속 장부에 새 기능이 추가되도 기존 서버 코드가 빌드 오류 없이 구동되는 효과도 함께 얻습니다.)
* **Q. 만약 이 줄(`protoapi.UnimplementedIoTServiceServer`)을 지우면 어떻게 되나요?**
gRPC가 자동 생성한 인터페이스와의 호환성이 깨져 Go 컴파일러가 아래와 같은 에러를 내며 빌드를 거부합니다:
```text
cannot use IoTServer{} (value of type IoTServer) as protoapi.IoTServiceServer in argument to protoapi.RegisterIoTServiceServer:
IoTServer does not implement protoapi.IoTServiceServer (missing method mustEmbedUnimplementedIoTServiceServer)
```
따라서 나중에 메서드를 더 추가할 일이 없더라도, gRPC 서버의 정상적인 구동을 위해 반드시 포함시켜야 합니다.
* **상세 설명**: `UnimplementedIoTServiceServer`를 임베딩하여 정의합니다. 이는 향후 새로운 메서드가 프로토콜 스펙에 추가되더라도 기존 서버가 빌드 오류 없이 구동 호환성(미구현 메서드 호출 시 unimplemented 에러 반환)을 안전하게 유지하도록 제약하기 위한 설정입니다.
* **센싱 데이터 업데이트 수신 처리 (`UpdateSensingData`)**:
```go
func (IoTServer) UpdateSensingData(ctx context.Context, r *protoapi.SensingData) (*protoapi.SensingResponse, error) {
@@ -207,10 +220,11 @@ protoc --go_out=. --go-grpc_out=. protoapi.proto
return &protoapi.SensingResponse{Success: true, Message: "Sensing data updated successfully!"}, nil
}
```
클라이언트 디바이스로부터 센싱 패킷을 수신하면, 기기 식별 및 온습도 측정 매개변수를 포맷팅하여 표준 출력에 표시한 후 정상 처리 완료 플래그를 담은 응답 구조체(`SensingResponse`)를 반환합니다.
* **쉬운 설명**: 기기(클라이언트)가 온습도 데이터를 전송해 왔을 때, 서버 콘솔 화면에 이를 예쁘게 출력한 뒤 "성공적으로 업데이트되었습니다"라는 확인 영수증(`SensingResponse`)을 만들어 돌려주는 실제 서비스 동작 부위입니다.
* **상세 설명**: 클라이언트 디바이스로부터 센싱 패킷을 수신하면, 기기 식별 및 온습도 측정 매개변수를 포맷팅하여 표준 출력에 표시한 후 정상 처리 완료 플래그를 담은 응답 구조체(`SensingResponse`)를 반환합니다.
> [!NOTE]
> **기술 분석: 왜 Set 메서드는 없고 Get 메서드만 제공되는가? (Go와 Protobuf의 설계 철학)**
> **(심화) 기술 분석: 왜 Set 메서드는 없고 Get 메서드만 제공되는가? (Go와 Protobuf의 설계 철학)**
>
> 자동으로 생성된 Go 소스코드를 들여다보면, 필드 데이터를 추출하는 `r.GetDeviceId()` 메서드는 존재하나 값을 쓰는 `r.SetDeviceId()` 메서드는 설계 단계에서 배제되어 있습니다. 이 비대칭 구조에는 아래와 같은 언어 및 아키텍처적 지향점이 투영되어 있습니다:
>
@@ -223,7 +237,8 @@ protoc --go_out=. --go-grpc_out=. protoapi.proto
listen, _ := net.Listen("tcp", port)
server.Serve(listen)
```
지정된 포트(기본 포트 `:8080`)의 TCP 소켓 포트를 활성화하고, 클라이언트의 접속 및 RPC 서비스 호출에 대해 지속적으로 대기하는 리스너 구동의 진입점입니다.
* **쉬운 설명**: `:8080` 포트로 통하는 소켓(전화선)을 개통하고, 기기들의 전화(접속 및 호출)가 오기를 기다리며 대기 상태로 들어가는 서버 구동 시작점입니다.
* **상세 설명**: 지정된 포트(기본 포트 `:8080`)의 TCP 소켓 포트를 활성화하고, 클라이언트의 접속 및 RPC 서비스 호출에 대해 지속적으로 대기하는 리스너 구동의 진입점입니다.
### 5.2 gRPC 클라이언트 구현 분석 ([client.go](../examples/grpcentity/client.go))
@@ -232,13 +247,15 @@ protoc --go_out=. --go-grpc_out=. protoapi.proto
conn, _ := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
client := protoapi.NewIoTServiceClient(conn)
```
서버와의 TCP 채널(`conn`)을 평문 전송(insecure) 기반으로 바인딩한 뒤, 해당 채널을 통해 원격 서비스를 호출할 수 있는 전용 클라이언트 인스턴스를 확보합니다.
* **쉬운 설명**: 서버 주소로 전화를 거는 통신선(TCP 채널)을 안전 보안(TLS) 없이 개설한 뒤, 이 선을 통해 gRPC 약속 장부(`IoTService`)대로 서버에 원격 호출을 요청할 수 있는 전용 전화기(클라이언트 인스턴스)획득하는 과정입니다.
* **상세 설명**: 서버와의 TCP 채널(`conn`)을 평문 전송(insecure) 기반으로 바인딩한 뒤, 해당 채널을 통해 원격 서비스를 호출할 수 있는 전송용 클라이언트 인스턴스를 확보합니다.
* **패킷 구성 및 RPC 호출 실행 (`AskUpdateSensingData`)**:
```go
request := &protoapi.SensingData{DeviceId: deviceId, Temperature: temp, Humidity: humid}
return m.UpdateSensingData(ctx, request)
```
센서 측정값을 메시지 스펙 규격에 맞추어 `SensingData` 구조체 인스턴스로 바인딩한 후, 기설정된 클라이언트 인터페이스를 경유하여 서버의 `UpdateSensingData` 엔드포인트를 호출합니다.
* **쉬운 설명**: 온습도 데이터 상자(`SensingData`)를 접어서 기기 번호와 센서값을 가지런히 담은 뒤, 전용 전화기(클라이언트 인터페이스)통해 서버의 `UpdateSensingData` 기능을 직접 원격 실행(호출)하는 부분입니다.
* **상세 설명**: 센서 측정값을 메시지 스펙 규격에 맞추어 `SensingData` 구조체 인스턴스로 바인딩한 후, 기설정된 클라이언트 인터페이스를 경유하여 서버의 `UpdateSensingData` 엔드포인트를 호출합니다.
### 5.3 통신 세션 동작 시퀀스 및 흐름
실습 예제를 기동하면 클라이언트와 서버 간에 아래와 같은 동작 흐름이 순차적으로 실행됩니다:
@@ -247,8 +264,8 @@ protoc --go_out=. --go-grpc_out=. protoapi.proto
3. **환경 수집 센싱 데이터 동기화**: 클라이언트가 기기 식별값("sensor-room-01")과 가상의 온습도 변수를 실어 `UpdateSensingData`를 기동하면, 서버는 수신 값을 화면에 검증 및 로깅한 후 완료 보고 영수증을 반환합니다.
> [!TIP]
> **상세 네트워크 시퀀스 및 엔진 내부 동작 분석**:
> 클라이언트의 `AskingDateTime()` 호출을 시작으로 gRPC 네트워크 프레임 전송 및 직렬화/역직렬화가 수반되는 상세한 동작 시퀀스 다이어그램과 단계별 라이프사이클은 [ASKING_DATETIME_WORKFLOW.md](ASKING_DATETIME_WORKFLOW.md) 문서에서 추가적으로 확인할 수 있습니다.
> **(선택 학습) 상세 네트워크 시퀀스 및 엔진 내부 동작 분석**:
> 기본 실습 과정을 모두 마치고 gRPC 내부에서 네트워크 패킷이 구체적으로 어떻게 직렬화되고 HTTP/2 프레임으로 오가는지 깊이 알고 싶다면, [ASKING_DATETIME_WORKFLOW.md](ASKING_DATETIME_WORKFLOW.md) 문서에서 상세한 동작 시퀀스 다이어그램과 라이프사이클 분석 자료를 선택적으로 확인할 수 있습니다.
---
@@ -266,9 +283,8 @@ protoc --go_out=. --go-grpc_out=. protoapi.proto
## 7. 한 걸음 더 나아가기 (다음 단계)
본 기초 실습을 끝마치셨다면, 함께 학습하는 후배나 동료분들에게 아래와 같은 도전 과제들을 제안해 보십시오:
1. **약속 스펙 확장해 보기**: [protoapi.proto](../examples/grpcentity/protoapi.proto) 파일에 새로운 환경 데이터(예: 미세먼지 수치 `double Dust = 4;`)를 슬쩍 얹어본 뒤, 직접 번역기를 새로 돌리고 Go 소스코드를 고치며 확장해 봅니다.
2. **실전 분산 환경 상상하기**: 수많은 자율 에이전트나 IoT 센서 단말이 하나의 gRPC 중앙 관제 서버로 동시에 데이터를 주고받는 분산 AIoT 멀티 에이전트 인프라로의 아이디어를 고민해 봅니다.
본 기초 실습을 끝마치셨다면, 아래 과제를 해결해보세요:
**약속 스펙 확장해 보기**: [protoapi.proto](../examples/grpcentity/protoapi.proto) 파일에 새로운 환경 데이터(예: 미세먼지 수치 `double Dust = 4;`)를 슬쩍 얹어본 뒤, 직접 번역기를 새로 돌리고 Go 소스코드를 고치며 확장해 봅니다.
---