본문으로 이동
메뉴 여닫기
환경 설정 메뉴 여닫기
개인 메뉴 여닫기
로그인하지 않음
지금 편집한다면 당신의 IP 주소가 공개될 수 있습니다.
GraphQL; 그래프QL
클라이언트가 필요한 데이터의 모양을 직접 적어 요청하는 API용 질의 언어이자 실행 규칙

GraphQL은 Facebook(현 메타 플랫폼즈)이 만든 API용 질의 언어이자, 그 질의를 검증하고 실행하는 방법을 정한 명세다. 서버는 제공할 수 있는 데이터를 타입 시스템(스키마)으로 선언하고, 클라이언트는 그 스키마 안에서 필요한 필드만 골라 중첩된 모양으로 요청한다. 서버는 요청과 같은 모양의 JSON으로 응답한다. 2012년 Facebook 모바일 앱을 위해 내부에서 개발되었고 2015년 공개되었으며, 2018년부터는 리눅스 재단 산하 GraphQL Foundation이 명세를 관리한다.

이름에 "Graph"가 들어가지만 그래프 데이터베이스 질의 언어는 아니다. 특정 데이터베이스나 저장 방식과 무관하게 API 계층에서 동작하며, 뒤쪽 데이터는 관계형 DB, NoSQL, 다른 REST API, 마이크로서비스 어디서 와도 된다. 그래프 데이터베이스용 국제 표준 질의 언어 GQL(ISO/IEC 39075)과도 다른 것이다. 여러 자원을 한 번의 요청으로 가져오고, 화면마다 필요한 필드만 받을 수 있어 모바일 앱과 단일 페이지 애플리케이션의 데이터 계층으로 많이 쓰인다. Facebook, GitHub, Shopify, Yelp 등이 공개 API를 GraphQL로 제공하고, Airbnb, Netflix, Pinterest, AWS, Microsoft 등도 쓰는 것으로 알려져 있다.

시기 사건
2012 Facebook이 iOS·안드로이드 네이티브 앱의 뉴스피드 데이터를 가져오려고 내부 개발을 시작했다. 당시 앱은 웹뷰를 감싼 구조라 성능이 나빴고, 네이티브 앱으로 옮기면서 화면에 필요한 데이터를 한 번에 가져올 방법이 필요했다. Lee Byron, Nick Schrock, Dan Schafer가 공동 창시자로 알려져 있다.
2015-07-02 명세 초안(July 2015 판)을 공개했다.[1]
2015-09-14 Lee Byron이 공식 발표 글을 올리고 명세 작업 초안, 참조 구현 graphql-js, 브라우저 안 탐색 도구 GraphiQL을 공개했다. 이때 이미 Facebook 모바일 앱의 거의 모든 데이터 요청을 처리하며 하루 수천억 건의 API 호출을 받고 있었다. 같은 해 공개된 React용 데이터 프레임워크 Relay에 대한 관심이 GraphQL 공개로 이어졌다.[2]
2016 April 2016, October 2016 판이 나왔다. GitHub이 공개 API 새 버전(v4)을 GraphQL로 내놓는 등 대형 공개 API 채택이 시작되었다.
2018-02-09 스키마 정의 언어(SDL)가 명세에 들어갔다.
2018-06-10 June 2018 판. 구독(subscription)이 정식 연산 유형으로 명세에 포함되었다.[1]
2018-11-06 리눅스 재단이 GraphQL Foundation 설립을 발표했다. 창립 회원은 Airbnb, Apollo, Coursera, Elementl, Facebook, GitHub, Hasura, Prisma, Shopify, Twitter였다.[3]
2019 Apollo가 여러 팀의 스키마를 하나로 합치는 Apollo Federation을 내놓았다.
2021-10-26 October 2021 판.[1]
2024-05 GraphQL 명세 워킹 그룹이 벤더 중립 페더레이션 명세를 만드는 Composite Schemas 워킹 그룹을 발표했다.[4]
2025-09 September 2025 판. 첫 초안 이후 10년 만의 개정으로 30명이 100건 넘게 수정했다. OneOf 입력 객체, 스키마 좌표(schema coordinates), 유니코드 전체 지원, 문서 설명(document descriptions) 등이 들어갔다.[5]

명세 판 이름은 발행 연월을 쓴다: July 2015, October 2015, April 2016, October 2016, June 2018, October 2021, September 2025. 그 사이의 변경은 작업 초안(Working Draft)에 쌓인다.[1] 명세 저장소와 참조 구현은 MIT 라이선스이고, 해마다 GraphQL Conference가 열린다.

