GraphQL — API를 위한 쿼리 언어이며 그 쿼리들을 실행하기 위한 런타임으로, 2012년 Facebook이 개발하고 2015년 오픈소스로 공개했습니다. 서버가 응답 구조를 결정하는 REST와 달리, GraphQL은 클라이언트가 필요한 데이터를 정확히 지정할 수 있게 하여 overfetching과 underfetching 문제를 완전히 해결합니다. State of JavaScript Survey(2025)에 따르면, 설문조사된 개발자의 35%가 GraphQL을 사용하고 있으며, 대기업 중에서는 GitHub, Shopify, Airbnb 그리고 The New York Times가 채택했습니다. GraphQL은 query(읽기), mutation(쓰기) 그리고 subscription(WebSocket을 통한 실시간 업데이트) 세 가지 작업 유형을 지원합니다.
핵심 요점
GraphQL — API를 위한 명세 및 런타임으로, 클라이언트가 수신하는 데이터에 대한 완전한 컨트롤을 제공합니다. News Feed 모바일 애플리케이션의 문제를 해결하기 위해 Facebook 엔지니어가 개발했으며, 2015년에 오픈 표준으로 공개되었습니다. 2018년 이후로 GraphQL은 Linux Foundation과 Apollo, AWS, GitHub, SAP 등 기업들의 지원을 받아 GraphQL Foundation이 관리하고 있습니다.
각 endpoint가 고정된 데이터 구조를 반환하는 REST와 달리, GraphQL은 쿼리 문자열을 수락하는 단일 endpoint를 사용합니다. 클라이언트는 쿼리에서 필요한 필드를 설명하고, 서버는 꼭 그것만을 반환합니다. 예를 들어, 쿼리 { user(id: "1") { name email } }는 사용자의 이름과 이메일만 반환하며, REST에서 가져와야 하는 address, phone 또는 createdAt과 같은 추가 필드가 없습니다.
GraphQL은 특정 데이터베이스나 언어에 종속되지 않습니다. 명세는 쿼리와 응답 형식만을 정의합니다. Node.js(graphql-js, Apollo Server), Kotlin(graphql-kotlin, Netflix DGS Framework), Python(Graphene, Strawberry), Ruby(graphql-ruby) 및 기타 언어로 서버 구현이 존재합니다. iOS, Android 및 웹용 Apollo Client를 포함한 클라이언트 라이브러리가 모든 주요 플랫폼에서 사용 가능합니다.
GraphQL 아키텍처는 세 가지 핵심 컴포넌트로 구성됩니다: 스키마(Schema), 리졸버(Resolvers) 그리고 GraphQL 엔진(GraphQL Engine). 스키마는 어떤 데이터 타입이 사용 가능한지, 어떤 쿼리를 실행할 수 있는지, 그리고 어떤 인자를 받는지를 정의합니다. 리졸버는 각 스키마 필드에 대한 데이터를 반환하는 서버 측 함수입니다. 엔진은 수신된 쿼리를 받아 스키마에 대해 검증하고, 적절한 리졸버를 호출하여 응답을 집합합니다.
쿼리 처리 흐름은 다음과 같습니다:
GraphQL 아키텍처의 주요 장점은 필드 레벨 해결입니다. REST에서는 개발자가 리소스의 모든 필드를 혹은 추가로 가져오거나 ?fields=name,email 같은 확장에 의존해야 합니다. GraphQL에서는 이러한 필터링이 언어에 내장되어 있습니다: 각 쿼리가 필요한 필드를 명시적으로 지정하고, 서버는 꼭 그것만을 반환합니다. 이는 전송되는 데이터의 양이 로딩 속도와 데이터 사용량에 직접 영향을 미치는 모바일 애플리케이션에서 특히 중요합니다.
GraphQL은 세 가지 작업 유형을 정의하며, 각각은 특정 상호작용 시나리오에 해당합니다. Query — 데이터 읽기 용, REST의 GET과 유사. Mutation — 데이터 변경(생성, 갱신, 삭제) 용, POST/PUT/DELETE와 유사. Subscription — WebSocket을 통한 실시간 업데이트 용, 클래식 REST에는 직접적인 유사가 없음(WebSocket 또는 Server-Sent Events와 같은 추가 솔루션 필요).
기본 쿼리 구문은 직관적입니다:
// 인자를 가진 단순 쿼리
query {
user(id: "42") {
name
email
avatarUrl
}
}
// 변경된 데이터를 반환하는 Mutation
mutation {
updateProfile(name: "이반") {
id
name
updatedAt
}
}
// Subscription — 실시간 업데이트 리스닝
subscription {
newMessage(chatId: "chat_1") {
id
text
sender { name }
}
}
Query는 병렬로 실행됩니다 — 동일 레벨의 모든 필드가 동시에 로딩됩니다. 이를 통해 여러 round-trip 없이 하나의 요청으로 관련 데이터(사용자와 그의 게시물)를 로딩할 수 있습니다. Mutation은 순차적으로 실행됩니다 — 하나의 요청에서 뮤테이션은 선언 순서대로 하나씩 실행됩니다. Subscription은 WebSocket을 통한 지속적인 연결을 설정하고, 이벤트 발생 시 서버가 데이터를 전송합니다.
작업은 데이터와 쿼리를 분리하기 위한 변수, 조건부 필드 포함을 위한 지시문(@include, @skip) 그리고 필드 셋의 재사용을 위한 프래그먼트를 받을 수 있습니다. 이러한 기능은 GraphQL 쿼리를 유연하고 재사용 가능하게 만들며, 다양한 화면과 컴포넌트가 있는 대형 프로젝트에서 특히 중요합니다.
GraphQL의 핵심에는 모든 가능한 데이터와 API 작업을 설명하는 타입 시스템이 있습니다. 스키마는 서버가 반환할 수 있는 타입과 수락하는 쿼리에 대한 설명입니다. 스키마는 Schema Definition Language(SDL)로 쓰이며 클라이언트와 서버 간의 계약으로 작용합니다. 클라이언트는 내장 탐색(introspection)을 통해 스키마를 얻을 수 있습니다 — API의 완전한 설명을 반환하는 특별 쿼리 __schema.
블로그용 스키마 예시:
// SDL — Schema Definition Language
type User {
id: ID!
name: String!
email: String
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String
author: User!
}
type Query {
user(id: ID!): User
posts(page: Int): [Post!]!
}
느낌표(!)는 non-null 필드를 나타냅니다 — 응답에 포함된다고 보장됩니다. 대괄호[ ]는 목록을 나타냅니다. GraphQL은 스칼라 타입(Int, Float, String, Boolean, ID), 객체 타입, enum, union, interface 그리고 입력 타입(뮤테이션 인자용)을 지원합니다. 엄격한 타입은 API를 자체 문서화하고 클라이언트 도구가 TypeScript 타입, Kotlin 데이터 클래스, Swift 구조체 등의 코드를 생성할 수 있게 합니다.
내장 탐색(Introspection)은 REST에는 없는 GraphQL의 고유한 기능입니다. 클라이언트는 스키마에 쿼리를 보내 모든 타입, 필드, 인자 및 지시문에 대한 완전한 설명을 얻을 수 있습니다. 이는 개발자를 위한 문서와 자동 완성을 자동으로 생성하는 GraphiQL과 Apollo Studio 같은 도구의 기반입니다. 또한 내장 탐색을 통해 스키마가 예상 구조와 일치하는지 확인하는 자동 테스트를 작성할 수 있습니다.
GraphQL과 REST 사이의 선택은 API 설계 시 주요 아키텍처 결정 중 하나입니다. 두 접근 방식 모두 장점과 단점이 있으며, 선택은 프로젝트의 구체적인 요구 사항에 따릅니다. REST는 단순성과 보편성에서 우수하고, GraphQL은 유연성과 쿼리 효율성에서 우수합니다. 비교 표를 확인해 보겠습니다.
| 기준 | REST | GraphQL |
|---|---|---|
| 응답 구조 | 고정, 서버 정의 | 유연, 클라이언트 정의 |
| Overfetching | 추가 데이터 반환 빈번 | 없음 — 필요한 필드만 요청 |
| 요청 수 | 여러 round-trip | 모든 데이터에 대한 단일 요청 |
| 캐시 | 고유 HTTP 캐시 | 수동 구성 필요 |
| 타입 | 내장 없음(형식 의존) | SDL 스키마를 통한 엄격함 |
| 도구 | curl, Postman, Swagger | GraphiQL, Apollo Studio, 내장 탐색 |
| 파일 업로드 | multipart를 통한 고유 지원 | 추가 프로토콜 필요 |
| 성능 | 예측 가능, 최적화 용이 | 중첩 쿼리 복잡성에 의존 |
GraphQL의 주요 단점은 캐시 복잡성입니다. REST에서는 HTTP 캐시가 URL 레벨에서 작동합니다: /api/users/42에 대한 요청은 항상 동일한 구조를 반환하며, 응답을 URL로 캐시할 수 있습니다. GraphQL에서는 모든 요청이 단일 endpoint로 가며, 응답 구조는 요청 바디에 의존합니다. 이 문제를 해결하기 위해 Apollo Client는 클라이언트 측에서 정규화된 캐시를 사용하여 응답을 ID별로 개별 엔티티로 분할하고, 새 데이터를 받으면 자동으로 갱신합니다.
또 다른 중요한 측면은 N+1 문제입니다. 중첩된 데이터(예: 사용자의 게시물과 각 게시물에 대한 댓글)를 요청할 때 GraphQL은 목록의 각 항목에 대해 별도의 SQL 쿼리를 실행할 수 있습니다. 이는 DataLoader를 사용하여 해결됩니다 — 개별 요청을 하나의 배치로 그룹화하고 단일 HTTP 요청 내에서 결과를 캐시하는 유틸리티입니다. REST에서는 개발자가 서버 측에서 응답 구조를 제어하므로 이 문제가 더 잘 발생하지 않습니다.
Apollo Client를 사용한 Kotlin 모바일 애플리케이션에서 GraphQL의 실용적인 예를 살펴보겠습니다. 예제는 일반적인 시나리오를 보여줍니다: 프로필 화면을 위한 데이터 로딩(query), 새 게시물 작성(mutation), 그리고 새 댓글 구독(subscription). 각 예제는 GraphQL 쿼리와 클라이언트 코드를 모두 포함합니다.
하나의 GraphQL 쿼리가 사용자, 최신 게시물 그리고 총 팔로워 수를 로딩합니다. REST에서는 이를 위해 최소 2-3개의 요청(/users/42, /users/42/posts, /users/42/stats)이 필요합니다. GraphQL은 이들을 하나의 round-trip으로 결합하여, 느린 연결에서 화면 로딩 시간을 줄입니다.
// GraphQL 쿼리(.graphql 파일 내)
query ProfileScreen($userId: ID!) {
user(id: $userId) {
name
bio
avatarUrl
posts(limit: 10) {
id
title
createdAt
}
followersCount
followingCount
}
}
// 클라이언트 호출(Apollo Client + Kotlin)
val response = apolloClient
.query(ProfileScreenQuery(userId = "42"))
.execute()
binding.nameText.text = response.data?.user?.name
뮤테이션은 리소스를 생성할 뿐만 아니라 UI 업데이트를 위한 현재 데이터도 반환합니다. __typename 필드는 Apollo Client가 캐시 정규화를 위해 사용합니다 — 클라이언트는 뮤테이션 응답 성공 시 캐시에서 Post 레코드를 자동으로 업데이트합니다.
// GraphQL 뮤테이션
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
createdAt
author {
id
name
}
}
}
// input 타입을 사용한 뮤테이션 호출
val input = CreatePostInput(
title = "GraphQL에 관한 새 게시물",
content = "GraphQL이 API 작업을 단순화..."
)
val result = apolloClient
.mutation(CreatePostMutation(input))
.execute()
모바일 개발 환경에서 REST보다 GraphQL의 중요한 장점은 자동 코드 생성입니다. Kotlin용 Apollo Client(Apollo GraphQL)는 빌드 시에 .graphql 파일로부터 타입 안전한 클래스를 생성합니다. 서버가 스키마를 변경하면 쿼리가 업데이트될 때까지 프로젝트가 빌드되지 않습니다. 이는 응답 구조 변경이 개발 중에 발견되지 않을 수 있는 REST에서 발생하는 런타임 오류를 방지합니다.
GraphQL 에코시스템에는 개발과 운영을 단순화하는 여러 핵심 라이브러리와 도구가 포함됩니다. Apollo Client는 가장 인기있는 클라이언트 라이브러리로, React, iOS, Android 및 Kotlin Multiplatform을 지원합니다. Facebook의 Relay는 데이터 관리와 캐시에 대한 고유한 접근 방식을 가진 React 애플리케이션용 대안입니다. Apollo와 Relay 사이의 선택은 플랫폼과 성능 요구 사항에 따릅니다.
서버 측에서는 Apollo Server(Node.js), Netflix DGS Framework(Kotlin/Java) 그리고 graphql-ruby가 선도적입니다. 스키마 개발과 쿼리 테스트를 위해서는 브라우저에 내장된 대화형 IDE인 GraphiQL이 사용됩니다. Apollo Studio는 생산 환경을 위한 성능 메트릭스, 쿼리 추적 및 스키마 관리를 제공합니다. 별도로 주목할 것은 GraphQL Code Generator — SDL 스키마에서 TypeScript, Kotlin, Swift 및 Dart 타입을 생성하는 도구입니다.
모바일 개발에서는 Apollo Kotlin(Apollo GraphQL)이 특히 주목만합니다 — 코루틴, Flow 및 Multiplatform을 지원하며 완전히 Kotlin으로 작성된 라이브러리입니다. 이를 통해 Kotlin Multiplatform 프로젝트에서 Android와 iOS용 통일된 GraphQL 쿼리를 사용할 수 있습니다. Apollo Kotlin은 캐시를 정규화하고, 필드 레벨 오류(부분 오류)를 지원하며, .graphql 파일로부터 데이터 모델을 자동으로 생성합니다. 이로써 개발 속도와 타입 안전성이 중요한 대형 모바일 프로젝트에서 GraphQL이 선호되는 선택이 되고 있습니다.
자주 묻는 질문
GraphQL은 REST를 대체하지 않습니다, 대신 대안 접근 방식을 제공합니다. REST는 단순한 CRUD API, HTTP 캐시 및 예측 가능한 부하의 공용 API에 더 적합합니다. GraphQL은 많은 관련 데이터가 있는 복잡한 인터페이스에 최적적입니다.
이전은 점진적으로 가능합니다: GraphQL은 기존 REST 서비스 앞에서 게이트웨이(계층)로 작동할 수 있습니다. 많은 기업들이 이전 API를 끄지 않고 REST와 함께 GraphQL을 추가합니다. 완전한 교체는 리졸버 재작성이 필요합니다.
N+1은 목록의 각 항목에 대해 별도의 데이터베이스 쿼리가 실행될 때 발생합니다. DataLoader를 사용하여 해결됩니다 — 개별 요청을 하나로 배치화하고 단일 HTTP 요청 내에서 결과를 캐시하는 라이브러리.
GraphQL 명세는 파일 업로드를 직접 정의하지 않습니다. 실무에서는 base64 인코딩(큰 파일에 부적합), graphql-multipart-request-spec 프로토콜에 따른 multipart 요청 또는 파일용 별도 REST endpoint가 사용됩니다.
GraphQL 보안은 추가 조치가 필요합니다: 중첩 깊이 제한, 쿼리 복잡도 제한, 작업 레벨 레이트 제한. 공용 스키마 내장 탐색은 데이터 구조를 노출할 수 있으므로 생산 환경에서는 비활성화하는 것이 좋습니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.