[graphql] api fetcherContext 를 활용해 사용한 불필요한 traffic 줄이기

sujin·2023년 6월 14일
1

grapqhl

목록 보기
1/2

netflix grapqhl에 대해서 알아보면서 알아둬야할 점인 context 정보 활용하는 방법에 대해서 공유하려고한다.
context를 잘 활용한다면 api에서 불필요한 데이터들에 대한 query traffic이 발생하지 않을 것이다.

0. Intro

java spring으로 예시를 들자면, HttpServeltRequest를 사용하며 RequestContextHolder를 사용해서 지금까지의 api에 담긴 내용에서 필요한 정보들을 가져와서 사용할 것이다.

그렇다면 graphql에서는 특히 netflix graphql에서는 어떻게 사용할 수 있을까?

netflix graphql에서 지금까지의 context 정보를 활용하기 위해서 DataFetchingEnvironment를 상속받아서 만들어진 DgsDataFetchingEnvironment를 사용할 수 있다.

그렇다면 DgsDataFetchingEnvironment는 어떤 기능을 제공하고 어떨때 사용하는 것일까?

1. GraphQL Context

먼저 Graphql Context 자체에 대한 이해가 필요할 것이다.

GraphqlContext는 승이노딘 컨텍스트 메커니즘으로 key-value쌍으로 적절한 context를 전달하고 framework 및 사용자 공간 코드 서로 "독립적"으로 적절히 사용하여 문맥정보를 얻고, 공유 및 활용할 수 있도록 하기 위해서 제공된다.

https://github.com/graphql-java/graphql-java/blob/master/src/main/java/graphql/GraphQLContext.java 해당 링크를 통해서 GraphqlContext에 대한 source code를 확인할 수 있다.

어떤 기능을 제공하는지는 알겠는데, 어떻게 제공이 가능한 것일까에 대한 의문이 생길 것이다.

application의 흐름이 담긴 context가 각 메서드에서마다 어떻게 사용될 수 있는 것일까일반 계측 클래스가 호출되기 전에 DguRequestData를 활용해서 제공된 모든 항목을 검사하고 제공된 GraphQLContext.Builder를 통해서 GrapqhlContext에 값을 설정하는 과정을 거치기 때문에 문맥 정보가 기록되는 것이다.
또한 우리는 이러한 문맥정보를 활용함으로써 request에 대한 header값과 같은 필요한 정보들을 확보할 수 있는 것이다.

context를 사용하는 방법에 대해서 알아보자.

2. Data Fetching Context

그렇다면 문맥정보를 어떻게 활용할 수 있을까?

DataFetchingEnvironment.getContext()
를 사용함으로써 Context 정보를 얻어올 수 있다.

가장 대표적인 예시로는, 해당 문맥정보는 로그인을 할 때 사용할 수 있을 것이다.
로그인을 하고 발생한 JWT token을 cookie에 저장하고 싶은 경우를 생각해보면, cookie에 데이터를 넣기 위해서는 request를 먼저 불러와야하고 해당 요청이 들어온 request를 불러와야한다.
이때, context를 사용할 수 있을 것이다.

위의 경우가 아니라, 아래의 경우도 생각해보자!! 이는 앞서 언급했듯이 불필요한 트래픽을 없애주는데 도움을 줄 것이기에 알아두면 좋겠다!!

예를 들어서 아래와 같은 스키마가 있다고 해보자.


type Query {
   shows: [Show]
}

type Show {
  title: String
  actors: [Actor]
}

이때, @DgsQuery를 사용해서 shows 데이터를 얻으려고 한다고 해보자.


{
	shows{
        title
    }
}

이때, client가 shows query를 사용해서 title, actors중에서 title만 query를 했다고 해보자. 이때 show 1개에 존재할 수 있는 모든 actors에 대해서 불러오는 traffic이 존재하게 될 것이다.
왜냐하면 사용자가 어떤 값을 쿼리 필드에 포함했는지는 중요하지 않기 때문이다.

그렇다면 title 하나만을 위해 쿼리를 했는데도 actors에 대한 "불필요한" 트래픽이 발생하게 되는 것이다.

이를 방지하기 위해서는 아래와 같이 datafetcher를 분리하면 된다.

우선, Query를 해야하는 api는 당연히 그대로 써줘야할 것이다(사용자가 query를 해야하니까).
그렇다면 그대로 놓는다. 다음으로 DgsDataFetchingEnvironment를 사용하면 된다.
DgsDataFetchingEnvironment에서 현재 context 정보를 가져올 수 있다.

@DgsQuery
public List<Show> shows() {

    //Load shows, which doesn't include "actors"
    return shows;
}

@DgsData(parentType = "Show", field = "actors")
public List<Actor> actors(DgsDataFetchingEnvironment dfe) {

   Show show = dfe.getSource();
   actorsService.forShow(show.getId());
   return actors;
}

이렇게 작성을 한다면 dfe를 활용해서 actor가 필요한 경우에만 context에서의 show를 불러와서 제공할 수 있게 된다.
자세한 방법은 아래에서 알아보도록하자!
우선 간단히 context를 사용하는 흐름에 대해서 이해하는 것이 중요할 것이다.

