· 6분 읽기
Proto 계약 중심 API 운영: Buf v2와 gRPC-Gateway 듀얼 프로토콜
Proto 하나를 잘 관리하면 API 계약 검증부터 HTTP/JSON 서빙까지 한 번에 해결할 수 있습니다. 이 글은 Buf v2로 Proto 스키마의 린트, 브레이킹 체인지 검증, 코드 생성을 CI 계약 관리 체계로 세우는 과정에서 시작해, 같은 Proto 정의에서 gRPC-Gateway를 통해 내부는 gRPC로, 외부·브라우저는 HTTP/JSON으로 동시에 서빙하는 구성까지 이어지는 흐름으로 정리합니다.
Proto를 API 계약으로: Buf v2
protoc를 단독으로 쓰면 의존성 관리, 린트 규칙 통일, breaking change 검증, 플러그인 기반 코드 생성을 각자 방식대로 처리하게 되어 팀 간 편차가 커집니다. Buf는 이 문제를 정리해 줍니다.
기본 파일 구성
buf.yaml: 모듈, 의존성, 린트, 브레이킹 규칙을 정의합니다.buf.gen.yaml: 코드 생성 플러그인을 설정합니다.buf.lock: 의존성을 잠급니다.
추천하는 디렉터리 구조는 다음과 같습니다.
proto/
v1/
service.proto
buf.yaml
buf.gen.yaml
핵심 명령과 CI 기준
buf lint
buf breaking --against '.git#branch=main'
buf generate
CI에서는 최소한 buf lint 통과, buf breaking 통과, buf generate 실행 후 diff가 없는지 확인하는 세 단계를 필수로 둡니다.
운영 규칙
package와 파일 경로는 v1, v1beta1처럼 버전 기준으로 고정합니다. field 번호는 재사용을 금지하고, 삭제할 때는 reserved를 선언합니다. 호환성을 깨는 변경은 기존 필드를 고치는 대신 새 버전 패키지로 분리합니다.
코드 생성에서는 Go, gRPC, gateway, OpenAPI 산출물의 생성 경로를 명확히 분리하고, 생성 산출물을 커밋할지 여부를 팀 규칙으로 고정하며, 로컬과 CI에서 동일한 플러그인 버전을 사용해야 합니다.
자주 하는 실수는 lint 통과만 확인하고 의미상 호환성 검토를 생략하는 것, breaking 기준 브랜치 설정을 잘못하는 것, 옵션 파일이나 외부 proto 의존성의 버전이 어긋나는 것입니다. Buf 도입의 핵심 가치는 생성 편의가 아니라 계약 품질 관리이며, proto를 API 계약으로 운영하려면 lint, breaking, generate를 CI 필수 단계로 묶어야 합니다.
하나의 Proto로 듀얼 프로토콜: gRPC-Gateway
Buf로 Proto 계약이 안정화되면, 같은 정의를 재사용해 내부 통신은 gRPC로, 외부나 브라우저는 HTTP/JSON으로 동시에 제공할 수 있습니다. 동일한 비즈니스 로직을 유지하면서 두 프로토콜을 함께 서빙하려는 경우가 많은데, gRPC-Gateway는 이 요구를 Proto 중심으로 묶어줍니다.
Proto에 HTTP 매핑 추가
syntax = "proto3";
import "google/api/annotations.proto";
service DocumentService {
rpc GetDocument(GetDocumentRequest) returns (GetDocumentResponse) {
option (google.api.http) = {
get: "/v1/documents/{id}"
};
}
rpc CreateDocument(CreateDocumentRequest) returns (CreateDocumentResponse) {
option (google.api.http) = {
post: "/v1/documents"
body: "*"
};
}
}
코드 생성
# buf.gen.yaml
version: v2
plugins:
- remote: buf.build/grpc/go
out: generated/go
opt: paths=source_relative
- remote: buf.build/grpc-ecosystem/gateway
out: generated/go
opt:
- paths=source_relative
- standalone=true
buf generate
앞서 정리한 Buf 워크플로우에 gateway 플러그인 하나만 추가하면 되므로, 계약 관리 체계를 새로 만들 필요가 없습니다.
서버 구성 원칙
gRPC 서버는 단일 진입점으로 유지하고, Gateway는 가능한 인-프로세스로 등록(Register...HandlerServer)하는 방식을 우선합니다. HTTP와 gRPC의 에러 코드는 같은 도메인 코드로 매핑해야 클라이언트가 프로토콜에 상관없이 일관된 에러를 받습니다.
grpcServer := grpc.NewServer()
pb.RegisterDocumentServiceServer(grpcServer, serviceImpl)
gwMux := runtime.NewServeMux()
_ = pb.RegisterDocumentServiceHandlerServer(context.Background(), gwMux, serviceImpl)
http.ListenAndServe(":8080", gwMux)
운영 시 주의사항
HTTP 레이어가 추가되면서 지연이 늘어날 수 있고, 스트리밍 RPC는 HTTP로 변환할 때 제약이 큽니다. 인증·인가 정책이 gRPC와 HTTP에서 서로 다르게 흘러가지 않도록 공통 미들웨어 정책을 둬야 합니다.
운영 체크리스트
- 경로별 p95/p99를 gRPC와 HTTP로 분리해서 관측합니다.
- OpenAPI 산출물을 CI에서 자동 갱신합니다.
- 클라이언트 계약 테스트로 HTTP 매핑의 회귀를 방지합니다.
- Buf의
lint·breaking·generate세 단계와 Gateway 등록 코드를 함께 리뷰 대상에 포함합니다.
정리
Buf v2가 Proto를 API 계약으로 관리하는 도구라면, gRPC-Gateway는 그 계약을 두 프로토콜로 동시에 서빙하는 도구입니다. 두 도구를 조합하면 Proto 하나로 계약 검증부터 코드 생성, 듀얼 프로토콜 서빙까지 이어지는 일관된 워크플로우를 만들 수 있습니다. gRPC-Gateway의 핵심 가치는 중복 구현 제거이지만, 성공 여부는 생성 도구 자체가 아니라 Proto 계약, 에러 매핑, 관측 체계를 한 번에 유지하는 데 달려 있습니다.