2015년 공개 글이 내세운 원칙은 다음과 같다.[2]

  • 응답이 질의 모양을 그대로 따른다.
  • 계층적이다. 객체 사이 관계를 따라 중첩해서 한 번에 요청한다.
  • 강한 타입을 갖는다. 모든 필드에 타입이 있어 실행 전에 검증하고 명확한 오류를 준다.
  • 저장소가 아니라 프로토콜이다. 저장 계층이 아닌 애플리케이션 로직을 노출한다.
  • 자기 기술적(introspective)이다. 서버가 지원하는 타입을 질의로 물어볼 수 있다.
  • 버전이 없다. 필드를 추가하고 낡은 필드는 폐지 표시하며 하위 호환을 유지한다.

타입 시스템과 스키마

편집 원본 편집

서비스가 제공하는 데이터와 연산 전체를 스키마라고 하며, 스키마 정의 언어(SDL)로 적는다. 명세가 정한 타입 종류는 이름 있는 타입 6가지(스칼라, 객체, 인터페이스, 유니언, 열거형, 입력 객체)와 감싸는 타입 2가지(리스트, 널 불가)다.[6]

종류 SDL 키워드 설명
스칼라 scalar 더 쪼갤 수 없는 값. 기본 제공은 Int(부호 있는 32비트 정수), Float, String, Boolean, ID 5개. DateTime처럼 사용자 정의 스칼라를 만들고 @specifiedBy로 형식 명세를 연결할 수 있다.
객체 type 필드 묶음. 필드마다 인자와 반환 타입이 있다.
인터페이스 interface 여러 객체 타입이 공유하는 필드 집합
유니언 union 여러 객체 타입 중 하나(공통 필드 없음)
열거형 enum 정해진 값 목록
입력 객체 input 인자로 넘기는 구조화된 값. @oneOf를 붙이면 필드 중 정확히 하나만 채워야 한다(September 2025 판).
리스트 [T] T의 목록
널 불가 T! null이 될 수 없음. 필드는 기본이 널 허용이다.

루트 연산 타입은 조회용 Query(필수), 변경용 Mutation, 실시간 알림용 Subscription(선택)이다.

"""블로그 글쓴이"""
type User {
  id: ID!
  name: String!
  posts(first: Int = 10): [Post!]!
}

type Post {
  id: ID!
  title: String!
  author: User!
  status: Status!
  oldTitle: String @deprecated(reason: "title을 쓸 것")
}

enum Status { DRAFT PUBLISHED }

input NewPost {
  title: String!
  body: String!
}

type Query {
  user(id: ID!): User
  search(text: String!): [SearchResult!]!
}

union SearchResult = User | Post

type Mutation {
  createPost(input: NewPost!): Post!
}

type Subscription {
  postPublished: Post!
}

[Post!]!는 "목록 자체도 null이 아니고 원소도 null이 아님"을 뜻한다. 기본 제공 지시자(directive)는 @skip, @include, @deprecated, @specifiedBy, @oneOf다.[6]

클라이언트는 필요한 필드만 중첩해 적는다. 변수, 별칭(alias), 프래그먼트(fragment), 지시자를 쓸 수 있다.

query UserPage($id: ID!, $withPosts: Boolean!) {
  user(id: $id) {
    name
    recent: posts(first: 3) @include(if: $withPosts) {
      ...PostSummary
    }
  }
}

fragment PostSummary on Post {
  id
  title
}

변수는 별도 JSON으로 보낸다({"id": "42", "withPosts": true}). 응답은 질의와 같은 모양이다.

{
  "data": {
    "user": {
      "name": "김철수",
      "recent": [
        {"id": "p1", "title": "GraphQL 입문"},
        {"id": "p2", "title": "리졸버 작성법"}
      ]
    }
  }
}

유니언이나 인터페이스처럼 실제 타입이 여러 가지인 필드는 인라인 프래그먼트(... on Post { title })로 타입별 필드를 고르고, __typename으로 실제 타입 이름을 받는다.

데이터를 만들고 고치고 지우는 연산이다. 쿼리처럼 돌려받을 필드를 고른다. 명세는 한 요청 안의 여러 뮤테이션 루트 필드를 순서대로(직렬로) 실행하도록 정한다. 쿼리 필드는 병렬 실행이 허용된다.[6]

mutation {
  createPost(input: {title: "새 글", body: "본문"}) {
    id
    status
  }
}

