실패에 이름을 붙인다: 상태 코드와 오류 상세
한 줄 요약
상태 코드는 클라이언트에게 「다음에 무엇을 하라」 고 보내는 신호입니다. 코드 하나로 모자란 정보(어느 필드가, 어떤 이유로, 몇 초 뒤에)는 google.rpc.Status 메시지에 담아 트레일러 grpc-status-details-bin 으로 보냅니다.
왜 이게 필요했나
gRPC 호출은 OK 를 포함해 열일곱 가지 상태 코드 가운데 하나로 끝납니다. 처음 서비스를 만들면 실패를 전부 UNKNOWN 이나 INTERNAL 로 보내고 설명 문자열에 사정을 적기 쉽습니다. 그러면 클라이언트는 문자열을 파싱해야 하고, 문구를 바꾸는 순간 분기가 깨집니다.
더 큰 문제는 자동으로 돌아가는 장치들이 코드를 본다는 점입니다. 재시도 정책은 「어떤 코드면 다시 보낼지」 를 코드 목록으로 받고, 경보는 코드별 오류율로 울리며, 게이트웨이는 코드를 HTTP 상태로 바꿉니다. 점검 중이라는 실패를 INTERNAL 로 보내면 재시도해야 할 일이 버그로 신고되고, 잔액 부족을 UNAVAILABLE 로 보내면 다시 해도 소용없는 요청이 재시도 폭풍이 됩니다.
어떻게 동작하나
상태는 응답 메시지 뒤에 오는 트레일러에 실립니다. grpc-status 에 코드 숫자, grpc-message 에 설명이 들어갑니다. 코드를 고를 때 gRPC 문서가 주는 기준은 「클라이언트가 무엇을 해야 하는가」 입니다.
| 상황 | 코드 | 클라이언트가 할 일 |
|---|---|---|
| 요청 자체가 틀렸다(빈 필드, 음수 금액) | INVALID_ARGUMENT | 요청을 고친다 |
| 요청은 맞지만 시스템 상태가 맞지 않는다(잔액 부족, 동결) | FAILED_PRECONDITION | 상태가 바뀌기 전에는 다시 하지 않는다 |
| 같은 호출을 잠시 뒤 다시 하면 된다(점검, 연결 끊김) | UNAVAILABLE | 물러났다가 다시 보낸다 |
| 더 위에서 처음부터 다시 해야 한다(동시 수정 충돌) | ABORTED | 읽고 고치고 쓰기를 다시 한다 |
| 한도를 넘었다 | RESOURCE_EXHAUSTED | 알려 준 시간만큼 기다린다 |
| 누구인지 모른다 · 누구인지는 알지만 권한이 없다 | UNAUTHENTICATED · PERMISSION_DENIED | 로그인한다 · 권한을 받는다 |
라이브러리가 스스로 만드는 코드도 있습니다. 연결을 못 맺으면 UNAVAILABLE, 기한이 지나면 DEADLINE_EXCEEDED, 처리기가 예상하지 못한 예외를 던지면 UNKNOWN 입니다. 마지막 것이 함정입니다. 파이썬 grpcio 1.84.0 에서 처리기가 RuntimeError("세션 키 ...") 를 던지게 해 보면 클라이언트는 UNKNOWN 과 함께 「Exception calling application: 세션 키 ...」 를 그대로 받습니다(실측). 예외 문자열에는 내부 주소나 키가 섞이곤 하므로, 서버는 예상하지 못한 예외를 잡아 INTERNAL 과 참조 번호만 내보내고 원인은 안쪽 기록에 남깁니다.
코드 하나로 모자란 것을 위해 구글 API 오류 모델이 있습니다. google.rpc.Status 는 code·message·details 세 칸이고, details 에는 표준 상세 메시지를 Any 로 싸서 넣습니다. 이 메시지를 직렬화해 트레일러 grpc-status-details-bin 에 실으며, 머리 이름이 -bin 으로 끝나면 값은 바이너리입니다.
BadRequest 어느 필드가 왜 틀렸는가(틀린 필드 전부) INVALID_ARGUMENT 와 함께
ErrorInfo 기계가 분기할 이유 reason · 정의한 쪽 domain FAILED_PRECONDITION 등과 함께
RetryInfo 언제 다시 해도 되는가(retry_delay) UNAVAILABLE · RESOURCE_EXHAUSTED 와 함께
QuotaFailure 어떤 한도를 넘었는가 RESOURCE_EXHAUSTED 와 함께
지킬 규칙이 둘 있습니다. Status.code 와 message 는 grpc 상태와 같게 둡니다. 그리고 ErrorInfo.reason 은 설명 문구가 아니라 API 계약의 일부인 상수로 다룹니다. 클라이언트가 그 값으로 분기하기 때문입니다.
현장에서 만나는 모습
결제나 이체처럼 실패 방법이 많은 호출에서 이 구분이 화면 문구를 정합니다. 잔액 부족이면 충전 안내를, 동결이면 고객센터 안내를, 점검이면 「잠시 뒤 다시」 를 보여 줘야 하는데, 셋 중 둘은 같은 FAILED_PRECONDITION 입니다. 그래서 ErrorInfo.reason 이 필요합니다.
운영 지표에서는 상태 코드별 오류율을 볼 때 NOT_FOUND 처럼 정상적인 응답을 오류로 세지 않도록 경보 식을 나눕니다. HTTP 로 트랜스코딩하는 게이트웨이는 INVALID_ARGUMENT 를 400, UNAVAILABLE 을 503 처럼 바꾸므로 7모듈에서 그 표를 다시 봅니다. 트레일러가 와이어에서 어떻게 생겼는지는 블로그 글 HTTP/2 위의 gRPC: 머리말, 5바이트 접두, 트레일러의 상태 코드 에 있습니다.
다음 실습에서 할 것
이체 서비스 bank.v1.Bank 를 만들며 장부의 예외를 상태 코드로 바꿉니다. 예상하지 못한 예외를 숨기고 참조 번호로 기록한 뒤, BadRequest·ErrorInfo·RetryInfo·QuotaFailure 를 트레일러에 싣고, 마지막에 클라이언트가 그 상세를 읽어 분기하게 합니다.