GraphQL.js의 리졸버는 네개의 인자를 가지고 있습니다. 그 중 첫번째 인자는 source 입니다.

GraphQL Java에서는 DataFetchingEnvironment.getSource로 얻을 수 있습니다.

앞서 설명했듯이 리졸버는 상위 필드에서 하위 필드 순서로 호출됩니다. source는 상위 필드에서 반환한 값이 들어갑니다. 상위 필드가 없는 Query 필드에는 execute(graphql) 메소드의 rootValue 값이 들어옵니다. 다만 실전에서 rootValue를 사용해 본 적은 없습니다.

굉장히 단순한 동작이지만 상위 필드에서 반환한 값이 그대로 들어온 다는 점을 이해해두면 좋습니다. 카카오스타일에서는 이 특성을 활용한 리졸버가 있습니다.

앞선 예제를 다시 보겠습니다.

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

type Post {
  id: ID!
  title: String!
  author_id: ID!
  author: User!
}

type Query {
  getPosts: [Post!]!
  getUser(id: ID!): User!
}`;

const posts = [
  { id: '1', title: 'Post 1', author_id: '51' },
  { id: '2', title: 'Post 2', author_id: '52' },
  { id: '3', title: 'Post 3', author_id: '51' },
];

const resolvers = {
  Query: {
    getPosts: () => posts,
  },
  Post: {
    author: (source: any) => authors[source.author_id],
  },
};

GraphQL의 Post 타입과 구현 부분의 Post 인터페이스는 명백하게 같지 않습니다. 하지만 문제 없이 동작하죠. 이 차이를 명확히 보기 위해 Post 클래스를 정의하는 것으로 바꿔보겠습니다.

class Post {
  constructor(public id: string, public title: string, public author_id: string) {}
}

const posts: Post[] = [new Post('1', 'Post 1', '51'), new Post('2', 'Post 2', '52'), new Post('3', 'Post 3', '51')];

const resolvers = {
  Query: {
    getPosts: () => posts,
  },
  Post: {
    author: (source: Post) => authors[source.author_id],
  },
};

이렇게 하면 GraphQL의 Post 타입과 다르다는 점이 명확히 눈에 들어옵니다.

타입스크립트에서는 필드만 동일하면 해당 클래스와 같은 타입으로 인지합니다. 둘을 섞어서 정의해보면 어떻게 될까요?

const posts: Post[] = [
  new Post('1', 'Post 1', '51'),
  { id: '2', title: 'Post 2', author_id: '52' },
  { id: '3', title: 'Post 3', author_id: '51' },
];

const resolvers = {
  Post: {
    author: (source: Post) => {
      console.log(source.id, source instanceof Post);
      return authors[source.author_id];
    },
  },
};

결과는

1 true
2 false
3 false

이 됩니다. 극단적으로는 다음과 같은 것도 가능합니다.

const resolvers = {
  Query: {
    getPosts: () => posts.map((post) => ({ _id: post.id })),
  },
  Post: {
    id: (source: { _id: string }) => source._id,
    title: (source: { _id: string }) => posts.find((post) => post.id === source._id)!.title,
    author: (source: { _id: string }) => authors[posts.find((post) => post.id === source._id)!.author_id],
  },
};

위에서 알 수 있듯이 상위 필드가 반환하는 값이 GraphQL 타입과 무관해도 아무 문제없습니다.

GraphQL Java에서도 동작은 같습니다. (DGS 예이지만, DGS가 GraphQL Java 위에서 동작하므로 차이가 없습니다.)

data class Id(val _id: String)
data class User(val id: String, val name: String, val mobile_tel: String)

val users = mapOf(
    "51" to User("51", "John", "01012345678"),
    "52" to User("52", "Alex", "01087654321"),
)

@DgsComponent
class PostDataFetcher {
    data class Post(val id: String, val title: String, val author_id: String)

    private val posts = listOf(
        Post("1", "Post 1", "51"),
        Post("2", "Post 2", "52"),
        Post("3", "Post 3", "51"),
    )

    @DgsQuery
    fun getPosts() = posts.map { Id(it.id) }

    @DgsData(parentType = "Post", field = "id")
    fun id(dfe: DgsDataFetchingEnvironment) = dfe.getSource<Id>()._id

    @DgsData(parentType = "Post", field = "title")
    fun title(dfe: DgsDataFetchingEnvironment) = posts.find { it.id == dfe.getSource<Id>()._id }!!.title

    @DgsData(parentType = "Post", field = "author")
    fun author(dfe: DgsDataFetchingEnvironment) = users[posts.find { it.id == dfe.getSource<Id>()._id }!!.author_id]
}

잘 활용하면 좋지만 코드를 새로 접한 사람에게 혼란을 줄 수도 있습니다. 또 위 특성 때문에 타입 언어와 안 어울리는 부분이 있습니다. 저희의 경우 TypeScript 프로젝트에 GraphQL Code Generator를 사용해 리졸버 타입을 지정해주고 있는데, getPosts 리졸버가 반환하는 값에 non-null인 author 필드 값이 없다고 타입 에러가 나기 때문에 어쩔 수 없이 any를 써주고 있습니다.



comments powered by Disqus