REST vs GraphQL: 캐싱과 에러 처리는 어떻게 다를까?

REST vs GraphQL: 캐싱과 에러 처리는 어떻게 다를까?

API를 개발하거나 선택할 때, 단순히 데이터를 주고받는 방식의 차이(JSON 구조 등)보다 더 깊게 고민해야 할 지점들이 있습니다. 바로 “캐싱을 어떻게 할 것인가?”“에러를 어떻게 처리할 것인가?” 입니다.

이 두 가지 측면에서 REST와 GraphQL은 완전히 다른 접근 방식을 취하고 있습니다. 그 핵심적인 차이를 정리해 봅니다.


1. 캐싱 (Caching) 전략의 차이

가장 큰 차이는 “누가 캐싱을 담당하는가?” 입니다.

REST: “네트워크”가 해줍니다 (HTTP Caching)

REST는 웹의 표준 프로토콜인 HTTP를 그대로 활용합니다. 따라서 URL 자체가 고유한 리소스의 주소가 됩니다.

  • 작동 원리: GET /users/1 요청은 언제나 1번 유저의 정보를 의미합니다.
  • 장점: 브라우저, CDN, 프록시 서버 등이 URL과 HTTP 헤더(Cache-Control, ETag)를 보고 알아서 캐싱을 수행합니다. 개발자가 별도로 복잡한 캐싱 로직을 구현하지 않아도 네트워크 레벨에서 성능 최적화가 가능합니다.

GraphQL: “라이브러리”가 해야 합니다 (Application Caching)

GraphQL은 캐싱 관점에서 두 가지 난관이 있습니다.

  1. 단일 엔드포인트: 모든 요청을 /graphql이라는 하나의 주소로 보냅니다. (URL로 데이터 구분이 불가능)
  2. POST 메서드 사용: 주로 POST를 사용하므로, HTTP 스펙상 기본적으로 캐싱되지 않습니다.

✅ 해결책: 클라이언트 내부 캐싱 (Apollo Client 등) 그래서 GraphQL은 네트워크 대신, Apollo Client 같은 클라이언트 라이브러리가 앱 내부 메모리에서 캐싱을 담당합니다.

  • 정규화(Normalization): 서버에서 받은 데이터를 __typenameid를 조합해 고유 키(예: User:1)로 저장합니다.
  • 재사용: 이후 같은 ID의 데이터가 필요하면 서버 요청 없이 메모리에서 꺼내 씁니다. 데이터가 업데이트되면, 해당 ID를 바라보는 모든 UI가 자동으로 갱신됩니다.

한 줄 요약:

  • REST: URL 기반 캐싱 (네트워크 레벨)
  • GraphQL: ID 기반 정규화 캐싱 (클라이언트 앱 레벨)

2. 에러 처리 (Error Handling) 방식의 차이

두 번째 큰 차이는 성공과 실패를 판단하는 “신호(Signal)”에 있습니다.

REST: 명확한 신호등 (HTTP Status Code)

REST는 HTTP 상태 코드를 통해 요청의 결과를 명확히 알립니다.

  • 200 OK: 성공
  • 400 Bad Request: 클라이언트 요청 실수
  • 401 Unauthorized: 로그인 필요
  • 404 Not Found: 리소스 없음
  • 500 Server Error: 서버 내부 오류

👉 특징: 프론트엔드나 모니터링 도구(Sentry 등)가 상태 코드만 보고도 즉시 에러 여부를 판단할 수 있습니다.

GraphQL: 겉과 속이 다른 200 OK

GraphQL은 네트워크 통신 자체만 성공했다면, 내부 로직에서 에러가 발생해도 대부분 HTTP 200 OK를 반환합니다. 진짜 결과는 응답 본문(Body)을 열어봐야 압니다.

⚠️ 문제점 1: 부분 실패 (Partial Failure) REST는 성공이면 성공, 실패면 실패지만, GraphQL은 “일부만 성공” 할 수 있습니다.

{
  "data": {
    "user": { "name": "Gemini" }, // 유저 정보 조회 성공
    "posts": null                   // 게시글 목록 조회 실패
  },
  "errors": [
    { "message": "DB Error on posts", "path": ["posts"] }
  ]
}

⚠️ 문제점 2: 에러 판단의 모호함

  • 모니터링의 사각지대: HTTP 상태 코드가 200이기 때문에, 기존의 에러 모니터링 도구(Sentry, Datadog 등)가 이를 ‘성공한 요청’으로 착각하여 로그를 남기지 않거나 알림을 보내지 않을 수 있습니다. 별도의 로깅 설정이 필요합니다.
  • 프론트엔드 로직의 복잡성: 클라이언트는 항상 data가 있는지 확인하는 것뿐만 아니라, errors 필드가 존재하는지도 동시에 체크해야 합니다. 심지어 데이터는 왔는데 에러도 같이 오는 상황(부분 성공)까지 고려하여 if-else 분기 처리를 꼼꼼하게 해야 합니다.

💡 심화: 왜 GraphQL은 에러가 나도 200 OK일까?

이 차이는 “HTTP 프로토콜을 바라보는 관점” 이 다르기 때문입니다. 이해하기 쉽게 택배 기사(HTTP) 에 비유해 보겠습니다.

구분 택배 기사(HTTP)의 행동 비유
REST “내용물을 확인하고 배송한다” 기사가 주소지에 갔는데 사람(데이터)이 없으면, 송장에 ‘수취인 불명(404)’ 도장을 찍습니다.
GraphQL “문 앞까지만 배달한다” 기사는 프론트 데스크(/graphql)에 상자(쿼리)만 잘 전달하면 끝입니다. 송장에 ‘배송 완료(200)’ 도장을 찍고 떠납니다. 상자를 열었을 때 내용물이 있는지는 나중에 직원이 확인합니다.

기술적 이유: 프로토콜 중립성 (Protocol Agnostic) GraphQL은 HTTP에 종속되지 않도록 설계되었습니다. HTTP가 아닌 WebSocket이나 다른 프로토콜을 쓸 수도 있기 때문에, HTTP 상태 코드(404, 500 등)에 의존하지 않고 응답 본문(Body)의 errors 필드를 통해 독자적으로 에러를 표현하는 방식을 택했습니다.


3. 요약 및 비교 분석

구분 REST GraphQL
캐싱 주체 브라우저, CDN (네트워크 레벨) 클라이언트 라이브러리 (앱 메모리)
캐싱 기준 (Key) URL (리소스 주소) ID (__typename + id)
에러 신호 HTTP 상태 코드 (4xx, 5xx) Response Body의 errors 배열
성공/실패 여부 성공(2xx) 아니면 실패(4xx, 5xx) 부분 성공(Partial Success) 가능

4. 결론

기술 선택은 결국 트레이드오프(Trade-off)의 문제입니다.

  • REST는 웹의 표준(HTTP)을 충실히 따르기 때문에 인프라(CDN, 로드밸런서)의 지원을 받기 쉽고, 디버깅이 직관적입니다.
  • GraphQL은 네트워크 레벨의 단순함을 포기한 대신, Apollo Client 같은 강력한 라이브러리를 통해 정교한 데이터 관리(정규화된 캐시)유연성(Over/Under-fetching 해결) 을 얻는 방식입니다.

따라서 공개 API나 단순한 서비스라면 REST가 여전히 강력한 선택지이며, 복잡한 데이터 관계를 가진 대규모 프론트엔드 애플리케이션이라면 GraphQL이 주는 이점이 초기 설정의 복잡함을 상쇄하고도 남을 것입니다.