Close

Golang + GraphQL을 사용한 Todo

Golang + GraphQL을 사용한 Todo

GraphQL을 이용한 서비스를 계획하며 연습했던 Todo 중 중요한 부분을 정리한 것입니다.

이 글은 GraphQL서버를 Go 언어로 구현하여 Postgre와의 간단하게 연동하는것을 목표로 합니다.

구성도

todo-diagram.jpeg

DB 마이그레이션

데이터베이스를 쿼리를 사용하지 않고 코드와의 연동을 위하여 ORM을 사용합니다.

프로젝트에 사용된 ORM은 go-pg 입니다.

// 모델 정의
type Default struct {  
   ID        int64              `json:"id,omitempty"`  
  CreatedAt time.Time   `sql:"default:now()"`  
  DeletedAt time.Time   `pg:",soft_delete"`  
  UpdatedAt time.Time  
}

type Todo struct {  
   tableName struct{}   `sql:"todo" json"omitempty"`
  Default  
  Todo     string           `json:"todo"`
  Priority int64            `json:"priority"`
}

......

// 테이블 생성 
err := db.CreateTable(model, &orm.CreateTableOptions{  
    FKConstraints: true,  
})

GraphQL 패키지 설치

다음줄을 실행하여 GraphQL Go 패키지를 설치합니다.

go get github.com/graphql-go/graphql

패키지에 대한 더 많은 정보는 프로젝트의 GitHub페이지에서 얻을수 있습니다.

GraphQL 타입 정의

ORM을 사용하면 모델과 데이터베이스를 맵핑하여 사용할수 있었습니다. 하지만 GraphQL 패키지는 ORM과는 다르게 사용하려면 GraphQL에서 사용할 타입(필드)을 정의해줘야 합니다.

func (*Todo) GrapqlType() *graphql.Object {  
   return graphql.NewObject(graphql.ObjectConfig{  
      Name: "Todo",  
      Fields: graphql.Fields{  
         "id": &graphql.Field{  
              Type: graphql.String,  
          },  
          "todo": &graphql.Field{  
              Type: graphql.String,  
          },  
          "priority": &graphql.Field{  
              Type: graphql.String,  
          },  
      },  
  })  
}

기본적으로 제공되는 필드 타입은 다음과 같습니다.
– graphql.Int
– graphql.Float
– graphql.String
– graphql.Boolean
– 기타

모델과의 타입을 일치시켜야 합니다 !!!
javascript는 기본적으로 Int32까지 밖에 지원하지 않습니다.
따라서 int64를 이용하려면 값을 string으로 받아 strconv.ParseInt을 사용하여 string값을 int64로 변환해줘야 합니다.

Query

GraphQL의 Query에 대하여 정의해줍니다.
기본적으로 graphql object로 선언되며 Name, Fields로 이루어져 있습니다.

여기서 Name을 통하여 Query인지 Mutation인지 구분합니다.
Fields는 해당 object의 내용을 정의합니다.

Fields는 Field 구조체의 집합이고, Fields에서 쿼리에서 사용할 명령어를 key값으로 두고 Field를 구성합니다.

이러한 Field는 Type, Args, Resolve로 이루어져 있으며
각각 Type 은 GraphQL의 타입을 정의하고 Args는 키값을 전달 받는 부분입니다. 그리고 Resolve를 통하여 함수를 동작시킵니다.

func (*Todo) Query() *graphql.Object {  
   return graphql.NewObject(graphql.ObjectConfig{  
      Name: "Query",  // Query or Mutation
      Fields: graphql.Fields{  
         "todos": &graphql.Field{  

            Type: graphql.NewList((*Todo)(nil).GrapqlType()),  

            Args: graphql.FieldConfigArgument{  
               "priority": &graphql.ArgumentConfig{  
                  Type: graphql.NewNonNull(graphql.String),  
                },  
            }, 
            Resolve: func(p graphql.ResolveParams) (interface{}, error) {  
               priority := p.Args["priority"].(string)  
               // select  
              var todo []Todo  

              db, _ := modules.OpenDB(true)  
               defer db.Close()  

               err := db.Model(&todo).Where("priority = ?", priority).Select()  
               fmt.Println(todo)  
               if err != nil {  
                  fmt.Println(err)  
               }  

              return todo, nil  
              },  
          },  
      },  
  })  
}

