# 프로젝트 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)