-
DataLoaderGraphql 2022. 10. 9. 13:50
기본적으로 graphQL에 대해 어느정도 알고 있다고 생각하고 작성하겠다.
DataLoader란
graphQL에서 Resolver를 사용할 때 N+1 문제가 생긴다. N+1 문제란 요청은 한번만 했지만 Resolver에 의해 N번의 트랜젝션이 발생할 수 있는 것이다.
예를 들면, user, purchase, stuff 이 3개의 table이 있다고 가정해보고 gql 타입을 아래와 같이 정의했다고 가정하자.
type User { id: String! name: String! email: String! purchases: [Purchase] # purchase를 조회하기 위한 Resolver가 있음 } type Purchase { id: String! total_price: Int! date: Time! stuffs: [Stuff] #Stuff를 조회하기 위한 Resolver가 있음 } type Stuff { name: String! price: Int! link: String! } Query { userDetail(id: String!): User! }
이 상황에서 userDetail query를 호출하면 User정보를 위해 DB 조회 1회, 여러 Purchase 조회를 위한 DB 조회 1회 Purchase 하나마다 Stuff조회를 따로해야하므로 Purchase의 갯수만큼 Stuff 조회를 위한 DB 조회 N 번이 필요하다.
이는 N이 커지면 커질수록 DB에 과부화를 줄 여지가 높아진다. 따라서 이를 해결하기 위해 DataLoader가 개발됐다.
위의 예시에서 dataLoader는 purchase의 id를 key 값으로 사용하고 이 id를 배열로 모아서 1번의 트랜잭션 Or 연산으로 stuff를 찾을 것이다
DataLoader의 동작 방식은 아주 간단하고 아래와 같다:
- Batch func을 선언한다.
- 이는 dataloader 모듈에서 관리하는데 대략 몇 ms안에 들어온 명령에 대해 그 Key값을 모았다가 한번에 요청하도록 도와준다.
- 사용 방법은 어느정도 차이가 있지만 아래에 golang으로 예시를 작성하겠다.
- middleware에서 context에 해당 batch func을 삽입한다.
- 필요한 resolver에서 dataLoader의 값을 Load해온다.
주의할 점
- dataLoader에 요청한 키 값의 길이와 데이터를 조회한 결과값의 길이가 달라서는 안된다. 즉, 위에 예시에 이어서 만약에 조회 할 때 purchase에 해당하는 stuff가 하나도 존재하지 않아서 Null 값을 return할 수 있다. 이 때, 생략해서는 안된다.
Golang 코드 예시
graph/initialize.go
package graph import ( "context" "log" "net/http" "os" "github.com/99designs/gqlgen/graphql/handler" "github.com/99designs/gqlgen/graphql/handler/transport" "github.com/99designs/gqlgen/graphql/playground" "github.com/caveakorea/goback.git/graph/generated" "github.com/caveakorea/goback.git/service" "github.com/go-chi/chi" "github.com/gorilla/websocket" "github.com/graph-gophers/dataloader" "github.com/rs/cors" ) type ctxKey string const ( loadersKey = ctxKey("dataloaders") uidKey = ctxKey("uid") ) var loader *Loaders // Loaders wrap your data loaders to inject via middleware type Loaders struct { VenuesByEventLoader *dataloader.Loader } // NewLoaders instantiates data loaders for the middleware func NewLoaders() *Loaders { // define the data loader loaders := &Loaders{ VenuesByEventLoader: dataloader.NewBatchedLoader(service.GetVenuesByEvents), } return loaders } func initializeGraphQLServer() { loader = NewLoaders() } func startGraphQLServer(port string) { router := chi.NewRouter() debugVar := true if len(os.Args) > 1 { debugVar = false } router.Use(cors.New(cors.Options{ AllowedOrigins: []string{"*"}, AllowedHeaders: []string{"*"}, AllowCredentials: true, Debug: debugVar, }).Handler) router.Use(Middleware()) srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &Resolver{}})) srv.AddTransport(&transport.Websocket{ Upgrader: websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { // Check against your desired domains here log.Println(r.Host) return r.Host == "studio.apollographql.com" }, ReadBufferSize: 1024, WriteBufferSize: 1024, }, }) router.Handle("/playground", playground.Handler("GraphQL playground", "/query")) router.Handle("/query", srv) log.Printf("connect to http://localhost:%s/ for GraphQL playground", port) log.Fatal(http.ListenAndServe(":"+port, router)) } func InitializeController() { initializeGraphQLServer() } func StartController(graphQLPort string) { startGraphQLServer(graphQLPort) } // Middleware decodes the share session cookie and packs the session into context func Middleware() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { uid := r.Header.Get("Uid") ctx := context.WithValue(r.Context(), "uid", uid) r = r.WithContext(ctx) ctx = context.WithValue(r.Context(), "dataloaders", loader) r = r.WithContext(ctx) next.ServeHTTP(w, r) }) } }
resolver.go
func (r *eventResolver) Venue(ctx context.Context, obj *model.Event) (*model.VenueDetail, error) { // 여기에 데이터로더 적용해보자 loaders := ctx.Value("dataloaders").(*Loaders) thunk := loaders.VenuesByEventLoader.Load(ctx, dataloader.StringKey(strconv.FormatInt(obj.UID, 10))) result, err := thunk() if err != nil || result == nil { return nil, err } return result.(*model.VenueDetail), nil }
service/venue.go
func GetVenuesByEvents(ctx context.Context, keys dataloader.Keys) []*dataloader.Result { store := infrastructure.GetStore() event_ids := make([]int64, len(keys)) for ix, key := range keys { val, err := strconv.ParseInt(key.String(), 10, 64) if err != nil { event_ids[ix] = 0 } else { event_ids[ix] = val } } venues, err := store.GetVenueByEventIDs(ctx, event_ids) if err != nil { return []*dataloader.Result{} } // return users in the same order requested output := []*dataloader.Result{} venuesMap := map[string]int{} for idx, venue := range venues { venuesMap[strconv.FormatInt(venue.EveID, 10)] = idx } for _, key := range keys { if _, ok := venuesMap[key.String()]; !ok { // 찾지 못하더라도 nil 넣어줘야함 output = append(output, &dataloader.Result{Data: nil, Error: nil}) continue } venue := venues[venuesMap[key.String()]] rate, err := strconv.ParseFloat(venue.Rate, 64) if err != nil { log.Println("check DB Rate EVENT UID: ", venue.Uid) } output = append(output, &dataloader.Result{Data: &model.VenueDetail{ ... }, Error: nil}) } return output }
실제 코드를 붙여넣은거라 적용하려면 본인의 코드에 맞게 바꾸면 될 듯 하다.
'Graphql' 카테고리의 다른 글
GraphQL에 대하여 ( Resolver & Dataloader / Aliases & Fragment) (1) 2023.06.03 GraphQL에 대하여 (Interface & Union Type) (0) 2023.05.07 GraphQL (Schema & Type) (0) 2023.04.23 GraphQL이란 (0) 2023.04.16 - Batch func을 선언한다.