본문 바로가기
IT&Tech

Golang을 이용한 Graphql서버 구현

by walter 2022. 1. 20.

안녕하세요 Walter입니다. 

이번시간은 Golang을 이용해서 Graphql서버를 구현하고자 합니다. 

개인적으로 Graphql 서버를 구현할때 자주 사용하는 라이브러리가 있어서 같이 소개하고자 합니다. 

 


Golang 다운로드 

맥사용자는 brew install golang도 가능하지만 개인적으로, 아래와같이 설치를 선호합니다. 

Golang 다운로드 https://go.dev/dl/  사이트에서 다운로드 

 


Tools 설치 

https://github.com/golang/tools  


Graphql 라이브러리 고려사항

golang에서 graphql 라이브러리가 몇가지 있습니다.  크게는 2~3개가 유명하지만,

라이브러리를 사용하는데 있어서 선정기준을 정했습니다.

 

1. 스키마 우선접근의 제공

2. 스키마를 통한 코드 제너레이터 제공여부  - resovler, method 등등 일일이 만들기위해서는 많은 노동이 소요됩니다. 

3. Apollo가 인정한 공식 라이브러리인가?

4. 별점과 지속적인 업데이트 라이브러리인가? 

 

위의 고려사항을 통해서 https://github.com/99designs/gqlgen  위의 라이브러리를 사용하게 됐습니다. 

https://gqlgen.com/ 위의 링크를 참조하여 코딩을 실행해보겠습니다. 

 


코딩 시작

1. 서비스 모듈 시작 

golang은 1.11버전이상부터 모듈시스템을 적용했습니다.  기존 gopath의 위치한 설정과 관계없이

아래와 같이 모듈을 생성하여 코드 작성 가능합니다. 

# 컨테이너 서비스로 등록할 Go 프로젝트 폴더 생성합니다. 
mkdir myservice 

# myservice라는 모듈 초기화 합니다. 
go mod init myservice
-> go: creating new go.mod: module myservice

total 8
drwxr-xr-x   3 user  staff   96  1 20 00:11 .
drwxr-xr-x  15 user  staff  480  1 20 00:10 ..
-rw-r--r--   1 user  staff   26  1 20 00:11 go.mod

 

2. Makefile 생성 

간단하게 서비스를 실행할 makefile을 만들었습니다. 

gen:
	go get -d github.com/99designs/gqlgen
	cd graphql/todo && gqlgen

start: 
	go mod tidy
	go run main.go

 

3. todo의 간단하 조회 구현해보자 

graphql 스키마 기본 구조 생성

# graphql 모움의 폴더에 todo 서비스 구현을 위해서 생성합니다. 
mkdir -p graphql/todo && cd graphql/todo

# graphql schema generate를 위한 설정을 세팅합니다. 
touch .gqlgen.yml

# 쿼리용 정의를 생성합니다. 
touch query.graphql 

# 데이타 enum, input용, 데이타 타입정의 폴더를 생성합니다. 
mkdir -p schema/enum schema/input schema/type

 

gqlgen gernerate파일  - .gqlgen.yml 아래와 같이 입력합니다. 

 

schema : graphql을 참조하는 폴더 & 적용합니다. 

exec: 실행시 gernerate 폴더와 파일 이름을 설정합니다. 

model : schema를 통해서 생성되는 모델 설정입니다. 

resovler: 구현실체입니다.  gernerate되어 생성되지만,  실제 비지니스 코드와 연결하여 수정할 곳입니다. 