서버가 이벤트가 생길 때마다 결과를 밀어 주는 연산이다. 구독 연산은 루트 필드를 정확히 하나만 골라야 한다.[6] 명세는 전송 방식을 정하지 않으며, 실제로는 WebSocket(graphql-ws 프로토콜 등)이나 SSE로 구현한다.

subscription {
  postPublished { id title author { name } }
}

실행과 리졸버

편집 원본 편집

서버는 요청을 받으면 구문 분석, 스키마 대비 검증, 실행 순서로 처리한다. 검증 단계에서 없는 필드, 타입이 맞지 않는 인자, 빠진 필수 변수를 실행 전에 걸러 낸다. 실행은 각 필드에 연결된 함수인 리졸버(resolver)를 부모에서 자식 순서로 호출해 값을 채운다. 리졸버는 부모 객체, 인자, 요청 공통 문맥(context, 인증 정보나 DB 연결), 실행 정보를 받는다.

// Apollo Server 예 (Node.js)
import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";

const resolvers = {
  Query: {
    user: (_parent, { id }, ctx) => ctx.db.users.findById(id),
  },
  User: {
    posts: (user, { first }, ctx) => ctx.db.posts.byAuthor(user.id, first),
  },
  Mutation: {
    createPost: (_p, { input }, ctx) => {
      if (!ctx.user) throw new Error("로그인 필요");
      return ctx.db.posts.create({ ...input, authorId: ctx.user.id });
    },
  },
};

const server = new ApolloServer({ typeDefs, resolvers });
await startStandaloneServer(server, {
  context: async ({ req }) => ({ db, user: await auth(req) }),
});

응답은 최상위에 data와 errors를 두고 선택적으로 extensions를 둔다. 오류 객체는 message, locations, path, extensions를 가진다.[6] 한 필드에서 오류가 나도 나머지 필드는 채워 부분 응답을 돌려줄 수 있다. 널 불가 필드에서 오류가 나면 null이 가장 가까운 널 허용 상위 필드까지 번진다.

인트로스펙션

편집 원본 편집

GraphQL 서버는 자기 스키마를 질의로 알려 준다. __schema, __type(name:), __typename 세 메타 필드가 명세에 정의되어 있다.[6]

{
  __type(name: "User") {
    fields { name type { name kind ofType { name } } }
  }
}

GraphiQL 같은 IDE의 자동 완성과 문서 탐색, 스키마에서 TypeScript 타입과 클라이언트 코드를 만드는 코드 생성기, GraphQL Voyager 같은 스키마 시각화 도구가 모두 인트로스펙션을 쓴다. 타입이 있는 스키마와 인트로스펙션 덕분에 자동 테스트 생성 연구도 이뤄진다.

HTTP로 제공하기

편집 원본 편집

명세 자체는 전송 방식을 정하지 않지만, 관례와 별도의 GraphQL over HTTP 명세 초안이 있다.[7]

  • 엔드포인트는 보통 /graphql 하나다. 반드시 하나여야 하는 것은 아니다.
  • POST는 모든 연산에 쓰고, 본문은 {"query": "...", "operationName": "...", "variables": {...}, "extensions": {...}} 형태의 JSON이다. GET은 쿼리에만 쓸 수 있다(/graphql?query={me{name}}).
  • 응답 미디어 타입은 application/graphql-response+json이 권장되고, 예전 서버는 application/json을 쓴다.
  • 인증은 GraphQL 검증 전에, 인가는 리졸버에서 필드 단위로 처리하는 것이 권장된다.
  • 파일 업로드는 명세에 없고, 멀티파트 요청을 쓰는 커뮤니티 규약이 쓰인다.
항목 REST GraphQL
자원 식별 자원마다 URL(/users/1, /users/1/posts) 보통 단일 엔드포인트, 스키마의 필드로 식별
동작 구분 HTTP 메서드(GET, POST, PUT, DELETE) 연산 유형(query, mutation, subscription)
응답 모양 서버가 엔드포인트마다 정함 클라이언트가 필드를 골라 정함
오버페칭 필요 없는 필드까지 받기 쉬움 요청한 필드만 받음
언더페칭 화면 하나에 여러 번 요청(N번 왕복) 중첩 질의로 한 번에
타입·문서 OpenAPI 등 별도 명세(선택) 스키마가 필수이고 인트로스펙션으로 조회
버전 관리 /v1, /v2 식 버전이 흔함 필드 추가와 @deprecated로 버전 없이 진화
HTTP 캐싱 URL 단위로 브라우저·CDN 캐시가 자연스럽게 동작 POST 단일 URL이라 기본 HTTP 캐시가 잘 안 먹힘
오류 표현 HTTP 상태 코드 대개 200 응답 안의 errors 배열(부분 성공 가능)
서버 부하 예측 엔드포인트별로 예측 쉬움 질의에 따라 비용이 크게 달라 제한 장치 필요
파일 전송 자연스러움 명세 밖(별도 규약)

