본문 바로가기

React/GraphQL

GraphQL #1 - GraphQL

육각형 모양의 앱 아이콘이 대새인가 봅니다.

GraphQL

GraphQL은 data query language로, SQL(Structured Query Language)과 같이 데이터베이스와 정보 시스템에 질의를 할 수 있게 하는 고급 컴퓨터 언어입니다. 다만 그 목적에 있어서, graphql.org/의 메인 페이지에 적힌 것처럼, "Apps using GraphQL are fast and stable because they control the data they get, not the server.", 데이터베이스에 웹 클라이언트가 데이터를 효율적으로 가져오기 위함입니다.

그래서 데이터베이스에 데이터를 요청하기 위한 쿼리를 조합하는 부분이 백엔드에서 어느 정도 프런트엔드로 넘어왔습니다. 그렇다고 해서 GQL과 SQL이 이율배반적 관계에 있는 것은 아닙니다. SQL/PGQ(PostgreSQL)로 sub-query를 wrapping 하는 방식 등 혼합적 사용이 가능한 예시가 있는데, 차후 말씀드리도록 하겠습니다.

앞서 말씀드린 것처럼 GQL은 API를 위한 쿼리 언어이며 타입 시스템을 사용하여 쿼리를 실행하는 서버사이드 런타임입니다. REST API처럼, GQL은 (Mysql / MariaDB / MongoDB / AWS Aurora / AWS DynamoDB...)등 모든 데이터베이스에 접목 가능합니다. 또한 기존 코드와 데이터에 의해서 대체가 가능합니다. HTTP method POST를 사용하는 GQL은 REST API의 HTTP method GET를 사용하는 것으로 활용할 수 있었던 캐싱 기법을 누리지 못하나, persisited query나 Apollo Client와 같은 서드 파티 자바스크립트 라이브러리를 사용하는 것으로 해소할 수 있습니다. Apollo Client에 대해서는 #4(?) 정도에 소개하고자 합니다.

Why GQL?

NBA결과를 보여주는 페이지를 구성하는 상황에 놓여 있다고 가정을 해보겠습니다. 서버에서 가져올 데이터는 기본적으로 경기 결과 / 경기팀 / 출전 선수 /... 이 필요할 것입니다. REST API를 사용한다면, 우리는 많은 방식 중 크게 2가지의 선택의 기로에 놓입니다.

OPTION 1) 페이지의 정보를 한 번에 다 가져오기 (혹은 최대한 많게)

 

// HTTP METHOD
HTTP GET /sports/:sportId/games/:gameNumber/information

// request
pathParameters: {sportId}, {gameNumber}

// response
data: {
	score: {
		home: 133,    
		away: 126,
	},
	teamList: {
		home: "Bulls",
		away: "Timberwolves",
	},
	playerList: {
		home: [
			{
				name: "Garret Temple",
				score: 5
				...
			},
			{
				name: "Patrick Williams":,
				score: 13
				...
			},
			...
		],
		away: ...
	},
	...    
}   

 

하나의 엔드포인트로 페이지에 사용할 정보들을 가져올 수 있습니다. 페이지를 로드할 때 하나의 API 호출만으로도 가능하기 때문에 페이지 컨트롤이 용이할 수 있습니다. 그러나 이런 방식은 궁극적으로 각 페이지마다 필요한 하나의 API를 만들기 때문에 재활용이 불가능하고, API의 개수가 페이지에 비례해서 증가하기 때문에 (클라우드를 사용한다면) AWS Lambda의 수가 증가하는 것이 될 수 있습니다. 또한 페이지에 보일 정보가 향후 추가되거나 삭제되는 경우, 그리고 페이지 로드에 포함되지 않고 추가 사용자의 작용에 따라서 가져와야 하는 정보가 있다면 이들을 적극적으로 반영하기 어렵습니다. 

OPTION 2) 페이지의 정보를 필요한 데이터의 성격에 맞게 나눠서 가져오기 (혹은 최대한 적게)

 

// HTTP METHOD
HTTP GET /sports/:sportId/games/:gameNumber/match/score

// request
pathParameters: {sportId}, {gameNumber}
queryString: {position: "away"}

// response
data: {
	count: 126,
	...
}

 

// HTTP METHOD
HTTP GET /sports/:sportId/games/:gameNumber/match/teams

// request
pathParameters: {sportId}, {gameNumber}
queryString: {position: "home"}

// response
data: {
	name: "Bulls",
	...
}

 

// HTTP METHOD
HTTP GET /sports/:sportId/games/:gameNumber/match/players