schema:
  - schema/**.graphql
  - ./*.graphql

exec:
  filename: gen/generated.go
  package: gen

federation:
  filename: gen/federation.go
  package: gen


model:
  filename: gen/graphqlmodel/models_gen.go
  package: graphqlmodel


resolver:
  layout: follow-schema
  dir: gen/resolver
  package: resolver

models:
  ID:
    model:
      - github.com/99designs/gqlgen/graphql.ID
      - github.com/99designs/gqlgen/graphql.Int
      - github.com/99designs/gqlgen/graphql.Int64
      - github.com/99designs/gqlgen/graphql.Int32
  Int:
    model:
      - github.com/99designs/gqlgen/graphql.Int
      - github.com/99designs/gqlgen/graphql.Int64
      - github.com/99designs/gqlgen/graphql.Int32

 

Query 함수정의

query.graphql 이름으로 아래와 같이 생성합니다. 

type Query {
  # Todo 
  todo(where: TodoWhere!): Todo!
}

 

타입 정의 

schema/type/todo.graphql -  데이타 타입

schema/input/tood.graphql - 입력 타입 

schema/enum/tood.graphql - enum 타입 

# Todo 할일정의 
type Todo {
    # 제목
    title: String! 
    # 셜명 
    content: String!
    # 타입 
    type: TodoType!
}

# 할일 타입 
enum TodoType {
  # 매일
  DAYLY
  # 매주
  WEEKLY
}

# TodoWhere 조건
input TodoWhere {
  todoType: TodoType!
}

 

4. grahql 코드 제너레이터 실행

go get -d github.com/99designs/gqlgen
cd graphql/todo && gqlgen

 

5. 메인 함수 & Graphql 서버 실행 구현

package main

import (
	"context"
	"flag"
	"fmt"
	"myservice/graphql/todo/gen"
	"myservice/graphql/todo/gen/resolver"
	"net/http"
	"time"

	graphqlHandler "github.com/99designs/gqlgen/graphql/handler"
	"github.com/99designs/gqlgen/graphql/playground"
	"github.com/rs/cors"
	"google.golang.org/grpc/metadata"
)

const servicePort = "9999"

func main() {
	errc := make(chan error)
	ctx := context.Background()

	startHTTPServer(ctx, servicePort, errc)
	fmt.Print("exit", <-errc)
}

// graphql server start
func startHTTPServer(
	ctx context.Context,
	port string,
	errc chan error,
) {
	var (
		httpAddr          = flag.String("http", ":"+port, "HTTP listen address")
		readHeaderTimeout = 60 * time.Second
		writeTimeout      = 60 * time.Second
		idleTimeout       = 60 * time.Second
		maxHeaderBytes    = http.DefaultMaxHeaderBytes
		queryPath         = "/query"
		graphqlPath       = "/graphql"
	)

	// http 세팅
	newResolver := resolver.NewResolver(
		ctx,
	)

	middleware := authMiddleware(ctx, graphqlHandler.NewDefaultServer(
		gen.NewExecutableSchema(gen.Config{Resolvers: &newResolver}),
	))
	mux := http.NewServeMux()
	mux.Handle(queryPath, middleware)
	mux.Handle(graphqlPath, playground.Handler("GraphQL playground", queryPath))
	handler := cors.New(corsOption()).Handler(mux)
	http := &http.Server{
		Addr:              *httpAddr,
		Handler:           handler,
		ReadHeaderTimeout: readHeaderTimeout,
		WriteTimeout:      writeTimeout,
		IdleTimeout:       idleTimeout,
		MaxHeaderBytes:    maxHeaderBytes,
	}

	// 서버 스타트
	go func() {
		fmt.Printf(
			"connect to http://localhost%s/graphql for GraphQL playground",
			*httpAddr,
		)
		errc <- http.ListenAndServe()
	}()
}

// auth token
func authMiddleware(ctx context.Context, h http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		md := metadata.Pairs("Authorization", r.Header.Get("Authorization"))
		ctx = metadata.NewIncomingContext(ctx, md)
		r = r.WithContext(ctx)
		h.ServeHTTP(w, r)
	})
}

// corsOption cors option
func corsOption() cors.Options {
	return cors.Options{
		AllowedOrigins: []string{"*"},
		AllowedMethods: []string{"HEAD", "GET", "POST", "PUT", "PATCH", "DELETE"},
		AllowedHeaders: []string{
			"Origin",
			"Content-Type",
			"Access-Control-Allow-Headers",
			"DeviceInfo",
			"Authorization",
			"X-Requested-With",
		},
		AllowCredentials: false,
	}
}

 

6. Todo 구현 

todo/query.go

package todo

import (
	"context"
	"myservice/graphql/todo/gen/graphqlmodel"
	"sync"
)

var (
	instance *todo = nil
	once     sync.Once
)

type todo struct {
	ctx context.Context
}

// Todo 인터페이스
type Todo interface {
	Todo(ctx context.Context, where graphqlmodel.TodoWhere) (*graphqlmodel.Todo, error)
}

// NewTodo 서비스
func NewTodo(
	ctx context.Context,
) Todo {
	once.Do(func() {
		instance = &todo{
			ctx: ctx,
		}
	})
	return instance
}

// Todo 조회
func (s *todo) Todo(
	ctx context.Context,
	where graphqlmodel.TodoWhere,
) (*graphqlmodel.Todo, error) {
	return &graphqlmodel.Todo{
		Title:   "할일",
		Content: "내일",
		Type:    graphqlmodel.TodoTypeDayly,
	}, nil
}

Makefie 실행 

make gen 
make start

결과 화면 

 


기타

소스에 대한 자세한 설명이 많이 빠져있어서 아쉽지만,  아래 다운로드를 통해서 실제 테스트해보며, graphql 데이타 스키마를 구현해 보는게 좋을듯 합니다. 

 

DDD구현을 위한 Graphql 스키마를 설계합니다.  각각의 영역별로 폴더로 나누어서 구현합니다. 

도메인 이벤트, 모델작성, 명령 command등등의 도메인 전문가와 함께 graphql 스키마를 구현하며 작업을 우선시 하면 좋을듯합니다. 

generate과정을 통해서 실제 비즈니스를 구현하며, 특정 영역에 따른 MSA구현하면 좋을듯합니다. 

 

다운로드 소스코드

https://github.com/csk6124/graphql_golang_myservice.git