API 스타일을 REST로 할지 GraphQL로 할지 gRPC로 할지는 지연시간·처리량 벤치마크로 갈리지 않는다. 실무에서 먼저 터지는 건 "이 응답이 성공인지 실패인지, 재시도해도 되는지"를 클라이언트가 못 믿을 때다. 나는 세 스타일을 모두 운영해보고 나서 결론을 바꿨다 — 스타일보다 에러 계약을 먼저 고정하는 쪽이 장애 회고에서 덜 나온다.
에러 응답 규격, 스타일 고르기 전에 뭐부터 정해야 할까?
결론부터 쓰면, API 스타일을 정하기 전에 "성공·부분성공·실패를 어떻게 기계가 판독 가능한 키로 구분할지"를 먼저 정해야 한다. REST·GraphQL·gRPC는 이 구분을 표현하는 방식이 근본적으로 다르기 때문에, 스타일을 먼저 정하고 에러 계약을 나중에 끼워 맞추면 SDK와 모니터링을 두 번 고치게 된다.
- REST는 HTTP 상태 코드 + 본문이 계약의 단위다
- GraphQL은 HTTP 200 안에서
data와errors가 동시에 존재할 수 있다 - gRPC는 RPC 상태 코드 + protobuf 메시지가 계약의 단위다
- 세 스타일 모두
message문자열이 아니라code·retryable·trace_id같은 안정 키를 우선해야 한다 - 선택 축은 "메시지 문자열 vs 기계 판독 키", "부분 성공 표현 가능 여부", "재시도 정책을 어디에 싣나" 세 가지로 갈린다
판단은 이 세 축에서 시작한다. 어느 스타일을 쓰든 "이 에러가 재시도해도 되는가"라는 질문에 한 문장으로 답할 수 없다면, 그 계약은 운영 중 클라이언트가 각자 다르게 해석하게 된다.
REST 에러 계약, RFC 7807로 통일하면 뭐가 갈리나?
REST의 대표 실패 모드는 에러 본문 형식이 엔드포인트마다 다른 것이다. 어떤 엔드포인트는 message, 다른 엔드포인트는 error, 또 다른 엔드포인트는 배열을 반환하면 SDK가 상태 코드별 예외 처리를 엔드포인트마다 중복 구현해야 한다. 나는 사내 API 10여 개를 RFC 7807 Problem Details(type, title, status, detail, instance)로 강제 통일한 뒤에야 클라이언트 쪽 예외 핸들러 코드량이 눈에 띄게 줄었다.
여기에 code: USER_EMAIL_ALREADY_EXISTS 같은 안정 키와 retryable, retry_after_ms, request_id를 추가로 얹으면 타임아웃·레이트리밋을 구분해서 재시도할 수 있다. 반대로 필드 추가는 대체로 하위 호환이지만 필드 삭제·타입 변경·의미 변경은 계약을 깨뜨린다. /v1, /v2 식 버전 분리는 운영은 단순하지만 문서·SDK·모니터링·캐시 키가 버전마다 중복된다는 비용을 감수해야 한다.
GraphQL은 왜 HTTP 200 안에서 실패를 감춰버릴까?
GraphQL은 HTTP 상태 코드로 성공·실패를 구분하지 않고 단일 응답 안에 data와 errors를 함께 담기 때문에, HTTP 200만 성공으로 기록하는 모니터링은 부분 실패를 놓친다. errors[].path, errors[].extensions.code, 추적 ID, 재시도 가능 여부를 규격화하지 않으면 같은 200 응답이 인증 실패·검증 오류·resolver 장애를 전부 같은 모양으로 표현해버린다.
실제로 2025년 REST·GraphQL 비교 논문은 고부하 조건에서 REST가 더 빠른 응답시간과 낮은 메모리 사용량을 보였고, GraphQL은 복잡한 질의에서 응답시간과 오류율이 함께 증가했다고 보고한다. 이는 프로토콜 자체의 우열이라기보다 질의 복잡도와 resolver 구조가 병목을 좌우한다는 사례로 읽어야 한다 출처. 서버가 내부 예외를 그대로 노출하면 구현 세부사항과 보안 정보가 함께 유출되므로, 외부 계약에는 extensions.code만 공개하고 stack trace는 관측 시스템으로만 보내는 편이 안전하다.
gRPC 상태 코드만으로 재시도 정책을 설계해도 될까?
UNAVAILABLE과 DEADLINE_EXCEEDED를 애플리케이션 오류와 구분하지 않고 그대로 재시도 로직에 넘기면 재시도 폭풍이 발생할 수 있다. INVALID_ARGUMENT, UNAUTHENTICATED, PERMISSION_DENIED, NOT_FOUND, RESOURCE_EXHAUSTED를 일관되게 쓰는 건 출발점일 뿐이고, 멱등성·최대 재시도 횟수·지수 백오프·전체 deadline을 계약에 명시해야 실제 운영에서 쓸 수 있다.
protobuf 필드 진화 쪽도 함정이 같은 자리에 있다. 삭제한 필드 번호를 재사용하거나 enum 값을 무방비하게 축소하면 구버전 클라이언트와의 호환성이 깨진다. 삭제한 필드 번호는 예약(reserved)하고 새 필드만 추가하는 규칙을 팀 전체가 지켜야, gRPC의 내부 서비스 간 강점(고동시성·저지연)이 유지된다. 다만 브라우저·캐시·범용 HTTP 도구와의 직접 통합에는 REST보다 추가 계층이 필요하다는 점은 그대로 남는 비용이다.
세 스타일의 지연시간·처리량 수치는 에러 계약과 무슨 관계가 있을까?
수치 자체는 에러 계약 설계와 직접 연결되지 않지만, 어느 구간에서 오류율이 튀는지를 보여주는 참고값으로는 쓸 수 있다. 한 2025년 비교 실험은 p50 기준 gRPC 0.8ms, REST 1.4ms, GraphQL 2.1ms, p99는 각각 2.1ms, 5.2ms, 8.7ms를 기록했고, 동시 연결 500개 기준 처리량은 gRPC 52,100, REST 31,200, GraphQL 21,400으로 제시됐다 출처. 이 수치는 서버 하드웨어·프레임워크 버전·데이터셋이 충분히 공개되지 않아 일반화할 근거로 쓰기는 어렵다.
같은 맥락에서 2026년 문헌 종합은 소형 메시지에서 gRPC가 HTTP/2 멀티플렉싱과 Protocol Buffers 직렬화의 이점을 보이고, REST는 평균 1,500~2,000 RPS 수준을 보였다고 정리하면서도, 세 스타일을 동일 조건에서 비교한 표준화된 프로덕션 규모 벤치마크가 부족하다고 분명히 지적한다 출처. 에러 계약 설계에서 이 수치를 쓸 자리는 "GraphQL의 복잡한 질의가 p95·p99와 오류율을 더 크게 흔든다"는 패턴을 참고하는 정도다.
에러 계약 설계에서 가장 자주 놓치는 함정은 무엇일까?
가장 흔한 함정은 정상 경로만 테스트하고 validation·인증·권한·timeout·rate limit·부분 성공 경로는 계약 검증에서 빠뜨리는 것이다. 한 2025년 비교 글은 부하 상황 오류율을 gRPC 0.25%, REST 0.4%, GraphQL 0.8%로 제시했는데, 독립적인 학술 검증이나 재현 저장소가 확인되지 않아 "한 실험의 관측값" 이상으로 쓰기는 어렵다 출처. 그럼에도 오류율이 스타일별로 균일하지 않다는 패턴 자체는 에러 경로를 별도로 부하 테스트해야 한다는 신호로는 유효하다.
캐시 쪽 함정도 자주 빠진다. REST와 GraphQL 모두 캐시 효율은 URL·쿼리·헤더의 안정성에 좌우되는데, REST는 HTTP 캐시 인프라를 바로 쓰기 쉬운 반면 GraphQL은 POST 본문과 질의 조합을 캐시 키로 따로 설계해야 하고, 사용자별 권한·필드 선택에 따른 캐시 오염을 막는 로직이 추가로 필요하다. 이 작업을 에러 계약 설계와 동시에 하지 않으면 캐시된 에러 응답이 새로운 요청에 잘못 재사용되는 사고로 이어질 수 있다.
그래서 언제 어떤 에러 계약을 고르나?
스타일 선택보다 에러 계약을 먼저 고정한다는 전제 위에서, 상황별 축은 아래처럼 정리된다.
- 공개 API·범용 클라이언트 다수: REST + RFC 7807 Problem Details, 상태 코드와 본문을 함께 계약으로 문서화
- 클라이언트마다 필요한 필드가 다르고 부분 성공이 흔함: GraphQL +
errors[].extensions.code·path 기반 계약, query depth·복잡도 제한 필수 - 내부 서비스 간 고동시성·저지연 통신: gRPC + 표준 상태 코드 + protobuf reserved 필드 관리, 재시도 정책을 deadline과 함께 명시
- 세 경우 모두
message보다code·retryable·trace_id를 먼저 고정하고, 벤치마크 수치는 측정 조건(버전·하드웨어·부하 모델)을 확인한 뒤에만 참고 자료로 쓴다
2026년 기준으로도 세 스타일을 동일 조건에서 비교한 표준화된 프로덕션 규모 벤치마크는 충분치 않다는 지적이 여러 문헌에서 반복된다. 그래서 이 글의 무게중심은 "어떤 스타일이 더 빠른가"가 아니라 "에러 계약을 별도의 안정된 인터페이스로 분리했는가"에 둔다.
핵심 정리
- 에러 응답 규격은 API 스타일 선택보다 먼저 고정해야 하는 계약이다
- REST는 RFC 7807 Problem Details로 상태 코드·본문을 통일하는 것이 출발점이다
- GraphQL은 HTTP 200 안의 부분 성공을
errors[].extensions.code·path로 규격화해야 모니터링이 가능하다 - gRPC는 표준 상태 코드 위에 멱등성·재시도 횟수·deadline을 명시해야 재시도 폭풍을 피한다
- 벤치마크 수치는 측정 조건(버전·하드웨어·부하 모델)이 불명확하면 확정 근거가 아니라 참고값으로만 쓴다