docs: reform tone of GRPC.md into objective textbook style

This commit is contained in:
2026-07-17 16:48:30 +09:00
parent 62a50dd760
commit d35620bae2
+52 -51
View File
@@ -132,24 +132,24 @@ message RandomPass {
--- ---
## 4. 말귀를 알아듣는 코드 변환기 준비 (Go Stub 컴파일 및 도구 체인) ## 4. Go Stub 컴파일 및 도구 체인
우리가 열심히 기획해서 적은 `.proto` 약속 파일을 컴퓨터(Go 언어)가 알아듣는 소스코드로 변환해 줄 **번역기(`protoc`)**와 **Go 전용 번역 플러그인**들을 로컬 개발 환경에 설치하고하는 방법입니다. 작성된 `.proto` 명세 파일을 Go 언어 소스코드로 변환하기 위해 프로토콜 버퍼 컴파일러(`protoc`)Go 전용 플러그인을 로컬 개발 환경에 구하는 가이드라인입니다.
### 4.1 핵심 도구 체인 삼각 편대 이해하기 ### 4.1 핵심 도구 체인 구성과 기능 개요
gRPC 개발 환경을 구축하기 위해 설치하는 세 가지 프로그램의 구체적인 역할과 설치 목적은 다음과 같습니다: gRPC 빌드 및 코드 생성 환경을 구축하기 위해 사용되는 세 가지 핵심 바이너리의 세부 역할은 다음과 같습니다:
1. **`protobuf-compiler` (또는 `protoc` 본체)**: 1. **`protobuf-compiler` (또는 `protoc` 본체)**:
* **역할**: 프로토콜 버퍼 코어 컴파일러 엔진 (**"번역기 본체"**) * **역할**: 프로토콜 버퍼 코어 컴파일러 엔진
* **목적**: `.proto` 문법 구조를 해석하고 분석하는 핵심 코어 본체입니다. 이 엔진 자체는 Go, Python 등 특정 프로그래밍 언어의 구체적인 소스 변환 지식은 없으며, 파싱한 데이터를 아래 기술되는 전용 플러그인(부품)에 전달하는 번역 지휘소 역할을 합니다. * **목적**: `.proto` 파일의 스키마 명세를 구문 분석하는 역할을 수행합니다. 특정 프로그래밍 언어에 의존하지 않는 공통 파서 역할을 하며, 생성된 구문 분석 정보를 아래의 언어별 플러그인 모듈에 위임하여 타깃 코드를 출력하도록 제어합니다.
2. **`protoc-gen-go` (Go 데이터 구조 금형틀)**: 2. **`protoc-gen-go` (Go 데이터 직렬화 플러그인)**:
* **역할**: Go 언어 전용 데이터 구조체 생성 플러그인 (**"구조체 조립 부품"**) * **역할**: Go 구조체 및 직렬화 소스코드 생성 플러그인
* **목적**: 명세서에 정의`message` 규격(예: `SensingData`)을 Go 프로그래밍 언어에서 바로 활용할 수 있도록 `struct`(구조체) 파일로 완전 자동 번역해 주는 부품입니다. (결과물: `*.pb.go` 파일 생성) * **목적**: 명세서 내에 선언`message` 정의를 Go 언어의 구조체(`struct`) 코드로 자동 생성합니다. 이를 통해 바이너리 직렬화/역직렬화 인터페이스 및 필드 매핑 코드가 구현된 `*.pb.go` 파일을 얻을 수 있습니다.
3. **`protoc-gen-go-grpc` (Go gRPC 통신망 금형틀)**: 3. **`protoc-gen-go-grpc` (Go gRPC 서비스 플러그인)**:
* **역할**: Go 언어 전용 gRPC 통신 인터페이스 생성 플러그인 (**"네트워크 안테나 부착 부품"**) * **역할**: Go gRPC 통신 인터페이스 소스코드 생성 플러그인
* **목적**: 명세서에 선언된 `service` 규격(예: `IoTService` RPC)을 해석하여 기기와 서버가 실제로 네트워크 채널을 열고 연결하여 핑퐁 대화를 나눌 수 있게 통신 모듈 코드를 제공합니다. (결과물: `*_grpc.pb.go` 파일 생성) * **목적**: 명세서에 선언된 `service` 정의를 해석하여 Go 언어 환경의 gRPC 서버 핸들러 뼈대와 클라이언트 송수신용 채널 인터페이스를 제공하는 `*_grpc.pb.go` 파일을 출력합니다.
### 4.2 번역 도구 설치하기 ### 4.2 컴파일 도구 체인 설치
* **macOS (Homebrew 사용)**: * **macOS (Homebrew 사용)**:
터미널에 아래 명령어를 입력해 컴파일러와 Go 언어 통신용 변환 플러그인을 설치합니다. 터미널에 아래 명령어를 입력해 컴파일러와 Go 언어 통신용 변환 플러그인을 설치합니다.
```bash ```bash
@@ -159,93 +159,94 @@ gRPC 개발 환경을 구축하기 위해 설치하는 세 가지 프로그램
``` ```
* **Linux (Ubuntu 기준)**: * **Linux (Ubuntu 기준)**:
패키지 관리자를 통해 컴파일러를 다운로드하고 마찬가지로 Go 플러그인을 환경에 바인딩합니다. 패키지 관리자를 통해 컴파일러를 다운로드하고 마찬가지로 Go 플러그인을 환경에 바인딩합니다.
```bash ```bash
sudo apt install -y protobuf-compiler sudo apt install -y protobuf-compiler
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
``` ```
### 4.3 실제로 번역하기 (컴파일 명령어) ### 4.3 프로토콜 버퍼 컴파일 수행
아래 명령어는 컴퓨터에게 작성한 명세 문서를 바탕으로 실제 Go 컴파일 결과 코드를 얻어내기 위해 기동하는 명령어입니다: 작성된 스키마 명세를 빌드하여 Go 소스코드를 생성하기 위해 다음 명령어를 구동합니다:
```bash ```bash
# 실습 디렉터리로 이동 후 컴파일 실행 # 해당 실습 디렉터리로 이동 후 컴파일 실행
cd examples/grpcentity cd examples/grpcentity
protoc --go_out=. --go-grpc_out=. protoapi.proto protoc --go_out=. --go-grpc_out=. protoapi.proto
``` ```
#### 컴파일 옵션 세부 작동 원리 #### 컴파일 옵션의 출력 경로 지정 동작 방식
컴파일 시 사용하는 옵션각 결과물 파일이 생성될 **개별 기준 디렉터리(Base Directory)**를 선언하는 지시자입니다: 명령어 실행 시 주입하는 각 옵션은 빌드 파일이 생성될 **기준 디렉터리(Base Directory)**를 개별 정의합니다:
* **`--go_out=.`**: 데이터 규격(Message) 번역본인 `protoapi.pb.go` 파일이 출력될 기준 위치를 현재 명령을 실행 중인 경로(`.`)로 지정합니다. * **`--go_out=.`**: 데이터 명세 구현체인 `protoapi.pb.go` 출력될 기준 경로를 현재 컴파일 실행 디렉터리(`.`)로 선언합니다.
* **`--go-grpc_out=.`**: 네트워크 원격 함수(Service) 번역본인 `protoapi_grpc.pb.go` 파일이 출력될 기준 위치를 현재 실행 경로(`.`)로 지정합니다. * **`--go-grpc_out=.`**: gRPC 통신 구현체인 `protoapi_grpc.pb.go` 출력될 기준 경로를 현재 실행 디렉터리(`.`)로 선언합니다.
**🚨 Go 패키지 옵션과의 상호 결합 메커니즘**: **Go 패키지 지정 옵션과의 결합 규칙**:
옵션들은 단독으로 최종 목적지정하지 않습니다. 컴파일러는 지정된 기준 경로(예: `--go_out=.`)를 바탕으로, `.proto` 스키마 장부 내부에 선언된 `option go_package = "./protoapi;protoapi"` 의 세부 경로 정보를 읽어 들여 최종 병합합니다. 해당 출력 경로 옵션들은 단독으로 파일의 최종 위치정하지 않습니다. 컴파일러는 지정된 기준 경로(예: `.`)에 `.proto` 스펙 내부의 `option go_package = "./protoapi;protoapi"` 설정값을 조합하여 최종 디렉터리 경로를 생성합니다.
따라 **기준 경로(`.`) + 상세 상대경로(`./protoapi`)**가 결합하여 현재 실행 디렉터리 하위에 `protoapi/` 폴더가 자동 생성되, 그 안에 번역된 다음 두 핵심 Go 파일이 정상적으로 놓이게 됩니다: 이에 따라 **컴파일 대상 디렉터리(`.`)**와 **상세 패키지 주소(`./protoapi`)**가 결합되어 `examples/grpcentity/protoapi/` 경로가 자동 생성되, 그 하위에 다음 소스코드들이 정상 배치됩니다:
* `protoapi.pb.go`: 약속 문서에 적은 데이터(Message) 규격을 Go 구조체로 변환해 놓은 파일입니다. * `protoapi.pb.go`: 명세에 정의된 메시지(Message) 규격을 Go 구조체로 변환 파일입니다.
* `protoapi_grpc.pb.go`: 기기와 서버가 실제로 요청을 주고받을 수 있게 하는 통신 창구(Service) 함수가 자동 완성된 파일입니다. * `protoapi_grpc.pb.go`: 클라이언트와 서버 통신을 위한 원격 호출(Service) 규격을 구현한 파일입니다.
--- ---
## 5. 실습 소스코드의 속살 들여다보기 (구현 상세 분석) ## 5. 실습 소스코드 상세 구현 분석
번역기가 뼈대 코드를 만들어 주었으니, 이기와 서버가 나눌 구체적인 대화의 내용을 채워 넣어 봅시다. 컴파일러를 통해 통신을 위한 스터브(Stub) 코드가 확보되었으므로, 이반으로 서버와 클라이언트의 비즈니스 로직을 연결하는 상세 코드를 검토합니다.
### 5.1 요청에 대답하는 서버 구현 ([server.go](../examples/grpcentity/server.go)) ### 5.1 gRPC 서버 구현 분석 ([server.go](../examples/grpcentity/server.go))
* **대답 행동 대장 구조체 (`IoTServer`)**: * **서버 서비스 인터페이스 매핑 구조체 (`IoTServer`)**:
```go ```go
type IoTServer struct { type IoTServer struct {
protoapi.UnimplementedIoTServiceServer protoapi.UnimplementedIoTServiceServer
} }
``` ```
`UnimplementedIoTServiceServer`를 품에 안은 구조체를 만듭니다. 이 친구는 **"혹시 기기가 아직 구현되지 않은 통신 창구를 두드리더라도 서버가 뻗지 않고 조용히 '아직 준비 중입니다' 에러 대답을 돌려주도록"** 든든하게 받쳐주는 안전 보디가드 역할을 해 줍니다. `UnimplementedIoTServiceServer`를 임베딩하여 정의합니다. 이는 향후 새로운 메서드가 프로토콜 스펙에 추가되더라도 기존 서버가 빌드 오류 없이 구동 호환성(미구현 메서드 호출 시 unimplemented 에러 반환)을 안전하게 유지하도록 제약하기 위한 설정입니다.
* **대답 채워 넣기 (`UpdateSensingData`)**: * **센싱 데이터 업데이트 수신 처리 (`UpdateSensingData`)**:
```go ```go
func (IoTServer) UpdateSensingData(ctx context.Context, r *protoapi.SensingData) (*protoapi.SensingResponse, error) { 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()) 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 return &protoapi.SensingResponse{Success: true, Message: "Sensing data updated successfully!"}, nil
} }
``` ```
기기가 온/습도 패킷을 들고 찾아오면, 서버 화면에 그 정보를 정답게 출력한 뒤 "이상 없이 잘 받았습니다!"라는 성공 영수증(`SensingResponse`)을 발급해 주는 역할을 기특하게 해내고 있습니다. 클라이언트 디바이스로부터 센싱 패킷을 수신하면, 기기 식별 및 온습도 측정 매개변수를 포맷팅하여 표준 출력에 표시한 후 정상 처리 완료 플래그를 담은 응답 구조체(`SensingResponse`)를 반환합니다.
* **서버 문 열기 (`ServerRun`)**: * **gRPC 리스너 구동 및 서빙 (`ServerRun`)**:
```go ```go
listen, _ := net.Listen("tcp", port) listen, _ := net.Listen("tcp", port)
server.Serve(listen) server.Serve(listen)
``` ```
지정된 문 번호(포트 `:8080`)의 문을 활짝 열고, 기기들의 접속 요청을 다정히 기다리는 시작 지점입니다. 지정된 포트(기본 포트 `:8080`)의 TCP 소켓 포트를 활성화하고, 클라이언트의 접속 및 RPC 서비스 호출에 대해 지속적으로 대기하는 리스너 구동의 진입점입니다.
### 5.2 요청을 보내는 클라이언트 구현 ([client.go](../examples/grpcentity/client.go)) ### 5.2 gRPC 클라이언트 구현 분석 ([client.go](../examples/grpcentity/client.go))
* **전송 전용 기기 만들기 (`NewIoTServiceClient`)**: * **원격 서비스 클라이언트 기동 (`NewIoTServiceClient`)**:
```go ```go
conn, _ := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials())) conn, _ := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
client := protoapi.NewIoTServiceClient(conn) client := protoapi.NewIoTServiceClient(conn)
``` ```
서버로 통하는 통로(`conn`)를 안전하게 확보하고, 그 길을 타고 데이터를 실어 보낼 **전송 전용 클라이언트 기기**를 조립해 내는 핵심 과정입니다. 서버와의 TCP 채널(`conn`)을 평문 전송(insecure) 기반으로 바인딩한 뒤, 해당 채널을 통해 원격 서비스를 호출할 수 있는용 클라이언트 인스턴스를 확보합니다.
* **데이터 포장해서 보내기 (`AskUpdateSensingData`)**: * **패킷 구성 및 RPC 호출 실행 (`AskUpdateSensingData`)**:
```go ```go
request := &protoapi.SensingData{DeviceId: deviceId, Temperature: temp, Humidity: humid} request := &protoapi.SensingData{DeviceId: deviceId, Temperature: temp, Humidity: humid}
return m.UpdateSensingData(ctx, request) return m.UpdateSensingData(ctx, request)
``` ```
센서가 수집한 온/습도 정보를 예쁘게 상자에 담아 포장한 뒤 서버의 `UpdateSensingData` 창구로 쏘아 올립니다. 센서 측정값을 메시지 스펙 규격에 맞추어 `SensingData` 구조체 인스턴스로 바인딩한 후, 기설정된 클라이언트 인터페이스를 경유하여 서버의 `UpdateSensingData` 엔드포인트를 호출합니다.
### 5.3 기기와 서버의 핑퐁 대화 흐름 ### 5.3 통신 세션 동작 시퀀스 및 흐름
실습 예제를 실행하면 서버와 클라이언트가 다음과 같이 대화를 나눕니다: 실습 예제를 기동하면 클라이언트와 서버 간에 아래와 같은 동작 흐름이 순차적으로 실행됩니다:
1. **서버 시간 물어보기**: 기기가 *"지금 몇 시인가요?"* 하고 문을 두드리면, 서버는 시스템의 현재 날짜와 정보를 보기 좋게 반환해 줍니다. 1. **서버 날짜/시간 조회**: 클라이언트가 `GetDate`를 호출하여 서버의 현재 가동 정보 문자열을 회신받아 표준 출력에 노출합니다.
2. **비밀번호 생성**: 기기가 시드값과 길이를 주면, 서버는 불규칙하게 글자들을 마구 섞어 일회 보안 패스워드를 발급해 줍니다. 2. **보안 토큰용 임시 암호 발급**: 클라이언트가 암호화 연산 시드와 길이를 전달하여 `GetRandomPass`를 호출하고, 서버가 생성한 일회 보안 패스워드를 응답받습니다.
3. **온습도 데이터 전송**: 기기가 *"현재 방 안 온도는 24.50°C이고 습도는 52.30%입니다!"* 하고 소리치면, 서버는 이를 받아 화면에 출력하고 *"데이터가 무사히 갱신되었습니다"* 라고 기분 좋게 응답해 줍니다. 3. **환경 수집 센싱 데이터 동기화**: 클라이언트가 기기 식별값("sensor-room-01")과 가상의 온습도 변수를 실어 `UpdateSensingData`를 기동하면, 서버는 수신 값을 화면에 검증 및 로깅한 후 완료 보고 영수증을 반환합니다.
--- ---
## 6. 개발하다 막혔을 때 찾아보는 해결사 가이드 (트러블슈팅) ## 6. 트러블슈팅 (Troubleshooting)
실습을 진행하다가 갑작스레 에러를 마주했을 때 당황하지 않고 해결할 수 있는 가이드입니다. 실습 구동 과정에서 마주할 수 있는 전형적인 에러 현상과 대처 방안입니다.
### 6.1 `bind: address already in use` (문 번호가 꽉 막혔을 때) ### 6.1 `bind: address already in use` (네트워크 소켓 포트 충돌)
* **원인**: gRPC 서버를 켜려고 하는데 이미 다른 백그라운드 프로그램(혹은 덜 꺼진 이전 실습 서버)이 포트 번호 `:8080`을 꽉 쥐고 있어 문을 열지 못하는입니다. * **발생 원인**: gRPC 서버 기동 시 설정한 통신 포트 `:8080`이 이미 다른 네트워크 프로세스나 이전 실습 서버의 비정상 종료 등으로 인해 점유되어 바인딩에 실패한입니다.
* **해결 방법**: * **조치 방법**:
- `examples/main.go`의 `port` 변수값과 [server.go](../examples/grpcentity/server.go)의 `port` 전역 변수값을 동시에 다른 번호(예: `:9090`)로 변경하고 다시 실행해 보십시오. - `examples/main.go`의 `port` 변수 [server.go](../examples/grpcentity/server.go)의 `port` 전역 변수값을 동시에 다른 유휴 포트(예: `:9090`)로 변경하여 구동을 시도합니다.
- **🚨 미묘한 함정**: `server.go` 내부의 `ServerRun(addr string)` 함수는 외부 진입점으로부터 인자 `addr`가받지만, 실제 포트 리슨 코드에서는 이를 슬쩍 무시하고 자체 패키지 전역 변수 `port = ":8080"`를 직접 읽어 처리하도록 하드코딩되어 있습니다. 따라서 정상적으로 포트를 바꾸기 위해서는 반드시 `server.go` 내부 전역 변수인 `port` 값을 수정해 주어야 포트 바인딩이 성공합니다. - **🚨 소스코드 설계 제약**: `server.go`에 기재된 `ServerRun(addr string)` 함수는 메인 환경으로부터 주입받은 `addr` 인자값을 사용하지 않고, 패키지 내부 전역 변수 `port` (하드코딩된 값 `:8080`)소켓 리슨 파라미터로 직접 참조하도록 되어 있습니다. 따라서 바인딩 충돌 회피를 위해서는 `main.go`뿐만 아니라 반드시 `server.go` 파일 내부 전역 변수인 `port` 역시 함께 갱신해 주어야 실제 바인딩 포트 변경이 유효하게 처리됩니다.
--- ---