GraphQL.js 리졸버의 마지막 인자는 info입니다. info는 현재 처리 중인 질의에 대한 정보가 들어가 있습니다. 보통은 리졸버 구현에 info가 필요하지 않지만 최적화나 복잡한 연결을 위해서는 info의 내용이 필요합니다.

variableValues

variableValues 속성에는 실행시 준 변수 값이 들어있습니다. 그런데 변수를 사용한 경우에만 값이 있기 때문에 생각보다 쓸 만한 곳은 없습니다. 아래 예에서 query도 필드의 인자이지만 variableValues에는 포함되지 않습니다.

import { makeExecutableSchema } from '@graphql-tools/schema';
import { graphql, GraphQLResolveInfo } from 'graphql';

const type_defs = `
type User {
  id: ID!
  name: String!
}

type Query {
  users(query: String, length: Int): [User!]!
}`;

const users = [
  { id: '1', name: 'Francisco' },
  { id: '2', name: 'Alexander' },
];

const resolvers = {
  Query: {
    users: (_source: unknown, _args: unknown, _context: any, info: GraphQLResolveInfo) => {
      console.log(info.variableValues); // { length: 5 }
      return users;
    },
  },
  User: {
    name: (source: any, _args: unknown, _context: any, info: GraphQLResolveInfo) => {
      console.log(info.variableValues); // { length: 5 }
      return source.name;
    },
  },
};

const schema = makeExecutableSchema({ typeDefs: type_defs, resolvers });

(async () => {
  await graphql({
    schema,
    source: `query($length: Int) { users(query: "H", length: $length) { id name } }`,
    variableValues: { length: 5 },
  });
})();

GraphQL Java에서는 DataFetchingEnvironment.getVariables()로 얻을 수 있습니다.

path, fieldName, parentType, returnType

path을 통해 현재 리졸버가 어떤 경로에 의해 실행됐는지 알 수 있습니다.

import { makeExecutableSchema } from '@graphql-tools/schema';
import { graphql, GraphQLResolveInfo } from 'graphql';

const type_defs = `
type Name {
  first: String!
  last: String!
}

type User {
  id: ID!
  name: Name!
}

type Query {
  users: [User!]!
}`;

const users = [
  { id: '1', name: { first: 'John', last: 'Doe' } },
  { id: '2', name: { first: 'Michael', last: 'Frank' } },
];

const resolvers = {
  Query: {
    users: (_source: unknown, _args: unknown, _context: any, info: GraphQLResolveInfo) => {
      console.log(JSON.stringify(info.path, null, 2));
      return users;
    },
  },
  User: {
    name: (source: any, _args: unknown, _context: any, info: GraphQLResolveInfo) => {
      console.log(JSON.stringify(info.path, null, 2));
      return source.name;
    },
  },
  Name: {
    first: (source: any, _args: unknown, _context: any, info: GraphQLResolveInfo) => {
      console.log(JSON.stringify(info.path, null, 2));
      return source.first;
    },
  },
};

const schema = makeExecutableSchema({ typeDefs: type_defs, resolvers });

(async () => {
  await graphql({ schema, source: `query { users { id name { first last } } }` });
})();

다음과 같은 구조의 데이터입니다. users는 배열이기 때문에 중간에 ‘0’이 키로 존재합니다.

{ "key": "users", "typename": "Query" }

{
  "prev": {
    "prev": { "key": "users", "typename": "Query" },
    "key": 0
  },
  "key": "name", "typename": "User"
}

{
  "prev": {
    "prev": {
      "prev": { "key": "users", "typename": "Query" },
      "key": 0
    },
    "key": "name", "typename": "User"
  },
  "key": "first", "typename": "Name"
}

fieldName, parentType, returnType에는 다음 스키마에 해당하는 타입이 들어가 있습니다.

users, Query, [User!]!
name, User, Name!
first, Name, String!

GraphQL Java에서는 DataFetchingEnvironment의 getExecutionStepInfo().getPath(), getField().getName(), getParentType(), getExecutionStepInfo().getType()으로 얻을 수 있습니다.

