들어가며
본문의 첫 번째 섹션은 REST 2.0 Is Here and Its Name Is GraphQL을 번역한 것입니다. 제목이 시각적으로 매우 강렬해서 나도 모르게 이끌려 들어갔습니다.
나머지 부분은 GraphQL에 대한 고찰입니다. 이제 번역본을 살펴보며 궁금한 점들을 정리해 봅시다.
1. 번역본
GraphQL은 API용 쿼리 언어입니다. REST와 본질적으로는 다르지만, GraphQL은 REST의 대안으로 사용될 수 있으며 높은 성능, 우수한 개발 경험 및 몇 가지 강력한 도구를 제공합니다.
이 글을 통해 REST와 GraphQL을 사용하여 몇 가지 일반적인 시나리오를 처리하는 방법을 살펴보겠습니다. 본문에는 인기 영화 및 배우 정보 API를 제공하는 3개의 프로젝트가 포함되어 있으며, HTML과 jQuery로 만든 간단한 프런트엔드 애플리케이션을 통해 해당 REST 및 GraphQL 소스 코드를 확인할 수 있습니다.
이러한 API를 통해 두 기술의 차이점을 살펴보고 장단점을 이해해 보겠습니다. 시작하기 전에 배경을 설명하기 위해 이러한 기술들이 어떻게 등장하게 되었는지 빠르게 훑어보겠습니다.
웹 초기
웹 초기 단계는 매우 단순했습니다. 초기 웹 애플리케이션은 정적 HTML 문서였습니다. 이후 웹사이트가 데이터베이스(예: SQL)에 저장된 동적 콘텐츠를 포함하고 JavaScript를 통해 상호작용 기능을 추가하는 방식으로 진화했습니다. 대부분의 웹 콘텐츠는 데스크톱 컴퓨터의 웹 브라우저를 통해 액세스되었으며, 모든 것이 순조로워 보였습니다.

REST: API의 등장
2007년 스티브 잡스가 iPhone을 소개했을 때로 건너뛰어 봅시다. 스마트폰은 세상과 문화, 소통에 깊은 영향을 미쳤을 뿐만 아니라 개발자의 업무를 훨씬 더 복잡하게 만들었습니다. 스마트폰은 개발 현황을 뒤흔들었고, 불과 몇 년 만에 데스크톱, iPhone, Android, 태블릿 컴퓨터를 모두 갖게 되었습니다.
이에 대응하여 개발자들은 다양한 모양과 크기의 애플리케이션에 데이터를 제공하기 위해 RESTful API를 사용하기 시작했습니다. 새로운 개발 모델은 대략 다음과 같았습니다.

GraphQL: API의 진화
GraphQL은 Facebook에서 설계하고 오픈 소스로 공개한 API용 쿼리 언어입니다. GraphQL을 REST의 대안으로 삼아 API를 구축할 수 있습니다. REST는 API를 설계하고 구현하는 데 사용할 수 있는 개념적 모델인 반면, GraphQL은 표준화된 언어, 타입 시스템, 그리고 클라이언트와 서버 사이의 강력한 약속을 세우는 명세 정의입니다. 모든 장치 간의 통신을 위한 표준 언어 세트를 제공하여 대규모 크로스 플랫폼 애플리케이션을 만드는 과정을 단순화합니다.
GraphQL을 사용하면 구조도는 다음과 같이 변합니다.

