인터셉터·메타데이터·에러 처리
RPC 메서드 밖에 있어야 하는 책임
gRPC 서비스를 처음 만들 때는 .proto에 정의된 메서드 구현에 관심이 쏠린다. 예를 들어 CreateOrder, GetUser, ListLogs 같은 함수가 실제 비즈니스 로직을 담는다. 하지만 운영 환경에서는 메서드 본문보다 더 넓게 적용되어야 하는 책임이 많다.
- 모든 요청에
request-id와 trace context를 붙인다. - 인증 토큰을 읽고 호출자를 식별한다.
- 메서드별 지연 시간, status code, payload 크기를 기록한다.
- 장애 주입, rate limit, 정책 검사, 감사 로그를 공통 처리한다.
- 에러를 내부 예외 그대로 노출하지 않고 안정적인 status code로 바꾼다.
이 책임을 각 RPC 메서드에 복사해 넣으면 금방 운영 비용이 커진다. gRPC에서는 이런 횡단 관심사를 인터셉터(interceptor) 로 분리하고, 호출과 함께 전달되는 부가 정보를 메타데이터(metadata) 로 주고받으며, 최종 결과는 status code와 optional message 로 표현한다.
세 개의 층으로 보기
metadata → Server Interceptor → RPC Handler
status + error details ← logging / metrics / authz ← response or error
운영 관점에서 이 구조는 다음처럼 나눠 이해하면 편하다.
| 층 | 담당 | 대표 질문 |
|---|---|---|
| 인터셉터 | 호출 전후의 공통 로직 | 이 RPC를 기록·검증·변환해야 하는가? |
| 메타데이터 | RPC에 붙는 key-value 부가 정보 | 이 호출의 인증, 추적, 정책 정보를 어떻게 전달할 것인가? |
| 에러 모델 | 호출의 최종 성공/실패 표현 | 클라이언트가 어떤 조치를 해야 하는가? |
핵심은 인터셉터가 “모든 문제를 해결하는 만능 훅”이 아니라는 점이다. gRPC 공식 문서는 인터셉터를 per-call 메커니즘으로 설명한다. TCP 연결 관리, 포트 설정, TLS 설정처럼 전송 계층 자체를 구성하는 일에는 맞지 않는다. 반대로 메서드별 비즈니스 로직과 독립적인 로깅, 메트릭, 정책 검사, 서버 측 인증·인가에는 잘 맞는다.
인터셉터: gRPC의 미들웨어
인터셉터는 웹 프레임워크의 middleware 또는 filter와 비슷하다. 클라이언트 쪽에서는 stub 호출을 감싸고, 서버 쪽에서는 실제 RPC handler 호출 전후를 감싼다.
서버 인터셉터가 적합한 일
서버 측 인터셉터는 다음 책임에 특히 잘 맞는다.
- 인증·인가: metadata의
authorization값을 읽어 principal을 만들고, 메서드 정책을 확인한다. - 관측성: method name, deadline, status code, latency, peer 정보를 metric/log/span에 남긴다.
- 에러 정규화: 내부 예외를
INTERNAL로 숨기거나, validation error를INVALID_ARGUMENT로 변환한다. - 정책 집행: rate limit, allowlist, feature flag, tenant boundary를 공통 적용한다.
단, 클라이언트 인증 정보를 붙이는 작업은 언어별로 전용 call credentials API가 더 적합할 수 있다. 인터셉터로도 토큰을 주입할 수 있지만, credential refresh나 보안 저장소 연동까지 고려하면 전용 인증 API가 더 안전한 추상화일 때가 많다.
순서가 의미를 바꾼다
여러 인터셉터를 체인으로 붙일 때는 순서가 결과를 바꾼다. 예를 들어 서버에 panic-recovery, auth, metrics, handler가 있다고 하자.
recovery가 가장 바깥쪽에 있으면 안쪽에서 발생한 예외를 폭넓게 잡아 INTERNAL로 변환할 수 있다. auth가 handler보다 앞에 있으면 인증 실패 요청은 비즈니스 로직에 도달하지 않는다. metrics의 위치는 무엇을 측정할지 결정한다. auth 실패까지 포함한 전체 요청량을 보고 싶다면 auth 바깥쪽에 두고, 실제 handler 실행 지연만 보고 싶다면 auth 안쪽에 둔다.
메타데이터: 메시지 본문 밖의 side channel
gRPC metadata는 RPC와 함께 전달되는 key-value 정보다. HTTP/2 headers와 trailers 위에 구현되며, 요청·응답 메시지 본문과는 분리된다.
headers와 trailers
| 구분 | 언제 전송되는가 | 대표 용도 |
|---|---|---|
| Request headers | 클라이언트가 요청 메시지를 보내기 전 | authorization, x-request-id, trace context, tenant id |
| Response headers | 서버가 응답 메시지를 보내기 전 | 서버 처리 힌트, 초기 응답 metadata |
| Response trailers | 서버가 메시지 전송을 끝낸 뒤 | 최종 status, error details, 처리 비용 정보 |
Metadata key는 ASCII 문자열이며 case-insensitive로 취급된다. grpc- prefix는 gRPC 내부용으로 예약되어 있으므로 애플리케이션 metadata key로 쓰면 안 된다. 값은 ASCII 문자열 또는 binary data일 수 있다. 또한 metadata는 HTTP/2 header 계층을 쓰기 때문에 크기 제한을 현실적으로 고려해야 한다. gRPC 문서는 request header 기본 제한으로 8 KiB가 제안될 수 있음을 경고한다.
metadata에 넣을 것과 넣지 말 것
metadata는 편리하지만 남용하면 API 계약이 흐려진다.
좋은 후보는 다음과 같다.
- 인증·인가에 필요한 credential 또는 tenant context
- tracing propagation 정보
- idempotency key, request id, correlation id
- rate limit이나 quota 계산에 필요한 가벼운 힌트
- 응답의 최종 상태를 보강하는 작은 error detail
반대로 다음 정보는 metadata에 넣지 않는 편이 낫다.
- 비즈니스 데이터 본문 자체
- 수십 KiB 이상의 큰 구조화 payload
- 클라이언트가 반드시 알아야 하는 필수 도메인 필드
- 공개 API에서 안정 계약으로 관리해야 하지만 문서화되지 않은 숨은 옵션
필수 도메인 데이터는 protobuf message 필드로 모델링하는 편이 좋다. metadata는 “호출을 처리하는 방법” 또는 “호출을 추적하는 방법”에 가까운 정보를 담는 보조 채널로 보는 것이 안전하다.
에러 처리: message보다 code가 먼저다
gRPC 호출은 성공 시 OK status로 끝나고, 실패 시 정해진 status code와 optional string message를 반환한다. 이 표준 에러 모델은 모든 gRPC 언어에서 지원되며 protobuf에만 묶이지 않는다.
중요한 운영 원칙은 클라이언트가 message 문자열을 파싱하지 않게 하는 것이다. message는 사람에게 설명하기 위한 텍스트다. 재시도, 입력 수정, 로그인 유도, 사용자 안내 같은 자동 처리는 status code와 구조화된 detail을 기준으로 해야 한다.
자주 헷갈리는 status code
| 상황 | 권장 code | 판단 기준 |
|---|---|---|
| 요청 인자가 시스템 상태와 무관하게 잘못됨 | INVALID_ARGUMENT | malformed id, 잘못된 enum, validation 실패 |
| 현재 시스템 상태 때문에 수행 불가 | FAILED_PRECONDITION | 먼저 다른 작업을 해야 성공 가능 |
| 인증 정보가 없거나 유효하지 않음 | UNAUTHENTICATED | “누구인지 모름” |
| 호출자는 식별됐지만 권한이 없음 | PERMISSION_DENIED | “누군지는 알지만 허용 안 됨” |
| 일시적 장애 또는 서버 종료 | UNAVAILABLE | backoff 후 재시도 가능성이 있음 |
| deadline 초과 | DEADLINE_EXCEEDED | 서버 성공 여부와 별개로 클라이언트 관찰상 시간 초과 |
| 내부 불변식 깨짐 또는 예기치 못한 버그 | INTERNAL | 클라이언트 조치로 해결하기 어려움 |
| 알 수 없는 예외 또는 status 변환 실패 | UNKNOWN | 더 구체적인 code를 줄 수 없는 경우 |
INVALID_ARGUMENT, NOT_FOUND, ALREADY_EXISTS, FAILED_PRECONDITION, ABORTED, OUT_OF_RANGE, DATA_LOSS는 gRPC library가 직접 생성하지 않고 애플리케이션 코드가 반환하는 code로 분류된다. 따라서 클라이언트가 이 code를 받았다면 보통 서버 애플리케이션이 의도적으로 의미를 부여한 실패라고 해석할 수 있다.
richer error model: 구조화된 실패 정보
표준 에러 모델은 code와 message만 제공하므로 validation field 목록, quota 초과 원인, retry delay 같은 세부 정보를 표현하기에는 부족하다. protobuf를 사용하는 API에서는 Google API의 richer error model을 적용해 google.rpc.Status와 google.rpc.ErrorInfo, BadRequest, RetryInfo, QuotaFailure 같은 detail message를 trailers에 담을 수 있다.
예를 들어 회원 생성 요청에서 이메일 형식과 나이 조건이 동시에 실패했다면 INVALID_ARGUMENT 하나만 돌려주는 것보다 BadRequest.field_violations에 실패 필드를 구조적으로 담는 편이 클라이언트 UX에 유리하다.
// 개념 예시: 실제 언어별 API는 다르다.
message BadRequest {
repeated FieldViolation field_violations = 1;
}
message FieldViolation {
string field = 1; // "email"
string description = 2; // "must be a valid email address"
}다만 richer error model도 공짜는 아니다. 공식 gRPC 문서는 언어별 구현 차이, proxy·logger가 detail을 보지 못하는 문제, trailers 크기 증가, HTTP/2 header compression 효율 저하, header size limit 초과 가능성을 주의점으로 든다. 그래서 모든 에러에 큰 detail을 붙이기보다, 클라이언트가 실제로 자동 처리하거나 사용자에게 구체적으로 안내해야 하는 실패에 우선 적용하는 편이 현실적이다.
운영 설계 패턴
1. 인터셉터에서 에러를 정규화한다
handler 내부에서 여러 종류의 예외가 발생하더라도 외부 계약은 안정적이어야 한다. 서버 인터셉터는 내부 예외를 잡아 다음처럼 변환할 수 있다.
- validation exception →
INVALID_ARGUMENT - optimistic lock conflict →
ABORTED - quota exceeded →
RESOURCE_EXHAUSTED - dependency timeout →
UNAVAILABLE또는DEADLINE_EXCEEDED - unexpected panic →
INTERNAL
이때 내부 stack trace나 DB error string을 그대로 message로 내보내면 보안과 호환성 문제가 생긴다. 내부 원인은 log/span에 남기고, 클라이언트에는 안정적인 code와 짧은 설명, 필요한 경우 구조화 detail만 제공한다.
2. correlation id를 metadata로 전파한다
장애 분석에서 가장 중요한 것은 “이 클라이언트 오류가 서버의 어떤 로그와 연결되는가”다. 클라이언트 인터셉터는 요청마다 x-request-id를 생성하거나 기존 값을 이어받아 metadata에 넣고, 서버 인터셉터는 그 값을 log field와 response trailer에 반영할 수 있다.
client log: request_id=7f3a status=UNAVAILABLE
server log: request_id=7f3a method=/order.v1.OrderService/CreateOrder dependency=mysql timeout
trace: request_id=7f3a span_id=...이 패턴은 단순하지만 운영 효율이 크다. 특히 streaming RPC에서는 중간 메시지가 많아도 최종 status와 request id가 하나의 호출 단위로 남아야 한다.
3. deadline과 cancellation을 status로 해석한다
클라이언트가 deadline을 설정하면 서버는 context cancellation을 감지하고 불필요한 작업을 멈춰야 한다. deadline 초과는 보통 DEADLINE_EXCEEDED로 관찰된다. 클라이언트가 직접 취소하면 CANCELLED가 된다.
주의할 점은 DEADLINE_EXCEEDED가 “서버가 절대 성공하지 않았다”는 뜻은 아니라는 점이다. 서버가 실제로 작업을 완료했더라도 응답이 deadline 이후 도착하면 클라이언트는 timeout으로 볼 수 있다. 그래서 결제, 주문 생성, 메시지 발행처럼 부작용이 있는 RPC는 idempotency key나 중복 처리 전략을 함께 설계해야 한다.
실전 체크리스트
- 공통 로깅, 메트릭, 인증·인가, 에러 변환은 handler가 아니라 인터셉터에 둔다.
- 인터셉터 순서를 문서화한다. 순서가 관측 범위와 보안 의미를 바꾼다.
- metadata key에
grpc-prefix를 쓰지 않는다. - metadata에는 작고 호출 관련적인 정보만 넣고, 도메인 데이터는 protobuf message 필드로 둔다.
- 클라이언트는 message 문자열을 파싱하지 말고 status code와 structured detail을 본다.
- 재시도 가능성은
UNAVAILABLE,DEADLINE_EXCEEDED,ABORTED같은 code와 idempotency를 함께 고려한다. - richer error detail은 유용하지만 크기·호환성·관측성 비용이 있으므로 필요한 실패에 선별 적용한다.
- 내부 예외와 stack trace는 외부 response가 아니라 log/span에 남긴다.