actor의 경우에 schema를 보면, Show type에 존재하는 것을 알 수 있다.
그리고 이것은 show 하나가 있을 때 한번의 actors datafetcher가 실행되어 해당하는 show에 대해서 접근이 가능하다.
이때, n+1 문제가 발생가능한데 이를 위해서 data loader를 사용한다는 것은 이전 포스트를 참고바란다! 지금은 해당 문제 최적화를 고려하지 않는다.

해당 예제를 통해서 사용자가 query를 진행할 때, 요청하지 않는데도 DB에 traffic이 발생하는 경우가 있을 때(title을 쿼리 하지 않는 경우가 아니라 actors와 같은 list를 반환하는 큰 비용이 발생하는 경우)에는 Context를 사용해서 child Datafetcher에서 처리를 진행해야함을 알아보았다!

3. DgsDataFetchingEnvironment@DgsData 사용해 쿼리 분리하기

그렇다면 netflix graphql에서는 query를 분리할 때 어떻게 작성해야하나?에 대해서 궁금할 것이다.

사실 위의 예제에서 큰 힌트를 얻을 수 있다.

DgsDataFetchingEnvironment

DgsDataFetchingEnvironment를 사용하면 context 정보를 활용할 수 있다.


class DgsDataFetchingEnvironment(private val dfe: DataFetchingEnvironment) : DataFetchingEnvironment by dfe {

    fun getDgsContext(): DgsContext {
        return DgsContext.from(this)
    }

    fun <K, V> getDataLoader(loaderClass: Class<*>): DataLoader<K, V> {
        val annotation = loaderClass.getAnnotation(DgsDataLoader::class.java)
        return if (annotation != null) {
            dfe.getDataLoader(DataLoaderNameUtil.getDataLoaderName(loaderClass, annotation))
        } else {
            val loaders = loaderClass.fields.filter { it.isAnnotationPresent(DgsDataLoader::class.java) }
            if (loaders.size > 1) throw MultipleDataLoadersDefinedException(loaderClass)
            val loaderField: java.lang.reflect.Field = loaders
                .firstOrNull() ?: throw NoDataLoaderFoundException(loaderClass)
            val theAnnotation = loaderField.getAnnotation(DgsDataLoader::class.java)
            val loaderName = theAnnotation.name
            dfe.getDataLoader(loaderName)
        }
    }
}

코드를 보면 다음과 같이 작성이 되어있다. 이것은 DataFetchingEnvironment를 상속받아서 생성됐음을 알 수 있는데, DataFetchingEnvironment의 경우에는 getSource, getArguments, getContext 등등의 메서드를 제공한다.

DgsDataFetchingEnvironment 는 context에 접근을 하도록 해준다.

  1. query itself -> query 그 자체에 대한 값
  2. data loaders
  3. source Object -> 필드 정보를 포함하는 객체

등등을 제공해준다.

그렇다면 이를 활용해서 문맥정보를 얻고 문맥 정보안의 필요한 데이터를 사용하면 될 것이다.

다시 그러면 위의 예제를 살펴보자!!

@DgsQuery
public List<Show> shows() {

    //Load shows, which doesn't include "actors"
    return shows;
}

@DgsData(parentType = "Show", field = "actors")
public List<Actor> actors(DgsDataFetchingEnvironment dfe) {

   Show show = dfe.getSource();
   actorsService.forShow(show.getId());
   return actors;
}
  • @DgsData

query안의 chlid fetcher(데이터처리를 위한)로 분리를 진행하였다. 이때 fetcher의 경우에는 DgsData를 사용하면 된다.

필요한 값은 어떤 쿼리에서 나온 것인지 알려줘야할 것이다. 따라서 스키마의 이름을 parentType으로 넘겨줘야한다.
또한 그중에서 어떤 필드에 대한 값인지를 field에 알려줘야한다.

  • dfe
    DgsDataFetchingEnvironment 를 사용해서 getSource()를 진행하면 Show에 대한 source Object를 가져올 수 있고 이를 활용해서 id를 가져올 수 있다.
    그렇다면 우리가 미리 작성해놓은 service method에 id를 넘겨주면 되는 것이다.

이처럼 트래픽을 많이 차지하는 쿼리인데 client가 요청을 하지 않을 가능성이 많다면?
해당 필드를 child로 분리해서 DgsData annotation을 사용한다.
그렇게 필요할 때 traffic을 처리할 수 있게 된다.
그말은 필요하지 않다면 traffic을 낭비하지 않는다는 말이된다.

마무리

지금까지 context를 활용해서 netflix grapqhl에서 어떤 경우에 traffic을 어떻게 줄일 수 있는지에 대해서 알아보았다!

netflix graphql뿐만 아니라 graphql 모두 적용이 가능할 것이다.
restapi에서는 api를 분리하는게 client에 따라서 어떻게 다르게 해야할지 고민을 해본적이 없는데 graphql에서는 client가 많이 사용하지 않는데 traffic cost가 많이 드는 필드에 대해서는 sub datafetcher를 만드는 식으로 고려해야한다는 것을 느꼈다.

그리고 이러한 고려사항이 있다는 것은 성능개선의 여지(가능성?)이 더 존재한다는 것이니까 restapi보다 더욱 효율적인 api라고 생각이든다 :)

0개의 댓글