GraphQL vs REST
아래 섹션은 소스 코드를 따라가며 읽는 것을 권장합니다. accompanying GitHub repo에서 관련 소스 코드를 찾을 수 있습니다.
소스 코드에는 3가지 프로젝트가 포함되어 있습니다.
-
RESTful API 구현
-
GraphQL API 구현
-
jQuery와 HTML로 만든 간단한 클라이언트 웹 페이지
두 기술을 가능한 한 쉽게 비교하기 위해 프로젝트는 의도적으로 매우 단순하게 설계되었습니다.
함께 실습해 보려면 3개의 터미널 창을 열고 프로젝트 저장소의 RESTful, GraphQL, Client 디렉토리로 cd한 뒤 npm run dev를 통해 서비스를 시작하세요. 준비가 되면 계속 읽어주세요. :)
REST를 통한 쿼리
우리의 RESTful API에는 몇 가지 엔드포인트(endpoints)가 있습니다.
| Endpoint | Description |
|---|---|
| /movies | 영화로 연결되는 링크를 포함하는 객체 배열을 반환합니다 (예: [ { href: ‘http://localhost/movie/1’ } ]) |
| /movie/:id | id = :id 인 단일 영화를 반환합니다. |
| /movie/:id/actors | id = :id 인 영화에 출연한 배우들의 링크를 포함하는 객체 배열을 반환합니다. |
| /actors | 배우로 연결되는 링크를 포함하는 객체 배열을 반환합니다. |
| /actor/:id | id = :id 인 단일 배우를 반환합니다. |
| /actor/:id/movies | id = :id 인 배우가 출연한 영화들의 링크를 포함하는 객체 배열을 반환합니다. |
참고: 이렇게 단순한 데이터 모델임에도 벌써 6개의 엔드포인트를 유지 관리하고 문서를 작성해야 합니다.
우리가 클라이언트 개발자이고, 이 영화 API를 사용하여 HTML과 jQuery로 간단한 웹 페이지를 구축해야 한다고 가정해 봅시다. 이 페이지를 만들기 위해 영화와 그에 해당하는 배우 정보가 필요합니다. 우리의 API는 필요한 모든 기능을 갖추고 있으므로 직접 데이터를 가져옵니다.
새 터미널을 열고 실행하세요.
curl localhost:3000/movies
다음과 같은 응답을 받게 됩니다.
[
{
"href": "http://localhost:3000/movie/1"
},
{
"href": "http://localhost:3000/movie/2"
},
{
"href": "http://localhost:3000/movie/3"
},
{
"href": "http://localhost:3000/movie/4"
},
{
"href": "http://localhost:3000/movie/5"
}
]
RESTful 방식에서는 API가 실제 영화 객체에 대응하는 링크 배열을 반환합니다. 그런 다음 첫 번째 영화를 가져오기 위해 curl http://localhost:3000/movie/1을 실행하고, 두 번째 영화는 curl http://localhost:3000/movie/2…… 이런 식으로 계속합니다.
app.js에서 페이지에 필요한 모든 데이터를 가져오는 데 사용되는 메서드를 볼 수 있습니다.
const API_URL = 'http://localhost:3000/movies';
function fetchDataV1() {
// 1 call to get the movie links
$.get(API_URL, movieLinks => {
movieLinks.forEach(movieLink => {
// For each movie link, grab the movie object
$.get(movieLink.href, movie => {
$('#movies').append(buildMovieElement(movie))
// One call (for each movie) to get the links to actors in this movie
$.get(movie.actors, actorLinks => {
actorLinks.forEach(actorLink => {
// For each actor for each movie, grab the actor object
$.get(actorLink.href, actor => {
const selector = '#' + getMovieId(movie) + ' .actors';
const actorElement = buildActorElement(actor);
$(selector).append(actorElement);
})
})
})
})
})
})
}
보시다시피 이상적이지 않아 보입니다. 이 모든 것을 완료하기 위해 우리는 1 + M + M + sum(Am)번의 API 호출을 했습니다. 여기서 M은 영화 수이고, sum(Am)은 M편의 영화에 출연한 모든 배우의 총합입니다. 데이터 요구 사항이 적은 앱이라면 괜찮겠지만, 대규모 운영 시스템에는 적합하지 않습니다.
결론은요? 우리의 단순한 RESTful 접근 방식은 부적절합니다. API를 최적화하기 위해 우리는 백엔드 팀에 이 페이지를 지원하기 위한 전용 /moviesAndActors 인터페이스를 요청할 수 있습니다. 이 인터페이스가 준비되면 1 + M + M + sum(Am)번의 네트워크 요청을 1번의 요청으로 바꿀 수 있습니다.
curl http://localhost:3000/moviesAndActors
다음과 같은 응답을 반환합니다.
[
{
"id": 1,
"title": "The Shawshank Redemption",
"release_year": 1993,
"tags": [
"Crime",
"Drama"
],
"rating": 9.3,
"actors": [
{
"id": 1,
"name": "Tim Robbins",
"dob": "10/16/1958",
"num_credits": 73,
"image": "https://images-na.ssl-images-amazon.com/images/M/MV5BMTI1OTYxNzAxOF5BMl5BanBnXkFtZTYwNTE5ODI4._V1_.jpg",
"href": "http://localhost:3000/actor/1",
"movies": "http://localhost:3000/actor/1/movies"
},
{
"id": 2,
"name": "Morgan Freeman",
"dob": "06/01/1937",
"num_credits": 120,
"image": "https://images-na.ssl-images-amazon.com/images/M/MV5BMTc0MDMyMzI2OF5BMl5BanBnXkFtZTcwMzM2OTk1MQ@@._V1_UX214_CR0,0,214,317_AL_.jpg",
"href": "http://localhost:3000/actor/2",
"movies": "http://localhost:3000/actor/2/movies"
}
],
"image": "https://images-na.ssl-images-amazon.com/images/M/MV5BODU4MjU4NjIwNl5BMl5BanBnXkFtZTgwMDU2MjEyMDE@._V1_UX182_CR0,0,182,268_AL_.jpg",
"href": "http://localhost:3000/movie/1"
},
...
]
좋습니다! 단 1번의 요청으로 페이지에 필요한 모든 데이터를 가져올 수 있었습니다. Client 디렉토리의 app.js에서 이 최적화 구현을 확인할 수 있습니다.
const MOVIES_AND_ACTORS_URL = 'http://localhost:3000/moviesAndActors';
function fetchDataV2() {
$.get(MOVIES_AND_ACTORS_URL, movies => renderRoot(movies));
}
function renderRoot(movies) {
movies.forEach(movie => {
$('#movies').append(buildMovieElement(movie));
movie.actors && movie.actors.forEach(actor => {
const selector = '#' + getMovieId(movie) + ' .actors';
const actorElement = buildActorElement(actor);
$(selector).append(actorElement);
})
});
}
새 애플리케이션은 이전 버전보다 빨라지겠지만, 여전히 완벽하지는 않습니다. http://localhost:4000을 열어 페이지를 확인하면 다음과 같이 보입니다.

자세히 보면 우리 페이지가 영화의 title과 image, 그리고 각 배우의 name과 image만 사용하고 있다는 것을 알 수 있습니다. (사실 우리는 영화 객체의 8개 필드 중 2개, 배우 객체의 7개 필드 중 2개만 사용하고 있습니다.) 즉, 네트워크 요청으로 가져온 정보의 3/4을 낭비하고 있는 셈입니다! 이렇게 대역폭을 과도하게 사용하면 성능에 큰 영향을 미치고 추가적인 인프라 비용이 발생합니다.
영리한 백엔드 개발자는 비웃으며 필드 이름 목록을 받는 fields라는 특수 쿼리 매개변수를 신속하게 구현하여 특정 요청이 반환해야 할 필드를 동적으로 결정할 수 있게 할 것입니다.
예를 들어 curl http://localhost:3000/moviesAndActors 대신 curl http://localhost:3000/moviesAndActors?fields=title,image를 사용할 수 있습니다. 심지어 배우 모델에 포함될 필드를 지정하기 위해 또 다른 매개변수 actor_fields를 추가할 수도 있습니다. 예: curl http://localhost:3000/moviesAndActors?fields=title,image&actor_fields=name,image
이제 이것은 우리 앱의 거의 최적의 구현이 되었지만, 클라이언트 앱의 특정 페이지를 위해 맞춤형 인터페이스를 만드는 나쁜 습관을 도입하게 되었습니다. 웹 페이지와 Android 앱에서 보여주는 정보가 다른 iOS 앱을 구축하기 시작하면 이 문제는 더욱 두드러집니다.
만약 우리가 데이터 모델의 엔터티와 그 관계를 명시적으로 설명하면서도 1 + M + M + sum(Am)의 성능 문제를 일으키지 않는 범용 API를 구축할 수 있다면 얼마나 좋을까요? 좋은 소식입니다! 가능합니다!
GraphQL을 통한 쿼리
GraphQL을 사용하면 불필요한 정보 없이 필요한 모든 정보를 한 번의 간단하고 직관적인 쿼리로 가져오는 최적의 쿼리로 바로 넘어갈 수 있습니다.
query MoviesAndActors {
movies {
title
image
actors {
image
name
}
}
}
강력히 추천합니다! 수동으로 시도해 보세요. http://localhost:5000의 GraphiQL(훌륭한 브라우저용 GraphQL IDE)을 열고 위의 쿼리를 실행해 보세요.
이제 조금 더 깊이 들어가 보겠습니다.
GraphQL 생각하기
GraphQL은 HTTP 구조(예: 메서드 및 URI)에 의존하지 않고 데이터 위에 직관적인 쿼리 언어와 강력한 타입 시스템 레이어를 제안하여 클라이언트와 서버 간의 강력한 약속을 제공하는, REST와는 완전히 다른 API 접근 방식을 채택합니다. 쿼리 언어는 클라이언트 개발자가 모든 페이지가 원하는 데이터를 영구적으로 가져올 수 있는 메커니즘을 제공합니다.
GraphQL은 데이터를 가상 정보 그래프로 간주할 것을 권장합니다. 정보를 포함하는 엔터티를 타입(type)이라고 하며, 이러한 타입들은 필드(fields)를 통해 서로 연결될 수 있습니다. 쿼리는 루트에서 시작하여 이 가상 그래프에서 필요한 정보를 탐색합니다.
이 "가상 그래프"를 스키마(schema)라고 하며, 스키마는 API 데이터 모델을 구성하는 타입(type), 인터페이스(interface), 열거형(enum) 및 유니온(union)의 집합입니다. GraphQL에는 API를 정의하는 데 사용할 수 있는 편리한 스키마 언어도 포함되어 있습니다. 예를 들어, 이것이 우리 영화 API에 대응하는 스키마입니다.
schema {
query: Query
}
type Query {
movies: [Movie]
actors: [Actor]
movie(id: Int!): Movie
actor(id: Int!): Actor
searchMovies(term: String): [Movie]
searchActors(term: String): [Actor]
}
type Movie {
id: Int
title: String
image: String
release_year: Int
tags: [String]
rating: Float
actors: [Actor]
}
type Actor {
id: Int
name: String
image: String
dob: String
num_credits: Int
movies: [Movie]
}
타입 시스템은 더 나은 도구, 더 나은 문서화, 더 효율적인 애플리케이션을 포함한 수많은 좋은 것들로 향하는 문을 열어주었습니다. 이 부분에 대해 이야기할 것이 많지만, 지금은 일단 넘어가서 REST와 GraphQL의 차이를 보여주는 더 많은 시나리오에 집중해 보겠습니다.
GraphQL vs Rest: 버전 관리
Google에서 검색해 보면 REST API 버전 관리에 대한(또는 이와 관련된) 수많은 의견을 얻을 수 있습니다. 여기서 깊이 탐구하지는 않겠지만, 이것이 의미 있는 문제라는 점을 강조하고 싶습니다. 버전 관리가 어려운 한 가지 요인은 어떤 정보가 어떤 앱과 장치에서 사용되고 있는지 알기 어렵다는 점입니다.
일반적으로 정보를 추가하는 것은 REST나 GraphQL 모두 쉽습니다. 필드를 추가하면 REST 클라이언트로 흘러 들어가고, GraphQL에서는 쿼리를 변경하지 않는 한 안전하게 무시됩니다. 그러나 정보를 삭제하거나 편집하는 것은 크게 다릅니다.
REST 방식에서는 필드 수준에서 어떤 정보가 사용되고 있는지 알기 어렵습니다. /movies라는 인터페이스가 사용되고 있다는 것은 알 수 있지만, 클라이언트가 title을 쓰고 있는지, image를 쓰고 있는지, 아니면 둘 다 쓰고 있는지 알 수 없습니다. 가능한 솔루션은 반환할 필드를 지정하는 쿼리 매개변수를 추가하는 것이지만, 이러한 매개변수는 보통 선택 사항입니다. 따라서 /v2/movies와 같이 엔드포인트 수준의 변화를 자주 보게 됩니다. 이것도 방법이지만 API의 표면적을 넓히고 개발자에게 지속적인 업데이트와 상세한 문서 제공이라는 부담을 안겨줍니다.
반면 GraphQL의 버전 관리는 다릅니다. 모든 GraphQL 쿼리는 요청된 필드를 정확하게 설명해야 합니다. 이러한 강제 요구 사항은 어떤 정보가 요청되었는지 정확히 알 수 있음을 의미하며, 요청 빈도와 요청 주체를 더 자세히 파악할 수 있게 해줍니다. 또한 GraphQL은 사용되지 않는 필드와 폐기 사유 정보를 사용하여 스키마의 프리미티브(primitives)를 수식하는 것을 지원합니다.
GraphQL의 버전 관리:

GraphQL vs REST: 캐싱
REST에서의 캐싱은 직접적이고 효율적입니다. 실제로 캐싱은 REST의 6가지 원칙 제약 조건 중 하나이며 RESTful 설계에 내장되어 있습니다. /movies/1 엔드포인트의 응답에 캐시가 가능하다는 내용이 포함되어 있다면, 향후 /movies/1에 대한 모든 요청은 캐시에 있는 내용으로 간단히 대체될 수 있습니다. 매우 간단하죠.
GraphQL에서의 캐싱 처리는 약간 다릅니다. GraphQL API를 캐싱하려면 보통 API의 각 객체에 일종의 고유 식별자를 도입해야 합니다. 각 객체에 고유 식별자가 있으면 클라이언트는 이 식별자를 사용하여 신뢰할 수 있는 캐싱, 업데이트 및 만료를 수행하는 표준화된 캐시를 구축할 수 있습니다. 클라이언트가 해당 객체를 참조하는 하위 쿼리를 시작할 때 해당 객체의 캐시된 버전을 사용합니다. GraphQL 캐싱 원리에 대해 더 알고 싶다면 이 주제를 깊이 있게 다룬 훌륭한 기사가 있습니다.
GraphQL vs REST: 개발 경험
개발 경험은 애플리케이션 개발에서 매우 중요한 측면이며, 엔지니어로서 우리가 좋은 도구를 만드는 데 많은 시간을 투자하는 이유이기도 합니다. 여기서의 비교는 다분히 주관적일 수 있지만 여전히 언급할 가치가 있다고 생각합니다.
REST는 실제로 풍부한 도구 생태계를 갖추고 있어 개발자가 RESTful API를 문서화하고 테스트하며 검사하는 것을 돕습니다. 하지만 개발자들은 REST API의 확장을 위해 큰 대가를 치렀습니다. 인터페이스 수는 순식간에 폭발하고, 불일치는 점점 더 뚜렷해지며, 버전 관리는 갈수록 어려워집니다.
GraphQL은 개발 경험 측면에서 확실히 뛰어난 점이 있습니다. 타입 시스템은 GraphiQL IDE와 스키마에 내장된 문서화 같은 놀라운 도구들로 가는 문을 열어주었습니다. GraphQL에는 단 하나의 엔드포인트만 있으며, 사용 가능한 데이터를 찾기 위해 문서에 의존할 필요가 없습니다. 타입 세이프한 언어를 갖게 되며 사용 가능한 항목을 자동 완성할 수 있어 API를 신속하게 만들 수 있습니다. GraphQL은 React 및 Redux와 같은 인기 있는 프런트엔드 프레임워크 및 도구와도 함께 사용할 수 있습니다. React로 앱을 구축할 계획이라면 Relay나 Apollo client를 확인해 볼 것을 강력히 추천합니다.
요약
GraphQL은 효율적인 데이터 주도 애플리케이션을 구축하는 데 유용한 강력한 도구 세트를 제공합니다. REST가 당장 사라지지는 않겠지만, 특히 클라이언트 애플리케이션을 구축할 때 필요한 많은 것들을 제공할 것입니다.
더 깊이 알고 싶다면 Scaphold.io’s GraphQL Backend as a Service를 확인해 보세요. AWS를 통해 몇 분 만에 운영 환경에서 사용할 수 있는 GraphQL API를 배포하고 자신의 비즈니스 로직에 맞게 확장할 수 있습니다.
이 글이 마음에 드셨기를 바라며, 어떤 생각이나 의견이든 환영합니다. 읽어주셔서 감사합니다!
2. 고찰
인터페이스 위의 추상화 계층을 한 층 더 두는 것은 확실히 더 큰 유연성을 가져다줄 수 있습니다. 예를 들어 원자적 인터페이스만 구현하면 반환할 콘텐츠를 자유롭게 조합할 수 있습니다.
참고: 위의 번역문에서는 GraphQL이 데이터 위의 추상화라고 했지만, 실제로는 인터페이스 위의 추상화여야 합니다. (다만 인터페이스 개념이 약화되어 외부로 드러나지 않을 뿐이며, 약화된 인터페이스는 SQL 문과 같은 것들에 더 가깝습니다.) 만약 모든 필드가 하나의 조회 인터페이스에 대응한다면, GraphQL의 모든 기능을 완성하는 범용 인터페이스 관리 계층을 쉽게 구현할 수 있습니다. 실제로 GraphQL은 이러한 범용적인 정의를 제공합니다.
그렇다면 가장 큰 문제는 중복 조회가 존재한다는 점일 것입니다. 필드를 자유롭게 조합하여 반환하려면 먼저 필드 수준까지 정밀해야 하기 때문입니다. 즉, 원래 하나의 강력한 인터페이스가 여러 필드를 반환했다면, 이제는 각 필드마다 약한 인터페이스를 제공해야 하며, 그래야만 사용자 정의 쿼리에 따라 반환 콘텐츠를 정밀하게 조합할 수 있습니다.
물론 쿼리 최적화를 통해 중복 조회를 일부 완화할 수 있습니다. 예를 들어 필드 의존 관계에 따라 필드를 묶어서 조회하는 식입니다. 하지만 복잡한 시나리오에서는 이러한 최적화가 쉽지 않을 수 있습니다.
만약 이러한 최적화가 내장된 데이터베이스(또는 추상 쿼리 계층)가 있어 성능 문제를 해결해 준다면, GraphQL은 압도적인 우위를 점하게 될 것입니다. 우선 끝없이 인터페이스를 추가할 필요가 없고, 또한 통일된 표준 세트를 유지 관리하는 것과 수많은 인터페이스 및 동일 인터페이스의 여러 버전을 관리하는 상황 사이에서 고민할 필요가 거의 없기 때문입니다.
프런트엔드 생태계와의 협력(Redux가 아주 범용적이지는 않더라도)은 명백히 큰 문제가 되지 않습니다.
아직 댓글이 없습니다