REST(Representational State Transfer)와 GraphQL을 둘러싼 논쟁은 단순한 선호도를 넘어, 워크로드의 특성, 캐싱 아키텍처, 클라이언트 환경 및 운영 복잡성을 종합적으로 고려하는 정교한 엔지니어링 평가의 영역으로 성숙했습니다. GraphQL을 REST의 무조건적인 대체제로 여기기보다는 각 모델의 기술적 트레이드오프를 정확히 이해하는 것이 필수적입니다.
이 아키텍처 가이드에서는 리소스 중심의 REST와 그래프 기반 쿼리 모델의 차이점을 분석하고, N+1 문제 해결, HTTP 엣지 캐싱의 장단점, 그리고 실무 시스템을 위한 의사결정 기준을 제시합니다.
아키텍처 패러다임: 리소스 vs 그래프
두 패러다임은 분산 데이터에 접근하는 철학에서 근본적인 차이를 보입니다.
REST (리소스 지향):
GET /api/v1/users/42 ──► [사용자 데이터]
GET /api/v1/users/42/orders ──► [주문 목록]
GET /api/v1/products/808 ──► [상품 상세]
(여러 차례의 네트워크 왕복 또는 비대해진 복합 엔드포인트)
GRAPHQL (그래프 지향):
POST /graphql
{
user(id: "42") {
name
orders { id total product { title price } }
}
} ──► [단 1회의 왕복으로 정확히 요청한 필드 수신]
REST 모델
고유한 URI로 식별되는 리소스를 표준 HTTP 메서드(GET, POST, PUT, DELETE)를 통해 조작합니다.
- 장점: HTTP 프로토콜과의 완벽한 일치, 풍부한 인프라 지원, 결정론적 URL 구조, CDN 엣지 캐싱과의 완벽한 호환성.
- 단점: 불필요한 데이터를 많이 내려받는 **오버페칭(Over-fetching)**과 복합 화면을 그리기 위해 여러 요청을 연속 호출하는 **언더페칭(Under-fetching)**이 발생하기 쉽습니다.
GraphQL 모델
강력한 정적 타입 시스템과 쿼리 언어를 기반으로, 클라이언트가 명시적으로 요청한 필드만을 정확하게 조합하여 반환합니다.
- 장점: 오버페칭 완벽 제거, 한 번의 네트워크 요청으로 연관 데이터 일괄 조회, 프론트엔드와 백엔드 간의 명확한 스키마 계약.
- 단점: HTTP 표준 캐싱 활용의 어려움, 악의적이거나 비효율적인 깊은 중첩 쿼리 방어 필요, 그리고 고질적인 N+1 쿼리 문제.
캐싱 비교: HTTP 엣지 캐시 vs 정규화된 클라이언트 스토어
대규모 트래픽 환경에서 API 확장성은 캐싱 효율성에 의해 결정됩니다.
REST: CDN을 통한 무비용 엣지 캐싱
REST의 GET 요청은 고유한 경로를 가지므로 Cloudflare, AWS CloudFront와 같은 CDN 엣지 서버에서 즉각적으로 응답을 캐시할 수 있습니다:
HTTP/1.1 200 OK
Cache-Control: public, max-age=3600, stale-while-revalidate=60
ETag: W/"33a64df551425fcc55e4d42a148795d9f25f89d4"
Content-Type: application/json
엣지 캐시가 적중되면 백엔드 서버나 데이터베이스를 전혀 거치지 않고 수 밀리초 내에 전 세계 사용자에게 데이터를 서빙합니다.
GraphQL: POST 기반 쿼리의 캐싱 한계
GraphQL 쿼리는 일반적으로 단일 /graphql 엔드포인트에 대한 HTTP POST로 전송되기 때문에 표준 HTTP 프록시나 CDN이 URL만으로 캐시 유효성을 판단하기 어렵습니다. 클라이언트 측에서 Apollo Client와 같은 정규화된 인메모리 캐시를 관리하거나 APQ(Automatic Persisted Queries) 기법을 병행해야 합니다.
DataLoader를 통한 N+1 쿼리 문제 해결
GraphQL 서버에서 중첩된 리졸버를 단순하게 구현하면 데이터베이스 쿼리가 기하급수적으로 폭증하는 N+1 문제가 발생합니다. 이를 해결하는 표준 아키텍처 패턴이 바로 DataLoader를 통한 배치 처리와 메모이제이션입니다:
import DataLoader from 'dataloader';
import { db } from './database';
interface Order {
id: string;
userId: string;
total: number;
}
export function createOrderLoader() {
return new DataLoader<string, Order[]>(async (userIds: readonly string[]) => {
// 단일 배치 SQL로 통합: SELECT * FROM orders WHERE user_id = ANY($1)
const orders = await db.query(
`SELECT * FROM orders WHERE user_id = ANY($1)`,
[userIds]
);
const orderMap = new Map<string, Order[]>();
for (const userId of userIds) {
orderMap.set(userId, []);
}
for (const order of orders) {
orderMap.get(order.userId)?.push(order);
}
return userIds.map((id) => orderMap.get(id) || []);
});
}
실무 의사결정 매트릭스
REST를 선택해야 하는 경우:
- 공개 API 및 외부 파트너 연동: 전 세계 모든 개발자 도구 및 HTTP 클라이언트와 범용적으로 호환되어야 할 때.
- 읽기 집약적이고 엣지 캐싱이 필수적인 서비스: 대규모 뉴스 포털, 미디어 카탈로그, 전자상거래 상품 목록.
- 대용량 파일 업로드 및 바이너리 스트리밍: 멀티파트 업로드 및 스트리밍 파이프라인.
GraphQL을 선택해야 하는 경우:
- 다양한 클라이언트 플랫폼 지원: 화면 크기와 통신 속도가 다른 모바일, 웹, 스마트워치 환경에 최적화된 페이로드가 필요할 때.
- 복합 대시보드 UI: 여러 마이크로서비스의 데이터를 한 화면에 집약하여 렌더링해야 할 때.
- 빠른 프론트엔드 기능 반복 개발: 백엔드 API 추가 개발 없이 클라이언트 주도로 화면을 유연하게 구성하고자 할 때.