diff --git a/docs/GRPC.md b/docs/GRPC.md index f0431e7..13d6c2a 100644 --- a/docs/GRPC.md +++ b/docs/GRPC.md @@ -132,24 +132,24 @@ message RandomPass { --- -## 4. 말귀를 알아듣는 코드 변환기 준비 (Go Stub 컴파일 및 도구 체인) +## 4. Go Stub 컴파일 및 도구 체인 -우리가 열심히 기획해서 적은 `.proto` 약속 파일을 컴퓨터(Go 언어)가 알아듣는 소스코드로 변환해 줄 **번역기(`protoc`)**와 **Go 전용 번역 플러그인**들을 로컬 개발 환경에 설치하고 구동하는 방법입니다. +작성된 `.proto` 명세 파일을 Go 언어 소스코드로 변환하기 위해 프로토콜 버퍼 컴파일러(`protoc`)와 Go 전용 플러그인을 로컬 개발 환경에 구성하는 가이드라인입니다. -### 4.1 핵심 도구 체인 삼각 편대 이해하기 -gRPC 개발 환경을 구축하기 위해 설치하는 세 가지 프로그램의 구체적인 역할과 설치 목적은 다음과 같습니다: +### 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` 파일 생성) + * **역할**: 프로토콜 버퍼 코어 컴파일러 엔진 + * **목적**: `.proto` 파일의 스키마 명세를 구문 분석하는 역할을 수행합니다. 특정 프로그래밍 언어에 의존하지 않는 공통 파서 역할을 하며, 생성된 구문 분석 정보를 아래의 언어별 플러그인 모듈에 위임하여 타깃 코드를 출력하도록 제어합니다. +2. **`protoc-gen-go` (Go 데이터 직렬화 플러그인)**: + * **역할**: Go 구조체 및 직렬화 소스코드 생성 플러그인 + * **목적**: 명세서 내에 선언된 `message` 정의를 Go 언어의 구조체(`struct`) 코드로 자동 생성합니다. 이를 통해 바이너리 직렬화/역직렬화 인터페이스 및 필드 매핑 코드가 구현된 `*.pb.go` 파일을 얻을 수 있습니다. +3. **`protoc-gen-go-grpc` (Go gRPC 서비스 플러그인)**: + * **역할**: Go gRPC 통신 인터페이스 소스코드 생성 플러그인 + * **목적**: 명세서 내에 선언된 `service` 정의를 해석하여 Go 언어 환경의 gRPC 서버 핸들러 뼈대와 클라이언트 송수신용 채널 인터페이스를 제공하는 `*_grpc.pb.go` 파일을 출력합니다. -### 4.2 번역 도구 설치하기 +### 4.2 컴파일 도구 체인 설치 * **macOS (Homebrew 사용)**: 터미널에 아래 명령어를 입력해 컴파일러와 Go 언어 통신용 변환 플러그인을 설치합니다. ```bash @@ -159,93 +159,94 @@ gRPC 개발 환경을 구축하기 위해 설치하는 세 가지 프로그램 ``` * **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 컴파일 결과 코드를 얻어내기 위해 기동하는 명령어입니다: +### 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` 파일이 출력될 기준 위치를 현재 실행 경로(`.`)로 지정합니다. +#### 컴파일 옵션의 출력 경로 지정 동작 방식 +명령어 실행 시 주입하는 각 옵션은 빌드 파일이 생성될 **기준 디렉터리(Base Directory)**를 개별 정의합니다: +* **`--go_out=.`**: 데이터 명세 구현체인 `protoapi.pb.go`가 출력될 기준 경로를 현재 컴파일 실행 디렉터리(`.`)로 선언합니다. +* **`--go-grpc_out=.`**: gRPC 통신 구현체인 `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) 함수가 자동 완성된 파일입니다. +**Go 패키지 지정 옵션과의 결합 규칙**: +해당 출력 경로 옵션들은 단독으로 파일의 최종 위치를 고정하지 않습니다. 컴파일러는 지정된 기준 경로(예: `.`)에 `.proto` 스펙 내부의 `option go_package = "./protoapi;protoapi"` 설정값을 조합하여 최종 디렉터리 경로를 생성합니다. +이에 따라 **컴파일 대상 디렉터리(`.`)**와 **상세 패키지 주소(`./protoapi`)**가 결합되어 `examples/grpcentity/protoapi/` 경로가 자동 생성되며, 그 하위에 다음 소스코드들이 정상 배치됩니다: +* `protoapi.pb.go`: 명세에 정의된 메시지(Message) 규격을 Go 구조체로 변환한 파일입니다. +* `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 type IoTServer struct { protoapi.UnimplementedIoTServiceServer } ``` - `UnimplementedIoTServiceServer`를 품에 안은 구조체를 만듭니다. 이 친구는 **"혹시 기기가 아직 구현되지 않은 통신 창구를 두드리더라도 서버가 뻗지 않고 조용히 '아직 준비 중입니다' 에러 대답을 돌려주도록"** 든든하게 받쳐주는 안전 보디가드 역할을 해 줍니다. -* **대답 채워 넣기 (`UpdateSensingData`)**: + `UnimplementedIoTServiceServer`를 임베딩하여 정의합니다. 이는 향후 새로운 메서드가 프로토콜 스펙에 추가되더라도 기존 서버가 빌드 오류 없이 구동 호환성(미구현 메서드 호출 시 unimplemented 에러 반환)을 안전하게 유지하도록 제약하기 위한 설정입니다. +* **센싱 데이터 업데이트 수신 처리 (`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`)**: + 클라이언트 디바이스로부터 센싱 패킷을 수신하면, 기기 식별 및 온습도 측정 매개변수를 포맷팅하여 표준 출력에 표시한 후 정상 처리 완료 플래그를 담은 응답 구조체(`SensingResponse`)를 반환합니다. +* **gRPC 리스너 구동 및 서빙 (`ServerRun`)**: ```go listen, _ := net.Listen("tcp", port) 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 conn, _ := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials())) client := protoapi.NewIoTServiceClient(conn) ``` - 서버로 통하는 통로(`conn`)를 안전하게 확보하고, 그 길을 타고 데이터를 실어 보낼 **전송 전용 클라이언트 기기**를 조립해 내는 핵심 과정입니다. -* **데이터 포장해서 보내기 (`AskUpdateSensingData`)**: + 서버와의 TCP 채널(`conn`)을 평문 전송(insecure) 기반으로 바인딩한 뒤, 해당 채널을 통해 원격 서비스를 호출할 수 있는 전송용 클라이언트 인스턴스를 확보합니다. +* **패킷 구성 및 RPC 호출 실행 (`AskUpdateSensingData`)**: ```go request := &protoapi.SensingData{DeviceId: deviceId, Temperature: temp, Humidity: humid} return m.UpdateSensingData(ctx, request) ``` - 센서가 수집한 온/습도 정보를 예쁘게 상자에 담아 포장한 뒤 서버의 `UpdateSensingData` 창구로 쏘아 올립니다. + 센서 측정값을 메시지 스펙 규격에 맞추어 `SensingData` 구조체 인스턴스로 바인딩한 후, 기설정된 클라이언트 인터페이스를 경유하여 서버의 `UpdateSensingData` 엔드포인트를 호출합니다. -### 5.3 기기와 서버의 핑퐁 대화 흐름 -실습 예제를 실행하면 서버와 클라이언트가 다음과 같이 대화를 나눕니다: -1. **서버 시간 물어보기**: 기기가 *"지금 몇 시인가요?"* 하고 문을 두드리면, 서버는 시스템의 현재 날짜와 시간 정보를 보기 좋게 반환해 줍니다. -2. **비밀번호 생성**: 기기가 시드값과 길이를 주면, 서버는 불규칙하게 글자들을 마구 섞어 일회용 보안 패스워드를 발급해 줍니다. -3. **온습도 데이터 전송**: 기기가 *"현재 방 안 온도는 24.50°C이고 습도는 52.30%입니다!"* 하고 소리치면, 서버는 이를 받아 화면에 출력하고 *"데이터가 무사히 갱신되었습니다"* 라고 기분 좋게 응답해 줍니다. +### 5.3 통신 세션 동작 시퀀스 및 흐름 +실습 예제를 기동하면 클라이언트와 서버 간에 아래와 같은 동작 흐름이 순차적으로 실행됩니다: +1. **서버 날짜/시간 조회**: 클라이언트가 `GetDate`를 호출하여 서버의 현재 가동 시계 정보 문자열을 회신받아 표준 출력에 노출합니다. +2. **보안 토큰용 임시 암호 발급**: 클라이언트가 암호화 연산 시드와 길이를 전달하여 `GetRandomPass`를 호출하고, 서버가 생성한 일회성 보안 패스워드를 응답받습니다. +3. **환경 수집 센싱 데이터 동기화**: 클라이언트가 기기 식별값("sensor-room-01")과 가상의 온습도 변수를 실어 `UpdateSensingData`를 기동하면, 서버는 수신 값을 화면에 검증 및 로깅한 후 완료 보고 영수증을 반환합니다. --- -## 6. 개발하다 막혔을 때 찾아보는 해결사 가이드 (트러블슈팅) +## 6. 트러블슈팅 (Troubleshooting) -실습을 진행하다가 갑작스레 에러를 마주했을 때 당황하지 않고 해결할 수 있는 가이드입니다. +실습 구동 과정에서 마주할 수 있는 전형적인 에러 현상과 대처 방안입니다. -### 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` 값을 수정해 주어야 포트 바인딩이 성공합니다. +### 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`)를 소켓 리슨 파라미터로 직접 참조하도록 되어 있습니다. 따라서 바인딩 충돌 회피를 위해서는 `main.go`뿐만 아니라 반드시 `server.go` 파일 내부의 전역 변수인 `port` 역시 함께 갱신해 주어야 실제 바인딩 포트 변경이 유효하게 처리됩니다. ---