REST에서 게시글 목록 화면을 그리려면 글 목록, 글쓴이 정보, 댓글 수를 각각 요청하거나(언더페칭), 서버가 화면 전용 엔드포인트를 따로 만들어야 한다. 반대로 사용자 이름 하나가 필요할 때도 사용자 객체 전체를 받는다(오버페칭). GraphQL은 이 둘을 클라이언트 쪽 질의로 해결하는 대신 서버 구현의 복잡도를 높인다. REST는 웹 자원을 설계하는 아키텍처 스타일이고 GraphQL은 API를 조회·조작하는 구체적 언어라 서로 완전히 대체 관계는 아니며, 공개 API는 REST, 앱 전용 BFF(Backend for Frontend)는 GraphQL처럼 함께 쓰는 경우가 많다. SOAP, gRPC와 비교하면 GraphQL은 스키마 기반 계약이라는 점은 같지만 호출할 함수가 아니라 가져올 데이터 모양을 적는다는 점이 다르다.

GraphQL은 SQL이나 SPARQL 같은 완전한 질의 언어가 아니다. 서버가 스키마로 열어 둔 필드만 가져올 수 있고, 임의 조건 필터나 조인, 재귀 질의는 서버가 그런 필드를 만들어 두어야만 가능하다. 예를 들어 "부모" 필드만 있으면 한 질의로 모든 조상을 가져올 수 없다.

N+1 문제와 DataLoader

편집 원본 편집

리졸버는 필드마다 따로 실행되므로, 글 목록 N개의 글쓴이를 가져오면 글 목록 조회 1번에 글쓴이 조회 N번이 따로 나가는 N+1 문제가 생기기 쉽다. 표준 해법은 한 이벤트 루프 틱 안에 들어온 개별 키 요청을 모아 한 번에 조회하는 일괄 처리(batching)이며, Facebook이 공개한 DataLoader가 대표적 구현이다. 요청 단위 캐시로 같은 키 중복 조회도 막는다.[8]

import DataLoader from "dataloader";

// 요청마다 새로 만든다(사용자 간 캐시 공유 방지)
const userLoader = new DataLoader(async (ids) => {
  const rows = await db.query("SELECT * FROM users WHERE id = ANY($1)", [ids]);
  const byId = new Map(rows.map((u) => [u.id, u]));
  return ids.map((id) => byId.get(id) ?? null); // 키 순서대로 돌려줘야 한다
});

const resolvers = {
  Post: { author: (post) => userLoader.load(post.authorId) },
};

일부 구현(Hasura, PostGraphile, Join Monster 등)은 질의의 선택 집합 전체를 SQL 한 문장으로 바꿔 N+1을 원천적으로 피한다.

HTTP 캐시는 URL을 키로 삼는데 GraphQL은 대개 한 URL에 POST로 보내므로 브라우저나 CDN 캐시를 그대로 쓰기 어렵다. 쓰이는 방법은 다음과 같다.[8]

  • 쿼리를 GET으로 보내 HTTP 캐시를 쓴다. URL 길이 제한이 있으므로 질의 대신 해시만 보내는 영속 질의(persisted queries)와 함께 쓴다. 미리 등록한 질의만 받는 방식을 신뢰 문서(trusted documents)라 하고, 처음 본 해시면 전체 질의를 한 번 받아 등록하는 방식을 자동 영속 질의(APQ)라 한다.
  • 클라이언트 쪽 정규화 캐시. Apollo Client와 Relay는 응답을 __typename과 id로 쪼개 객체 단위로 저장하므로, 한 화면에서 고친 객체가 다른 화면에도 반영된다.
  • 서버 쪽에서는 리졸버 단위 캐시나 필드별 캐시 힌트(Apollo의 @cacheControl 등)를 쓴다.

클라이언트가 질의 모양을 마음대로 정하므로 REST보다 서버를 과부하시키기 쉽다. GraphQL Foundation과 OWASP 치트시트가 권하는 대책은 다음과 같다.[9][10]