// request
pathParameters: {sportId}, {gameNumber}
queryString: {position: "home"}

// response
data: {
	playerList: [
		{
			name: "Garret Temple",
			score: 5
			...
		},
		{
			name: "Patrick Williams":,
			score: 13
			...
		},
		...    
	]
}   

 

각 필요한 정보들의 성격에 따라서 엔드포인트를 분리하는 방식을 선택할 수 있습니다. 다른 페이지에서도 사용할 가능성이 높은 단위로 분리를 할 수 있다면 재사용성을 증대함과 동시에 API 수를 줄일 수 있습니다. 그러나 엔드포인트를 나누는 것으로 인해서 필요한 정보들이 세분화되고 많아진다면 엔드포인트의 수가 매우 많아져서 관리하기가 어렵고, 특정 API들 간의 중복된 정보 호출이 있을 수 있습니다. 또한 한 페이지에서 호출하는 API의 수가 결과적으로 많아지기 때문에 (클라우드를 사용한다면) 해당 비용이 증가하게 됩니다.

Alternative : GraphQL

 

결국 어느 쪽을 선택하더라도 아쉬운 점이 남는 상황에 대해서 GraphQL은 다음과 같은 방식을 선택했습니다.

GraphQL은 단 하나의 엔드포인트를 가지고 불러오고자 하는 데이터의 종류를 쿼리 조합을 통해서 결정합니다. 타입과 필드를 정의하고, 각 타입에 대한 함수로 구현합니다. 그래서 GQL의 스키마의 타입에 따른 쿼리 생성을 하게 됩니다. GraphQL서비스가 실행되면 GraphQL 쿼리를 전송하여 유효성 검사 이후 실행하게 됩니다.

 

// normal query

{
	player {
		name
		score
		...
	}
}

// automatically converted into...

{
	player: {
		name: "Garret Temple",
		score: 5,
		...
	}
}

 

위 코드처럼 변수를 매개변수로 받지 않는 경우의 일반 쿼리 사용도 가능하며, 'query'를 통해서 오퍼레이션 네임 쿼리를 사용할 수 있습니다.

 

// operation name query

query getPlayerInformation($playerId: ID, $matchNumber: Int) {
	playerBasicInformation(playerId: $playerId) {
		// information based on playerId
	}

	playerMatchInformation(playerId: $playerId, matchNumber: $matchNumber) {
		// information based on playerId and matchNumber    
	}
	...
}

 

오퍼레이션 네임 쿼리를 사용하는 것으로, 필요한 정보(매개변수)에 따른 결과를 조회하기 위한 요청을 작성할 수 있습니다. 만약 playerBasicInformation()과 playMatchInformation()을 우리가 Axios()로 구성을 했다라면 각각 1번, 즉 총 2번의 API call을 수행해야 했을 것입니다. 해당 부분 및 구성요소에 대해서는 #2에서 자세히 다루도록 하겠습니다. 그러나 GraphQL을 적용했다고 해서 overfetching / underfetching에 무한한 유연성을 가지고 있는 것은 아닙니다. 파일 업로드와 같은 부분은 github.com/jaydenseric/apollo-upload-client와 같은 library를 지원받아야 하며, 스키마가 잘 구성되었는지에 대한 검증 과정을 위해서 www.apollographql.com/docs/apollo-server/testing/mocking/와 같은 작업이 필요합니다.

 

REST의 구조에서, API의 request-response의 범위를 결정하는 것은 정말 어려운 일입니다. 또한 잘 구성했다고 하더라도, 비즈니스 요건의 변화에 따라서 overfetching / underfetching가 일어난다면 백엔드의 controller - service - repository - query에 해당하는 파일을 모두 수정해야 하기 때문에 많은 수정사항이 일어나게 됩니다. GraphQL을 적용하는 것으로 API Contract를 작성할 때 URL을 정하는 고뇌(언제나 잘 작성하려고 해도 어렵습니다 ㅜㅜ)에서 어느 정도 벗어난다면 도입하지 않을 이유가 없다고 생각합니다.

 

 

※ 잘못된 부분이나 궁금한 점 댓글로 작성해 주시면 최대한 답변하겠습니다. 읽어주셔서 감사합니다~

※ 새로운 기술이나 정보를 알게 되면 글 추가 정리를 하니 하단의 좋아요 버튼 체크하시면 쉽게 확인 가능하십니다!

※ GraphQL #2 - GraphQL Structure에서 구조 및 구성요소에 대해서 말씀드리겠습니다.