본문 바로가기
golang

[golang] gqlgen을 사용하여 golang graphql 서버 구축하기(마무리)

by devjh 2022. 12. 10.
반응형

이번 게시글에서는 gorm을 사용하여 graphql서버의 전반적인 아키텍쳐를 구축하면 graphql 서버 구축을 마무리합니다.

소스코드는 github 링크에서 확인하실 수 있습니다(main 브랜치)

 

GitHub - jaeho310/golang-graphql-sample: golang graphql sample with gqlgen

golang graphql sample with gqlgen. Contribute to jaeho310/golang-graphql-sample development by creating an account on GitHub.

github.com

1. 패키지 구조

./
├── generated
│   └── generated.go
├── go.mod
├── go.sum
├── gqlgen.yml
├── graphql
│   └── user.graphql
├── main.go
├── model
│   └── user.go
├── repository
│   └── user_repository.go
├── resolver
│   ├── resolver.go
│   ├── types
│   │   └── types.go
│   └── user.resolvers.go
├── service
│   └── user_service.go
├── store
│   └── store.go
└── tools.go

패키지 구조는 3 tiered architecture를 사용하였습니다.

저수준의 모듈과 고수준의 모듈을 분리하여, 확장과 재사용에 유리하여 자주 사용되는 패키지 구조입니다.
resolver패키지가 표현 계층 servcie패키지가 비즈니스 계층 repository패키지가 영속성 계층 에 해당됩니다.

 

2. store/store.go

package store

import (
   "gorm.io/driver/sqlite"
   "gorm.io/gorm"
   "graphql-sample/model"
)

var DB *gorm.DB

func InitDatabase() {
   db, err := gorm.Open(sqlite.Open("local.db"), &gorm.Config{})
   if err != nil {
      panic(err)
   }
   err = db.AutoMigrate(&model.User{})
   if err != nil {
      panic(err)
   }
   DB = db
}

프로젝트의 시작점인 main문에서 gorm을 사용할수 있도록 InitDatabase()를 호출해줍니다.

사용자의 request가 아닌 프로젝트 init시에 일어나는 에러는 panic으로 처리해 db로드의 실패시 프로세스가 꺼지도록 합니다.

 

3. model/user.go 

package model

import (
   "graphql-sample/resolver/types"
   "strconv"
   "time"

   "gorm.io/gorm"
)

type User struct {
   ID        uint `gorm:"primaryKey"`
   Name      string
   CreatedAt time.Time
   UpdatedAt time.Time
   DeletedAt gorm.DeletedAt `gorm:"index"`
}

func (user *User) GetGqlResponse() *types.User {
   return &types.User{
      ID:   strconv.Itoa(int(user.ID)),
      Name: user.Name,
   }
}

func NewGormUser(input types.CreateUserInput) *User {
   return &User{
      Name: input.Name,
   }
}

gorm을 사용한 db model을 정의해줍니다.

db모델을 graphql response로 변환하는 메서드와 graphql request를 de모델로 변환하는 메서드를 만들어줍니다.

 

4.  resolver/user.resolver.go

// CreateUser is the resolver for the create_user field.
func (r *mutationResolver) CreateUser(ctx context.Context, input types.CreateUserInput) (*types.User, error) {
   user, err := service.UserService{}.CreateUser(input)
   if err != nil {
      return nil, err
   }
   return user.GetGqlResponse(), nil
}

// UserList is the resolver for the user_list field.
func (r *queryResolver) UserList(ctx context.Context) (*types.UserList, error) {
   userList, err := service.UserService{}.GetUserList()
   if err != nil {
      return nil, err
   }
   var list []*types.User
   for _, user := range userList {
      list = append(list, user.GetGqlResponse())
   }
   return &types.UserList{List: list}, nil
}

// User is the resolver for the user field.
func (r *queryResolver) User(ctx context.Context, input types.UserInput) (*types.User, error) {
   userId, err := strconv.Atoi(input.ID)
   if err != nil {
      return nil, err
   }
   user, err := service.UserService{}.GetUser(userId)
   if err != nil {
      return nil, err
   }
   // gorm model과 types.user는 필드와 타입이 다르니 변환하는 메서드를 하나 구축합니다.
   return user.GetGqlResponse(), nil
}

표현계층인 resolver에서는 interface영역의 데이터가 정상적으로 들어왔는지를 확인하고 service패키지의 비즈니스 코드를 호출합니다.

또한 비즈니스 코드를 호출한 후 내려줘야할 데이터를 변환하는 작업도 수행합니다.

 

5. service/user_service.go

package service

import (
   "graphql-sample/model"
   "graphql-sample/repository"
   "graphql-sample/resolver/types"
)

type UserService struct{}

func (UserService) GetUserList() ([]model.User, error) {
   return repository.UserRepository{}.FindAll()
}

func (UserService) GetUser(userId int) (*model.User, error) {
   return repository.UserRepository{}.FindById(userId)

}

func (UserService) CreateUser(input types.CreateUserInput) (*model.User, error) {
   return repository.UserRepository{}.CreateUser(input)

}

 

비즈니스 로직을 작성하기 위한 레이어입니다.

네임스페이스 충돌을 막기위해 더미 struct를 하나 만들었습니다.

golang은 package내 동일한 메서드, 변수명을 사용할수 없습니다.(패키지충돌, 네임스페이스충돌 이라고 부릅니다)

그렇다고 service/user/service.go 등으로 패키지를 나눠버리면 추후 순환참조로 인해 고생할 확률이 있으니 네임스페이스 충돌을 피하기 힘들어 보이는경우 더미 struct로 피해갈 수 있습니다.

 

6. repository/user_repository

package repository

import (
   "graphql-sample/model"
   "graphql-sample/resolver/types"
   "graphql-sample/store"
)

type UserRepository struct{}

func (UserRepository) FindAll() ([]model.User, error) {
   var userList []model.User
   err := store.DB.Find(&userList).Error // Find의 두번째 파라미터로 id list를 넘겨주지 않으면 모든 record 조회
   if err != nil {
      return nil, err
   }
   return userList, nil
}

func (UserRepository) FindById(id int) (*model.User, error) {
   user := model.User{}
   err := store.DB.First(&user, id).Error
   if err != nil {
      return nil, err
   }
   return &user, nil
}

func (UserRepository) CreateUser(input types.CreateUserInput) (*model.User, error) {
   user := model.NewGormUser(input)
   err := store.DB.Save(user).Error
   if err != nil {
      return nil, err
   }
   return user, nil
}

repository레이어에서는 데이터 저장관련 로직을 작성합니다.

더미 struct로 인해 네임스페이스 충돌없이 FindAll(), FindById() 등의 이름을 사용할 수 있습니다.

create같은경우 해당 레이어에서 graphql input을 gorm model로 변환해 저장합니다.

 

 

 

 

 

 

 

반응형

댓글