현인
GraphQL 학습 - 5장 실행(Execution) 본문
유효성 검증이 끝나면, GraphQL 쿼리는 GraphQL 서버에서 실행된다. 서버에서는 요청된 쿼리와 동일한 모습의 형식을 가진 결과를 리턴한다.
GraphQL은 타입 시스템 없이는 쿼리를 실행할 수 없으므로, 쿼리 실행을 설명하기 위해 예시 타입 시스템을 사용해보겠다. 이는 이 글들의 예시들에서 사용된 동일한 타입 시스템의 일부이다.
type Query {
human(id: ID!): Human
}
type Human {
name: String
appearsIn: [Episode]
starships: [Starship]
}
enum Episode {
NEWHOPE
EMPIRE
JEDI
}
type Starship {
name: String
}
쿼리가 실행될 때 어떤 일이 일어나는지 설명하기 위해, 예시를 들어 단계별로 살펴보겠다.
{
human(id: 1002) {
name
appearsIn
starships {
name
}
}
}
{
"data": {
"human": {
"name": "Han Solo",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
],
"starships": [
{
"name": "Millenium Falcon"
},
{
"name": "Imperial shuttle"
}
]
}
}
}
GraphQL 쿼리의 각 필드는 이전 타입의 함수 또는 메서드로 생각할 수 있으며, 이는 다음 타입을 반환한다. 실제로 이것이 바로 GraphQL의 작동 방식이다. 각 타입의 각 필드는 GraphQL 서버 개발자가 제공하는 리졸버라고 불리는 함수에 의해 지원된다. 필드가 실행될 때, 해당하는 리졸버가 호출되어 다음 값을 생성한다.
필드가 문자열이나 숫자와 같은 스칼라 값을 생성하면 실행이 완료된다. 그러나 필드가 객체 값을 생성하면 쿼리는 해당 객체에 적용되는 또 다른 필드 선택을 포함하게 된다. 이 과정은 스칼라 값에 도달할 때까지 계속된다. GraphQL 쿼리는 항상 스칼라 값에서 끝난다.
루트 필드 그리고 리졸버
GraphQL 서버의 모든 탑 레벨은 GraphQL API의 가능한 엔트리포인트들을 표현하는 타입이다. 이는 Root type 혹은 Query type이라고 불린다.
이 예시에서, 우리의 Query 타입은 id 인자를 받는 human이라는 필드를 제공한다. 이 필드의 리졸버 함수는 아마도 데이터베이스에 접근하여 Human 객체를 구성하고 반환할 것이다.
Query: {
human(obj, args, context, info) {
return context.db.loadHumanByID(args.id).then(
userData => new Human(userData)
)
}
}
이 예시는 JavaScript로 작성되었지만, GraphQL 서버는 다양한 언어로 구축될 수 있다. 리졸버 함수는 네 가지 인자를 받는다:
- obj 이전 객체로, 루트 Query 타입의 필드에서는 주로 사용되지 않는다.
- args GraphQL 쿼리에서 필드에 제공된 인자들이다.
- context 모든 리졸버에 제공되는 값으로, 현재 로그인한 사용자나 데이터베이스 접근과 같은 중요한 문맥 정보를 담고 있다.
- info 현재 쿼리와 관련된 필드별 정보와 스키마 세부 정보를 담고 있는 값이다. 자세한 내용은 type GraphQLResolveInfo를 참조하라.
비동기 리졸버
이 리졸버 함수에서 어떤 일이 일어나는지 자세히 살펴보자.
human(obj, args, context, info) {
return context.db.loadHumanByID(args.id).then(
userData => new Human(userData)
)
}
context는 GraphQL 쿼리에서 인자로 제공된 id를 사용하여 사용자 데이터를 로드하는 데이터베이스에 대한 접근을 제공한다. 데이터베이스에서 데이터를 로드하는 것은 비동기 작업이므로, 이는 Promise를 반환한다. JavaScript에서는 비동기 값을 다루기 위해 Promise를 사용하지만, 많은 언어에서 이와 같은 개념이 Futures, Tasks 또는 Deferred라는 이름으로 존재한다. 데이터베이스가 응답을 반환하면, 우리는 새로운 Human 객체를 구성하고 반환할 수 있다.
리졸버 함수는 Promise를 인식해야 하지만, GraphQL 쿼리는 그럴 필요가 없다는 점에 주목하라. 쿼리는 단순히 human 필드가 무언가를 반환하기를 기대하며, 그 후에 name을 요청할 수 있다. 실행 중에 GraphQL은 Promise, Futures, 그리고 Tasks가 완료될 때까지 기다린 후 계속 진행하며, 이를 최적의 동시성으로 수행한다.
사소한 리졸버
이제 Human 객체를 사용할 수 있게 되었으므로, GraphQL 실행은 그 객체에 요청된 필드들로 계속 진행될 수 있다.
Human: {
name(obj, args, context, info) {
return obj.name
}
}
GraphQL 서버는 다음에 무엇을 해야 할지 결정하는 데 사용되는 타입 시스템에 의해 구동된다. human 필드가 어떤 것을 반환하기도 전에, GraphQL은 타입 시스템이 human 필드가 Human을 반환할 것이라고 알려주기 때문에 다음 단계가 Human 타입의 필드를 해결하는 것임을 알고 있다.
이 경우 name을 resolve 하는 것은 매우 간단하다. name 리졸버 함수가 호출되고 obj 인자는 이전 필드에서 반환된 new Human 객체이다. 이 경우, 우리는 그 Human 객체가 우리가 직접 읽고 반환할 수 있는 name 속성을 가지고 있을 것으로 예상한다.
실제로, 많은 GraphQL 라이브러리들은 이렇게 간단한 리졸버를 생략하도록 허용하며, 필드에 대한 리졸버가 제공되지 않은 경우 동일한 이름의 속성을 읽고 반환해야 한다고 가정한다.
스칼라 강제
name 필드가 해결되는 동안, appearsIn과 starships 필드는 동시에 해결될 수 있다. appearsIn 필드도 간단한 리졸버를 가질 수 있지만, 좀 더 자세히 살펴보자:
Human: {
appearsIn(obj) {
return obj.appearsIn // [ 4, 5, 6 ]을 반환
}
}
우리의 타입 시스템은 appearsIn이 알려진 값을 가진 Enum 값을 반환할 것이라고 주장하지만, 이 함수는 숫자를 반환하고 있다! 실제로 결과를 보면 적절한 Enum 값들이 반환되고 있음을 알 수 있다. 무슨 일이 일어나고 있는 걸까?
이는 스칼라 강제의 예시다. 타입 시스템은 무엇을 기대해야 하는지 알고 있으며, 리졸버 함수가 반환한 값을 API 계약을 지키는 무언가로 변환할 것이다. 이 경우, 우리의 서버에 내부적으로 4, 5, 6과 같은 숫자를 사용하지만 GraphQL 타입 시스템에서는 Enum 값으로 표현하는 Enum이 정의되어 있을 수 있다.
배열 리졸버
우리는 이미 appearsIn 필드에서 필드가 항목 목록을 반환할 때 어떤 일이 일어나는지 조금 살펴보았다. 이는 enum 값의 목록을 반환했고, 타입 시스템이 예상한 것이 바로 그것이었기 때문에 목록의 각 항목은 적절한 enum 값으로 강제되었다. starships 필드가 해결될 때는 어떤 일이 일어날까?
Human: {
starships(obj, args, context, info) {
return obj.starshipIDs.map(
id => context.db.loadStarshipByID(id).then(
shipData => new Starship(shipData)
)
)
}
}
이 필드의 리졸버는 단순히 Promise를 반환하는 것이 아니라, Promise의 목록을 반환한다. Human 객체는 그들이 조종한 Starships의 id 목록을 가지고 있었지만, 우리는 실제 Starship 객체를 얻기 위해 이 모든 id를 로드해야 한다.
GraphQL은 계속 진행하기 전에 이 모든 Promise를 동시에 기다릴 것이며, 객체 목록이 남게 되면 다시 한 번 동시에 각 항목의 name 필드를 로드하기 위해 계속 진행할 것이다.
결과 생성
각 필드가 해결되면, 결과 값은 필드 이름(또는 별칭)을 키로, 해결된 값을 값으로 하는 키-값 맵에 배치된다. 이는 쿼리의 가장 하위 리프 필드에서 시작하여 루트 Query 타입의 원래 필드까지 계속된다. 이러한 과정을 통해 원래 쿼리를 반영하는 구조가 생성되며, 이는 요청한 클라이언트에 (일반적으로 JSON으로) 전송될 수 있다.
이 모든 리졸빙 함수들이 어떻게 결과를 생성하는지 보기 위해 원래 쿼리를 마지막으로 한 번 더 살펴보자:
{
human(id: 1002) {
name
appearsIn
starships {
name
}
}
}
{
"data": {
"human": {
"name": "Han Solo",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
],
"starships": [
{
"name": "Millenium Falcon"
},
{
"name": "Imperial shuttle"
}
]
}
}
}
출처
'기술 학습 > GraphQL' 카테고리의 다른 글
GraphQL 학습 - 6장 스키마 확인(Introspection) (2) | 2024.11.15 |
---|---|
GraphQL 학습 - 4장 유효성 검증(Validation) (0) | 2024.11.14 |
GraphQL 학습 - 3장 스키마(Schema) & 타입(Type) (3) | 2024.11.14 |
GraphQL 학습 - 2장 쿼리(Query) & 뮤테이션(Mutation) (1) | 2024.11.14 |
GraphQL 학습 - 1장 GraphQL 소개 (0) | 2024.11.14 |