위협 예 대책
깊은 중첩 질의 user { friends { friends { friends { ... } } } }처럼 순환 관계를 따라 결과가 기하급수로 커짐 최대 깊이 제한, 목록 필드는 더 작은 한도
넓은 질의·별칭 남용 한 요청에 같은 필드를 별칭으로 수천 번 요청 최상위 필드 수, 별칭 수, 배치 연산 수 제한
비싼 질의 목록 안의 목록을 한도 없이 요청 목록 필드 페이지 나누기(first/last 필수), 필드별 가중치로 비용 계산해 한도 초과 시 거절(질의 복잡도 분석), 실행 시간 제한
무차별 대입 우회 로그인이나 OTP 검증 뮤테이션을 별칭으로 한 요청에 수백 개 넣어 요청 수 기반 속도 제한과 WAF를 우회 요청 수가 아니라 연산·객체 단위로 속도 제한, 민감한 필드는 배치 금지
스키마 노출 인트로스펙션으로 내부용 필드와 관리자 뮤테이션을 파악, 필드 이름 오타에 "혹시 ~인가요?" 제안으로 정찰 운영 환경에서 인트로스펙션과 GraphiQL 끄기, 필드 제안 끄기, 오류 상세·스택 추적 숨기기(단, 숨기는 것만으로는 부족)
인가 누락(IDOR) node(id:)나 중첩 경로로 남의 객체에 접근 경로가 아니라 객체와 필드 단위로 리졸버에서 권한 확인
주입 인자를 그대로 SQL·명령에 붙임 매개변수화 질의 사용(SQL 인젝션 참고)
임의 질의 공격자가 자유롭게 만든 질의 자사 앱 전용 API라면 신뢰 문서(허용 목록)만 실행

페더레이션과 대규모 운영

편집 원본 편집

큰 조직에서는 팀마다 따로 GraphQL 서비스를 두고 하나의 스키마로 합쳐 제공한다. 초기에는 여러 스키마를 게이트웨이에서 이어 붙이는 스키마 스티칭(schema stitching)을 썼고, 2019년 Apollo Federation은 각 서비스가 자기 타입을 정의하고 @key 등으로 다른 서비스의 타입을 확장하면 라우터가 질의를 서비스별로 나눠 실행하는 방식을 도입했다. 이후 여러 벤더가 비슷한 방식을 내놓자 GraphQL Foundation의 Composite Schemas 워킹 그룹이 Apollo, ChilliCream, Graphile, Hasura, Netflix, The Guild, WunderGraph 등의 엔지니어와 함께 벤더 중립 명세를 만들고 있다.[4] 마이크로서비스 아키텍처에서 API 게이트웨이 역할을 GraphQL 라우터가 맡는 구성이 흔하다.

Relay는 커서 기반 페이지 나누기 규약(Connection: edges, node, cursor, pageInfo)과 전역 객체 식별 규약(Node 인터페이스와 node(id:) 필드)을 정했고, Relay를 쓰지 않는 API도 이 규약을 널리 따른다. 응답 일부를 먼저 보내는 @defer, @stream 지시자는 명세 워킹 그룹에서 제안·논의 중이며 일부 구현이 먼저 지원한다.

언어·분류 구현
명세 참조 구현 graphql-js (JavaScript)
JavaScript 서버 Apollo Server, GraphQL Yoga(The Guild), Mercurius, Pothos·Nexus(코드 우선 스키마)
JavaScript 클라이언트 Apollo Client, Relay(메타), urql, graphql-request
자바·JVM graphql-java, Spring for GraphQL, Netflix DGS, Sangria(Scala)
파이썬 Graphene, Strawberry, Ariadne
기타 언어 graphql-ruby, gqlgen·graphql-go(Go), async-graphql·Juniper(Rust), Hot Chocolate·GraphQL.NET(C#), webonyx/graphql-php(PHP)
DB에서 자동 생성 Hasura, PostGraphile(PostgreSQL), Supabase pg_graphql
관리형 서비스 AWS AppSync, Apollo GraphOS, Azure API Management의 GraphQL 지원
도구 GraphiQL, Apollo Studio Explorer, GraphQL Voyager, GraphQL Code Generator, GraphQL Hive, Postman 등

스키마를 SDL로 먼저 쓰고 리졸버를 붙이는 스키마 우선(schema-first) 방식과, 코드의 클래스·타입에서 스키마를 만들어 내는 코드 우선(code-first) 방식이 있다.