fieldNodes

fieldNodes에는 현재 실행중인 리졸버가 반환해야 하는 필드 정보가 들어가 있습니다. (GraphQL Java에는 getField()로 얻을 수 있습니다.) 쿼리 최적화등에 사용할 수 있어서 가장 많이 쓰는 속성입니다.

위 예에서 각 리졸버에 각각 users { id name { first last } }, name { first last }, first라는 값이 들어온다고 보면 됩니다. GraphQL.js의 내부 구조체를 그대로 주기 때문에 직접 쓰기에는 어렵습니다. 그래서 카카오스타일에서는 @croquiscom/crary-graphql이라는 자체 라이브러리를 만들어 사용하고 있습니다.

getFieldList

getFieldList(info)를 실행하면 현 리졸버가 반환해야 할 필드 목록을 반환합니다. 위 예에서는 [ 'id', 'name.first', 'name.last' ], [ 'first', 'last' ], []가 실행 결과가 됩니다. 이를 활용해 클라이언트가 요청한 필드만 처리하는 최적화를 할 수 있습니다.

중첩된 필드는 . 으로 묶여 1차원 배열로 반환하는데, 카카오스타일에서 쓰는 ORM인 CORMO에서 객체 컬럼이 있을 때 사용하는 select 구문의 인자로 그대로 쓸 수 있는 형태를 가지고 있습니다.

class Name {
  @cormo.Column({ type: String, required: true })
  public first: string;

  @cormo.Column({ type: String, required: true })
  public last: string;
}

@cormo.Model()
class User extends cormo.BaseModel {
  id: number;

  @cormo.ObjectColumn(Name)
  public name: Name;
}

const records = await User.where().select(getFieldList(info));

{ users { id name { first } } }와 같이 일부 필드만 요청한 경우 위 코드는 SELECT id, name_first FROM users와 같이 요청한 컬럼만 DB에서 가져오는 최적화된 쿼리로 변환됩니다.

getFieldList1st

getFieldList1st는 getFieldList와 유사하지만 첫번째 깊이의 필드만 반환합니다. 즉 위 예에서는 [ 'id', 'name' ], [ 'first', 'last' ], [] 를 반환합니다.

args 아티클에 있는 total_count, item_list 예에서 getFieldList1st(info).includes('item_list') 조건문을 써서 item_list를 요청한 경우에만 DB에서 목록을 가져오는 최적화를 할 수 있습니다.

addArgumentToInfo, addFieldToInfo, removeArgumentFromInfo, removeFieldFromInfo

카카오스타일에서는 API gateway를 두고 있고, 거기서 GraphQL을 각 마이크로서비스로 분배합니다. 이 때 클라이언트 요청을 일부 변형한 후에 마이크로서비스로 전달할 필요가 있어서 만든 유틸리티입니다. info에 필드를 추가/삭제하거나 인자를 변형할 수 있습니다.

마이크로서비스에서 계정 정보를 반환하는 Query.user(id: ID!) API를 제공한다고 가정해 봅시다. 하지만 API gateway에서는 이 API를 그대로 쓰는게 아니라 현재 로그인한 사용자의 정보를 반환하는 Query.user 형태로 클라이언트에 노출하고 있습니다. 로그인한 사용자 정보는 API gateway가 알고 있고, 마이크로서비스 API 호출시 이를 인자에 추가해줘야 합니다. 이 로직을 다음과 같이 구현하고 있습니다. (hookResolver는 기존 리졸버의 동작 앞뒤에 다른 동작을 추가할 수 있는 유틸리티입니다.)

hookResolver(schema.getQueryType()!.getFields().user, async (source, args, context, info, resolve) => {
  const user_id: string | undefined = context.session.login_user_id;
  if (!user_id) {
    return null;
  }
  info = addArgumentToInfo(info, 'id', user_id, new GraphQLNonNull(GraphQLID));
  // 또는 info = info.addArgument('id', user_id, new GraphQLNonNull(GraphQLID));
  return await resolve(source, args, context, info);
});


comments powered by Disqus