gqlgen으로 GraphQL 서버 만들기

dev_314·2023년 5월 29일
0

다짜고짜 환경 설정

# 프로젝트 init 
go mod init github.com/wonju-dev

# gqlgen 의존성 설정
# gqlgen 이후에 버전을 명시할 수 있음 (생략하면 최신 버전)
go get -d github.com/99designs/gqlgen

# gqlgen 프로젝트 구조 초기화
go run github.com/99designs/gqlgen init

위 명령을 수행하면 다음과 같은 프로젝트가 생성된다.

GraphQL Schema 정의하기

graph/sechema.graphqls회원 CRUD와 관련한 GraphQL 스키마를 정의한다.

GrahpQL 문법은 다음에서 설명

type Query {
  getUser(
    input: QgetUserInput
  ): [QgetUserOutput!]!
}

input QgetUserInput {
  id: [ID!]
  name: [String!]
  age: [Int !]
}

type QgetUserOutput {
  status: Boolean!
  result : [User!]
}

type Mutation {
  addUser(
    input: MaddUserInput
  ): MaddUserOutput

  updateUser(
    input: MupdateUserInput
  ): MupdateUserOutput

  deleteUser(
    input: MdeleteUserInput
  ): MdeleteUserOutput
}

input MupdateUserInput {
  id: ID!
  name: String
  age: Int
}
input MdeleteUserInput {
  ids: [ID!]
  names: [String!]
  ages: [Int!]
}

type MupdateUserOutput {
  state: Boolean
  results: User
}

type MdeleteUserOutput {
  state: Boolean
  results: [User]
}

input MaddUserInput {
  name: String!
  age: Int!
}

type MaddUserOutput{
  status: Boolean!
  result: User
}

type User {
  id: ID
  name: String!
  age: Int!
}

그런다음 go run github.com/99designs/gqlgen generate 명령을 수행하면 자동으로 GraphQL 서버를 위한 코드, 파일이 생성된다.

공식문서에 따르면

  1. gqlgenSchema first approach를 따르기 때문에, GraphQL 쿼리로 API명세를 정의할 수 있다.
  2. 위와 같은 접근법을 지원하기 위해, schema.graphql에 정의된 스키마를 토대로 golang의 구조체resolver template와 같은 boilerplate를 자동 생성한다.

그래서 우리는 schema.graphqls에 스키마를 정의했고, gqlgen의 도움을 받아 요청을 처리할 로직을 schem.resolver.go에 구현할 것이다.

유저 Create 예제

일반적인 Controller(resolver) - Service 구조를 만들기 위해 의존성을 주입한다.

// graph/resolver.go
package graph

import "github.com/wonju-dev/service/user"

// This file will not be regenerated automatically.
//
// It serves as dependency injection for your app, add any dependencies you require here.

type Resolver struct {
	userService user.UserService
}

Create

// graph/schema.resolvers.go
// AddUser is the resolver for the addUser field.
func (r *mutationResolver) AddUser(ctx context.Context, input *model.MaddUserInput) (*model.MaddUserOutput, error) {
	return r.userService.CreateUser(ctx, input)
}

// userService.go
package user

import (
	"context"
	"github.com/wonju-dev/graph/model"
	"github.com/wonju-dev/repository/User"
)

type UserService struct {
	userRepository User.UserRepository
}

func (userService *UserService) CreateUser(ctx context.Context, input *model.MaddUserInput) (*model.MaddUserOutput, error) {
	err := userService.userRepository.AddUser(input)
	if err != nil {
		return nil, err
	}

	return &model.MaddUserOutput{
		Status: true,
		Result: &model.User{
			Name: input.Name,
			Age:  input.Age,
		},
	}, nil
}

의존성 주입 실패

위 처럼 만든 다음 로컬에서 테스트를 했는데, nil pointer error가 발생한다.

생각해 보니 의존성 관계를 설정만 했지, 실제로 의존성을 주입한 부분이 없다.

당최 gqlgen은 어떻게 자동하는지 모르겠다. gqlgen 내부를 한 번 뜯어보자...

gqlgen 구성, 작동 방식

튜토리얼 강의를 보면 GraphQL 서버를 만든 다음, golang의 표준http라이브러리를 통해 서버를 띄운다.

	...
	srv := handler.NewDefaultServer(graph.NewExecutableSchema(graph.Config{Resolvers: &graph.Resolver{}}))

	http.Handle("/query", srv)
    ...

기본적으로 GraphQL은 HTTP Protocol을 기반으로 작동한다.
그렇기 때문에, GraphQL 요청을 처리하는 녀석(?)은 단순히 Request Handler와 다르지 않다. 다만 Requset Body 파싱하고, 요청 처리에 사용할 변수를 만들기 위해 조금 복잡할 뿐이다.