Query

{
    todos(priority) {
        todo,
        priority
    }
}

Mutation

Mutation의 구조도 앞서 Query에서 설명한 구조와 동일합니다.

func (*Todo) Mutation() *graphql.Object {  
   return graphql.NewObject(graphql.ObjectConfig{  
      Name: "Mutation",  
      Fields: graphql.Fields{  
         "createTodo": &graphql.Field{  
            Type: (*Todo)(nil).GrapqlType(),  
            Args: graphql.FieldConfigArgument{  
               "todo": &graphql.ArgumentConfig{  
                  Type: graphql.NewNonNull(graphql.String),  
                },  
               "priority": &graphql.ArgumentConfig{  
                  Type: graphql.NewNonNull(graphql.String),  
                },  
            },  
            Resolve: func(p graphql.ResolveParams) (interface{}, error) {  
               todo := new(Todo)  
               todo.Todo = p.Args["todo"].(string)  
               todo.Priority, _ = strconv.ParseInt(p.Args["priority"].(string), 10, 64)  

               // insert  
              db, _ := modules.OpenDB(true)  
              err := db.Insert(todo)  
              if err != nil {  
                 fmt.Println(err)  
              }  
              return nil, nil  
          },  
       },  
    },  
  })  
}

GraphQL 스키마 생성

위에서 구성한 Query와 Mutation을 실제로 사용하기 위하여 하나의 스키마로 구성 합니다.

func (*Todo) Schema() graphql.Schema {  
   schema, _ := graphql.NewSchema(graphql.SchemaConfig{  
      Query:    (*Todo)(nil).Query(),  
      Mutation: (*Todo)(nil).Mutation(),  
    })  
   return schema  
}

Mutation

{
      createTodo(todo:"Hello, World!", priority:"1") {
          todo,
          priority
      }
}

Http 패키지로 배포하기

이제 동작을 하는지 확인하기 위하여 http 패키지로 배포해 봅시다.

http.HandleFunc("/graphql", func(w http.ResponseWriter, r *http.Request) {  
   result := graphql.Do(graphql.Params{  
      Schema:        (*model.Todo)(nil).Schema(),  
      RequestString: r.URL.Query().Get("query"),  
   })  
   json.NewEncoder(w).Encode(result)  
})  
http.ListenAndServe(":3000", nil)

테스트

서버가 정상적으로 가동되면 쿼리가 정상적으로 동작하는지 테스트 해봅시다.

# Mutation 
curl -g 'http://localhost:3000/graphql?query=mutation{createTodo(todo:"Hello,World!",priority:"1"){todo,priority}}'

실행결과 :
todo-mutation.png

# Query
curl -g 'http://localhost:3000/graphql?query={todos(priority:"1"){todo,priority}}'

실행결과:
todo-query.png

진행하며…

Go언어로 GraphQL을 구현한 자료가 거의 없어 어떻게 프로젝트 구조를 잡아야할지 개념적인 부분에서 어려움이 있었고

SQL 예약어를 깜빡하고 order를 키값으로 진행하다가 에러가 나던 부분이 존재하였습니다.

실제 쿼리를 사용하는 방법이 기존의 RESTAPI를 구성하는 방법보다 더 편하지만 Node와는 다르게 타입이 맵핑이 안되고 두번선언(Struct, Graphql Type)해줘야 한다는 부분에서 아쉬움이 존재합니다.


전체 코드는 GITLAB에서 볼수 있씁니다.

Leave a Reply

avatar
  Subscribe  
Notify of
%d 블로거가 이것을 좋아합니다: