# 프로젝트 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
위 명령을 수행하면 다음과 같은 프로젝트가 생성된다.

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 서버를 위한 코드, 파일이 생성된다.
공식문서에 따르면
gqlgen은 Schema first approach를 따르기 때문에, GraphQL 쿼리로 API명세를 정의할 수 있다.schema.graphql에 정의된 스키마를 토대로 golang의 구조체와 resolver template와 같은 boilerplate를 자동 생성한다. 그래서 우리는 schema.graphqls에 스키마를 정의했고, gqlgen의 도움을 받아 요청을 처리할 로직을 schem.resolver.go에 구현할 것이다.
일반적인 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
}
// 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 내부를 한 번 뜯어보자...
튜토리얼 강의를 보면 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 파싱하고, 요청 처리에 사용할 변수를 만들기 위해 조금 복잡할 뿐이다.
NewDefaultServer은 http표준인 Handler 인터페이스를 구현한 Server라는 구현체를 반환한다.
Server 객체는 다음과 같다.Server struct {
transports []graphql.Transport
exec *executor.Executor
}
exec: 실재로 요청을 처리하는 객체transports가 존재한다.HTTP 요청이 발생하면 Server의 ServeHTTP가 요청을 처리한다.
ServeHTTP는
Server의 멤버 변수 배열에서, 요청을 처리할 수 있는 적절한Transport를 찾은 뒤, Transport.Do메서드에 멤버 변수exec을 파라미터로 전달하여 호출한다.참고
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
}
Transport 구현체인 POST의 Do메서드는 대략 다음과 같다.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))
}
Executeor의 DispatchOperation의 메서드는, 자신의 멤버 변수인 ExecutableSchema의 Exec메서드를 통해 최종적으로 요청을 처리한다. ExecutableSchema는 객체는 generated.go의 내용을 토대로 생성되었고, generated.go의 내용에 따라 schema.resolvers.go의 메서드들이 호출된다.앞 선 주제를 통해, gqlgen이 어떻게 작동하는지 파악했다.
이제 적절한 파라미터를 전달해서, 우리가 schema.graphqls에 정의한 내용을 토대로 서버가 작동하도록 해보자.
다시 맨 처음 코드로 돌아와서 GraphQL 서버를 설정하는 코드를 보면
config := graph.Config{Resolvers: &graph.Resolver{}}
executableSchema := graph.NewExecutableSchema(config)
srv := handler.NewDefaultServer(executableSchema)
다음 처럼 분석할 수 있다.
&graph.Resolver는 우리가 의존 관계를 설정한 구조체의 객체config와 executableSchema는 generated.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)