gqlgen 작동 방식

  1. NewDefaultServerhttp표준인 Handler 인터페이스를 구현한 Server라는 구현체를 반환한다.

    • Server 객체는 다음과 같다.
    Server struct {
    	transports []graphql.Transport
    	exec       *executor.Executor
    }
    • exec: 실재로 요청을 처리하는 객체
    • 참고: 사실 GraphQL은 POST말고, GET, WebSocket 으로도 요청을 보낼 수 있다. 서로 다른 요청 방식을 표준화 하기 위해 transports가 존재한다.
  2. HTTP 요청이 발생하면 ServerServeHTTP가 요청을 처리한다.

  3. ServeHTTP

    1. Server의 멤버 변수 배열에서, 요청을 처리할 수 있는 적절한Transport를 찾은 뒤,
    2. 선택된 Transport.Do메서드에 멤버 변수exec을 파라미터로 전달하여 호출한다.
    3. 아래에서 설명될 하위 메서드에서 만든 Response를 전송
    참고
    1. `Transport`는 `Server`객체 생성 시점에 자동으로 등록된다. (`NewDefaultServer`를 사용하는 경우)
    2. `New`를 통해 직접 서버를 만들 수도 있는데, 이 경우에는 직접 `Transport`를 등록해야 한다.
    3. 이번 분석에서는 NewDefaultServer를 통해 자동으로 등록된 `Transport`구현체인 POST만 다룬다. (일반적으로 GraphQL은 POST method로 이용하므로)
	// Transport는 단순히 인터페이스고
	// NewDefaultServer을 사용하는 경우 gqlgen은 자동으로 
	// Transport 구현체(GET, POST, WebSocket, Option, ...)를 등록한다.
	Transport interface {
		Supports(r *http.Request) bool // Transport가 Http Method를 구분할 때 사용
		Do(w http.ResponseWriter, r *http.Request, exec GraphExecutor)
	}
  • exec의 타입인 Executor은 다음과 같다.
type Executor struct {
	es         graphql.ExecutableSchema
	extensions []graphql.HandlerExtension
	ext        extensions

	errorPresenter graphql.ErrorPresenterFunc
	recoverFunc    graphql.RecoverFunc
	queryCache     graphql.Cache
}

다 필요 없고, 몇 가지 포인트만 짚자면

1. `Executor`은 `ExecutableSchema`을 멤버 변수로 갖는다.
2. `Executor`는 `GraphExecutor` 인터페이스를 구현체이다.
3. `Executor`는 `Server` 객체 생성시에 같이 생성되고, `Server`에 멤버 변수로 할당된다.
GraphExecutor interface {
	CreateOperationContext(ctx context.Context, params *RawParams) (*OperationContext, gqlerror.List)
	DispatchOperation(ctx context.Context, rc *OperationContext) (ResponseHandler, context.Context)
	DispatchError(ctx context.Context, list gqlerror.List) *Response
}
  1. (다시 원래 흐름으로 돌아와서)Transport 구현체인 POSTDo메서드는 대략 다음과 같다.
func (h POST) Do(w http.ResponseWriter, r *http.Request, exec graphql.GraphExecutor) {
    1. 기본적인 Response Message Header 구성
	2. Request Message Body 검증
    	2.1 유효하지 않은 포맷이면 Error를 Dispatch
	3. OperationContext 생성
    4. 요청 처리
	var responses graphql.ResponseHandler
	responses, ctx = exec.DispatchOperation(ctx, rc)
	5. Response Message 생성
    writeJson(w, responses(ctx))
}
  1. ExecuteorDispatchOperation의 메서드는, 자신의 멤버 변수인 ExecutableSchemaExec메서드를 통해 최종적으로 요청을 처리한다.
  2. ExecutableSchema는 객체는 generated.go의 내용을 토대로 생성되었고, generated.go의 내용에 따라 schema.resolvers.go의 메서드들이 호출된다.

gqlgen 사용 방법 (의존성 주입)

앞 선 주제를 통해, gqlgen이 어떻게 작동하는지 파악했다.
이제 적절한 파라미터를 전달해서, 우리가 schema.graphqls에 정의한 내용을 토대로 서버가 작동하도록 해보자.

다시 맨 처음 코드로 돌아와서 GraphQL 서버를 설정하는 코드를 보면

config := graph.Config{Resolvers: &graph.Resolver{}}
executableSchema := graph.NewExecutableSchema(config)
srv := handler.NewDefaultServer(executableSchema)

다음 처럼 분석할 수 있다.

  1. &graph.Resolver는 우리가 의존 관계를 설정한 구조체의 객체
    • 여기에 의존성 주입을 하면 된다.
  2. configexecutableSchemagenerated.go를 토대로 생성되었고, gqlgen은 executableSchema을 통해 schema.resolvers의 메서드를 사용하게 된다.

따라서 다음과 같이 의존성을 주입하면 된다.

config := graph.Config{
	Resolvers: &graph.Resolver{
		user.UserService{ // Resolver가 사용할 Service
			User.UserRepositoryImpl{ // Service가 사용할 Repository
				// 대충 db config 관련 내용
			},
		},
	},
}
executableSchema := graph.NewExecutableSchema(config)
srv := handler.NewDefaultServer(executableSchema)
profile
블로그 이전했습니다 https://dev314.tistory.com/

0개의 댓글