들어가며
Gatsby로 블로그를 운영하며 GraphQL을 가볍게 접해본 후, 그 독특한 방식이 호기심이 자극되어서 깊이 있게 공부를 시작했습니다. 쿼리를 통해 필요한 데이터만 골라 가져오는 방식이 과연 REST API보다 어떤 점이 좋을지 궁금했고, 공부할수록 GraphQL은 활용성이 높은 기술이라는 것을 느꼈습니다. 이번 포스트에서는 GraphQL의 핵심 개념을 살펴보겠습니다
왜 GraphQL 이 탄생했을까?
서론에서 언급했듯이, GraphQL은 기존 REST API의 구조적 한계를 극복하기 위해 탄생했습니다. 서비스가 복잡해질수록 REST 방식은 데이터를 주고받는 과정에서 비효율성이 커지는데, 대표적으로 두 가지 문제가 발생합니다.
-
오버페칭(Over-fetching): 필요한 것보다 더 많이 받음, 클라이언트에서 사용자의
이름만 필요한 상황임에도 불구하고, 서버의 엔드포인트에서 사용자의주소,연락처,가입일등 불필요한 정보까지 모두 내려받는 현상입니다. 이는 네트워크 대역폭을 낭비하고 모바일 환경에서 성능 저하의 원인이 됩니다. -
언더페칭(Under-fetching): 필요한 것보다 적게 받아 여러 번 요청함, 하나의 화면을 구성하기 위해 여러 개의 엔드포인트를 호출해야 하는 상황입니다. 예를 들어 게시글 내용과 작성자 정보, 댓글 목록을 가져오기 위해 각각
/posts,/users,/comments로 세 번의 요청을 보내야 합니다. 이 과정에서 언더페칭과 더불어 각 API 응답마다 원치 않는 데이터까지 섞여 들어오는 오버페칭이 동시에 발생하기도 합니다.
페이스북의 고민에서 부터 시작
오버페칭, 언더페칭 문제는 서비스 규모가 커질수록 치명적이었습니다.
2012년 당시, 페이스북은 모바일 앱으로의 전환 과정에서 성능 문제에 직면했습니다. 뉴스피드처럼 복잡하고 다양한 데이터가 얽혀 있는 화면을 구현할 때, REST API로는 수많은 네트워크 요청과 데이터 낭비를 감당하기에는 한계가 있었습니다.
그러하여 페이스북 개발팀은 "클라이언트가 직접 필요한 데이터의 구조를 정의하고, 단 한 번의 요청으로 원하는 데이터만 받을 수 없을까?" 라는 질문을 던졌고, 그 해답으로 GraphQL이 탄생하였습니다.
핵심 개념 알아보기
추상적인 개념을 알아보기 전에 어떻게 동작하는지 예를 먼저 들어보겠습니다.
GraphQL 은 클라이언트가 내가 필요한 데이터가 무엇인지 요청하면 서버에서 알려주면 서버는 그에 맞는 데이터를 돌려주는 방식이며, 이때 요청은 쿼리를 사용합니다.
블로그 포스트의 제목과 작성자 정보 요청(Query)
{
post(id: 1) {
title
author {
name
}
}
}서버의 응답
{
"data": {
"post": {
"title": "그래프큐엘 시작하기",
"author": { "name": "우디" }
}
}
}예시와 같이 클라이언트가 작성한 모양 그대로 서버의 응답이 JSON 형태로 돌아옵니다.
이러한 형태가 가능하려면 클라이언트는 서버가 "post 라는 데이터를 줄 수 있다", "id 를 입력하면 해당 id 에 대한 post 정보만 준다.", "post 에는 title, author 정보를 줄 수 있다." 등에 대한 정보를 알고 있다는 건데요.
이것을 가능하게 하는 것이 GraphQL 서버의 자기소개서와 같은 스키마와 타입입니다.
핵심1. 스키마와 타입(The Schema & Types)
스키마(Schema)는 GrahpQL 의 설계도이며 타입(Type)으로 구성되어 입니다.
먼저 타입부터 살펴보면
타입(Type)
타입에는 세 가지 종류가 있습니다.
- 스칼라 타입(Scalar Type): 가장 기본적인 데이터 조각입니다. 아래가 기본적으로 제공해주는 GrpahQL 스칼라 타입입니다.
- String: 글자 (예: "우디")
- Int: 정수 (예: 25)
- Float: 실수 (예: 4.5)
- Boolean: 참/거짓 (예: true)
- ID: 고유 식별자 (객체를 식별할 때 쓰는 특수한 값)
- 커스텀 스칼라(Custom Scalar): 사용자가 직접 생성한 스칼라 타입(날짜(Date)처럼 기본 타입 외에 특별히 정의한 타입)
- 객체 타입(Object Type): 스칼라 타입들을 모아서 만든 '복합 데이터'입니다. 우리가 실제 서비스에서 다루는 **사용자(User)**나 포스트(Post) 같은 데이터 입니다.
- 열거형 타입(Enum types): 미리 정의된 특정 값들의 집합 중 하나를 선택하도록 강제하는 특별한 타입입니다. 정해진 옵션 이외의 값은 허용하지 않을 때 사용합니다.
# 포스트의 상태를 정의하는 열거형 타입
enum PostStatus {
DRAFT # 초안
PUBLISHED # 발행됨
HIDDEN # 숨김
}
type Post {
id: ID
title: String # 스칼라 타입
content: String # 스칼라 타입
author: User # 다른 객체 타입과의 관계
status: PostStatus # 열거형 타입을 필드로 사용
}느낌표(!)를 통한 널 허용/비허용
타입 뒤에 붙는 느낌표(!)는 "이 값은 절대 비어있을 수 없다(Null이 될 수 없다)" 는 약속입니다.
- String: 이름이 없을 수도 있음 (Null 허용)
- String!: 이름이 무조건 있어야 함 (Null 비허용)
type Post {
id: ID! # ID는 무조건 있어야 합니다.
title: String! # 제목 없는 포스트는 존재할 수 없습니다.
content: String # 본문은 비어있을 수도 있습니다.
}이렇게 느낌표 하나로 데이터의 필수 여부를 명확하게 정할 수 있어, 스키마 레벨에서 데이터의 무결성을 강제하고 클라이언트의 런타임 에러를 사전에 방지합니다.
스키마(Schema)
스키마는 위에서 만든 타입들을 한데 모으고, 클라이언트가 어떤 행동(읽기, 쓰기 등) 을 할 수 있는지 정의한 최종 문서입니다.
스키마를 보면 이 서비스의 모든 것을 알 수 있습니다.
실제 스키마 파일은 보통 이런 식으로 구성됩니다. 마치 메뉴판의 '전체 카테고리'를 정해주는 것과 같습니다.
# 1. 데이터 타입 정의
type Post {
id: ID
title: String
author: User
}
type User {
id: ID
name: String
}
# 2. 할 수 있는 행동(Query, Mutation 등) 정의
type Query {
# "id를 주면 Post 하나를 줄게" 라는 약속
post(id: ID): Post
}
type Mutation {
# "제목을 주면 새로운 포스트를 만들고 그 결과를 보여줄게" 라는 약속
createPost(title: String): Post
}요약하자면 스키마는 "어떤 타입의 데이터를(Type), 어떤 방식으로(Query/Mutation) 주고받을 것인가" 에 대한 서버와 클라이언트의 표준이라고 할 수 있습니다.
마치 REST API 에서 API 명세서(API Specification)를 정의한 것과 같습니다.
스키마&타입 작성의 장점
- 안정성: 정의되지 않은 엉뚱한 데이터를 요청하면 서버가 실행되기도 전에 "그건 없는 데이터야"라고 알려줄 수 있습니다.
- 협업 효율: 프론트엔드 개발자는 스키마만 보고도 서버 개발자에게 물어볼 필요 없이 어떤 데이터를 받아올지 알 수 있습니다.
이제 스키마와 타입이 준비되었습니다. 그럼 이 스키마를 보고 실제로 데이터를 요청하는 방법인 **쿼리(Query)**에 대해 알아보겠습니다.
핵심2. 쿼리(Query) - 데이터 읽기
쿼리는 클라이언트가 서버에 데이터를 요청하는 방식입니다. REST API의 GET 요청과 비슷하지만, 훨씬 더 효율적입니다.
REST API는 서버가 정해준 데이터를 통째로 받아야 했지만, GraphQL 쿼리는 내가 필요한 필드(Field) 만 적어서 보냅니다.
만약 블로그 포스트 id:1 의 제목, 작성자 이름을 가져오고 싶으면
- REST API: /posts/1 호출 시 제목, 본문, 작성일, 댓글 등 모든 데이터를 다 줍니다. (Overfetching)
- GraphQL: "제목이랑 작성자 이름만 줘!"라고 요청하면 딱 그 데이터만 줍니다.
# "포스트 1번의 제목(title)과 작성자 이름(name)만 필요해"
{
post(id: 1) {
title
author {
name
}
}
}위와 같이 스키마만 알면 어떤 데이터를 가져올지는 클라이언트에게 주도권(Client-driven)' 이 있습니다.
프래그먼트(Fragment) - 쿼리의 재사용
쿼리를 작성하다 보면 여러 곳에서 공통된 필드들을 반복해서 적어야 할 때가 있습니다. 이때 프래그먼트를 사용하면 중복되는 필드 세트를 하나로 묶어 재사용할 수 있습니다.
# 공통 필드를 프래그먼트로 정의
fragment PostDetails on Post {
id
title
content
}# 쿼리에서 사용
{
post(id: 1) {
...PostDetails
}
}마치 레고 블록을 미리 조립해두고 필요할 때마다 끼워 쓰는 것과 같아 코드가 훨씬 간결해집니다.
핵심3. 뮤테이션(Mutation) - 데이터 수정
뮤테이션은 REST API의 POST, PUT, DELETE를 하나로 합쳐놓은 것이라고 생각하면 쉽습니다.
쿼리가 데이터를 가져오는 '읽기' 전용이라면, **뮤테이션(Mutation)**은 서버의 데이터를 변경하는 '쓰기' 전용 통로입니다. 이름 그대로 데이터에 '변화(Mutation)'를 주는 작업이죠.
CUD(생성, 수정, 삭제) 작업 처리
뮤테이션을 사용하면 새로운 포스트를 만들거나(Create), 기존 내용을 고치거나(Update), 마음에 안 드는 데이터를 지우는(Delete) 작업을 할 수 있습니다.
# 새로운 포스트를 작성하는 Mutation 예시
mutation {
createPost(title: "뮤테이션 배우기", content: "생각보다 쉬워요") {
id
title
createdAt
}
}위 코드는 createPost 로 title, content를 인자로 전달하고, 작업 성공 시 id, title, createdAt 필드를 받도록 요청하는 예시입니다.
{
"data": {
"createPost": {
"id": "101",
"title": "뮤테이션 배우기",
"createdAt": "2026-01-15T10:00:00Z"
}
}
}인풋 타입(input type) - 뮤테이션의 인자를 편하게
뮤테이션을 사용할 때 보낼 데이터가 많아지면(예: 제목, 본문, 카테고리, 태그 등) 인자가 너무 길어져서 가독성이 떨어집니다. 이때 관련된 인자들을 하나로 묶어주는 것이 바로 input 타입입니다.
- 깔끔한 코드: 여러 개의 인자를 하나하나 나열하지 않고, '가방'에 담아 한 번에 전달하는 것과 같습니다.
- 재사용성: 데이터를 생성할 때와 수정할 때 똑같은 input 구조를 재사용할 수 있어 편리합니다.
# 1. input 타입 정의
input CreatePostInput {
title: String!
content: String!
category: String
}
# 2. 뮤테이션에서 사용
mutation {
createPost(input: { title: "안녕", content: "반가워" }) {
id
}
}일반 타입(Type) vs 인풋 타입(Input)
겉모습은 비슷하지만 용도가 엄격히 구분됩니다.
- 객체 타입 (Object Type): 서버가 클라이언트에게 데이터를 보낼 때 사용하는 '출력용' 규격입니다.
- 인풋 타입 (Input Type): 클라이언트가 서버에 데이터를 보낼 때 사용하는 '입력용' 규격입니다.
참고: GraphQL에서는 보안과 구조상의 이유로 일반 객체 타입을 뮤테이션의 인자로 직접 사용할 수 없게 설계되어 있습니다. 그래서 꼭 input 키워드를 사용해야 합니다.
핵심 4: 서브스크립션 (Subscription) - 실시간 데이터
쿼리가 '질문과 답변'이라면, 서브스크립션은 **'구독과 알림'**입니다. 클라이언트가 특정 이벤트가 발생했을 때 알려달라고 서버에 요청해두면, 서버가 실시간으로 데이터를 밀어주는(Push) 방식입니다.
1. 웹소켓(WebSocket) 기반의 실시간 통신
일반적인 HTTP 요청은 클라이언트가 먼저 말을 걸어야 응답을 받을 수 있지만, 서브스크립션은 웹소켓(WebSocket) 프로토콜을 사용합니다. 한 번 연결을 맺어두면 서버와 클라이언트가 계속 연결된 상태를 유지하며 데이터를 주고받을 수 있게 됩니다.
2. 어떻게 사용하나요?
주로 새로운 댓글이 달렸거나, 채팅 메시지가 왔을 때처럼 '특정 조건이 충족되는 순간' 데이터를 전송합니다.
# "누군가 댓글을 달면(commentAdded), 그 댓글의 본문과 작성자를 실시간으로 보내줘!"
subscription {
commentAdded(postId: "101") {
content
author
}
}3. 주요 특징
- 이벤트 중심(Event-driven): 서버에서 특정 데이터의 변화(주로 뮤테이션 발생)가 감지될 때 동작합니다.
- 지속성: 연결이 끊어지기 전까지는 클라이언트가 별도의 추가 요청을 보내지 않아도 최신 데이터를 계속 받아볼 수 있습니다.
- 활용 사례: 실시간 채팅, 주식 시황 알림, 배달 위치 추적 등 실시간성이 중요한 기능에 필수적입니다.
핵심5. 리졸버(Resolver) - 데이터 채우기
스키마가 데이터의 생김새를 정의한 '설계도'라면, 그 설계도에 맞춰 실제로 데이터를 채워넣는 함수가 바로 리졸버입니다.
1. 리졸버란?
클라이언트로부터 쿼리가 들어왔을 때, 해당 필드에 어떤 값을 돌려줄지 결정하는 실행 함수입니다. 스키마에 정의된 모든 필드는 사실 각각의 리졸버 함수를 가지고 있습니다.
"스키마는 '무엇(What)'을 줄지 약속하고, 리졸버는 그 데이터를 '어떻게(How)' 가져올지 담당합니다."
2. 데이터의 소스를 가리지 않는 유연성 리졸버의 가장 강력한 특징은 데이터가 어디에 있든 상관없다는 점입니다. 하나의 쿼리 안에서도 여러 리졸버가 각각 다른 곳에서 데이터를 긁어와 합칠 수 있습니다.
- DB: 사용자 정보는 MySQL에서 가져오고,
- REST API: 포스트 정보는 기존 REST API에서 가져오고,
- Third-party: 날씨 정보는 외부 기상청 API에서 가져와서 하나로 묶어줍니다.
3. 동작 예시
// 리졸버 함수의 예 (JavaScript 스타일)
const resolvers = {
Query: {
// "post 쿼리가 들어오면 DB에서 해당 id를 찾아줘!"
post: (parent, args, context) => {
return db.Posts.findOne({ id: args.id });
}
}
};4. 리졸버가 주는 이점
클라이언트는 뒤에 DB가 있는지, 다른 API가 있는지 알 필요가 없습니다. 오직 GraphQL 엔드포인트 하나에만 물어보면 리졸버들이 각자의 위치에서 데이터를 모아 완벽한 하나의 응답을 만들어주기 때문입니다.
마치며
이로써, GraphQL 의 핵심 개념들에 대해서 살펴봤습니다.
간단하게 필요한 요소들만 설명하였는데요. 그래프 이론을 알고 GraphQL 을 배우면 이해하는데 더욱더 도움이 되지만, 그렇게 되면 포스팅을 하나 더 올려야되는 수준이라 설명드리지는 못 하였습니다.
앞으로 GraphQL을 계속해서 사용해야되는 상황이라면 찾아서 공부해 보시는 것을 추천드립니다.
다음 포스팅 주제는 시간이 되면 Client(Android,), Server(Spring Boot) 를 사용하여 실습하는 예제를 올려보도록 하겠습니다.
GraphQL의 장단점 정리
마지막으로 간단하게 GraphQL의 장단점을 정리해 보겠습니다.
장점
- 생산성 향상: 클라이언트가 필요한 데이터를 직접 정의하므로, API 변경을 위해 백엔드 개발자를 기다리는 시간이 획기적으로 줄어듭니다.
- 자기 문서화(Self-documenting): 스키마 자체가 곧 완벽한 API 명세서입니다. 별도의 문서 도구 없이도 인트로스펙션 기능을 통해 실시간으로 API 구조를 확인할 수 있습니다.
- 프론트-백엔드 의존성 분리: 서버는 데이터 소스 제공에 집중하고, 클라이언트는 화면 구성에 집중하는 진정한 의미의 관심사 분리가 가능해집니다.
단점
- 캐싱의 복잡성: REST API는 URL 단위로 캐싱하기 쉽지만, GraphQL은 요청마다 본문(Body)이 다르기 때문에 HTTP 캐싱을 그대로 쓰기 어렵습니다. (Apollo 등 별도 라이브러리의 도움 필요)
- 파일 업로드: 표준 사양에 파일 업로드가 포함되어 있지 않아, 별도의 멀티파트 요청 처리가 필요합니다.
- 초기 학습 곡선: 쿼리 언어뿐만 아니라 스키마 설계, 리졸버 구현 등 기존 REST 방식과는 다른 사고방식이 필요합니다.
다루지 않은 내용
- 인트로스펙션(introspection): 서버의 스키마, 타입을 조회하는 기능, Playground 에서 인스로스펙션 기능을 통해 스키마 정보를 실시간으로 확인하며 쿼리를 작성
- 추상 구문 트리(AST): 쿼리가 들어오면 서버는 '추상 구문 트리(AST)'로 파싱하여 분석하고, 그 결과에 따라 적절한 리졸버 함수를 실행
감사합니다.😊
