Part3: 실시간 구독 서비스 리졸버 만들기

송톰톰·2023년 1월 11일
0
post-thumbnail

commentAdded 구독 이해

commentAdded 구독은 특정 블로그 게시물에 새 댓글이 추가될 때마다 구독 클라이언트에게 알립니다.

Apollo Client Subscription 예제 구현을 위해 GraphQL 스키마를 정의합니다.

  • schema.graphqls
type Post {
    id: ID!
}

type Comment {
    id: ID!
    postId: ID!
    content: String!
}

type Query {
    comments(where:CommentsWhere!): [Comment]!
}

type Mutation {
    createPost:Post!
    createComment(input: CreateCommentInput!): Comment!
}

type Subscription {
    commentAdded:Comment!
}

input CreateCommentInput{
    postId: ID!
    content: String!
}

input CommentsWhere {
    postId: ID!
}
  • createComment: Comment 생성
  • comments: Comment 리스트
  • commentAdded: Comment 가 추가될 때마다 클라이언트에 구독 정보를 전달

MySQL 컨테이너 실행

  • docker-compose.yml
version: "3.7"

services:
  mysql:
    image: mysql:5.7
    container_name: example_mysql
    environment:
      MYSQL_USER: test
      MYSQL_PASSWORD: test
      MYSQL_ROOT_PASSWORD: test
      MYSQL_DATABASE: test
    ports:
      - 33006:3306

docker-composeup 하고 MySQL 을 시작 합니다.

$ docker-compose -f ./docker-compose.yml up --build -d

성공할 경우:

CONTAINER ID   IMAGE       COMMAND                  CREATED             STATUS             PORTS                                NAMES
306240f2708d   mysql:5.7   "docker-entrypoint.s…"   About an hour ago   Up About an hour   33060/tcp, 0.0.0.0:33006->3306/tcp   example_mysql

MySQL 연결

gorm 과 MySQL 드라이버를 설치합니다.

$ go get -u gorm.io/gorm
$ go get -u gorm.io/driver/mysql

MySQL을 연결합니다.

 dsn := "test:test@tcp(127.0.0.1:33006)/test?charset=utf8mb4&parseTime=True&loc=Local"

 // refer https://github.com/go-sql-driver/mysql#dsn-data-source-name for details
 db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
 if err != nil {
  log.Fatalf("failed to connect database: %v", err)
 }

GraphQL 스키마로 table 을 생성합니다.

 if err = db.AutoMigrate(&model.Post{}, &model.Comment{}); err != nil {
  log.Fatalf("failed to auto migration schema: %v", err)
 }

핸들러 코드 수정

  • server.go
package main

import (
 "github.com/99designs/gqlgen/graphql/handler/transport"
 "github.com/99designs/gqlgen/graphql/playground"
 "github.com/gorilla/websocket"
 "github.com/songtomtom/gqlgen-apollo-subscriptions/graph/model"
 "gorm.io/driver/mysql"
 "gorm.io/gorm"
 "log"
 "net/http"
 "os"

 "github.com/99designs/gqlgen/graphql/handler"
 "github.com/songtomtom/gqlgen-apollo-subscriptions/graph"
)

const defaultPort = "8080"

func main() {
 port := os.Getenv("PORT")
 if port == "" {
  port = defaultPort
 }

 dsn := "test:test@tcp(127.0.0.1:33006)/test?charset=utf8mb4&parseTime=True&loc=Local"

 // refer https://github.com/go-sql-driver/mysql#dsn-data-source-name for details
 db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
 if err != nil {
  log.Fatalf("failed to connect database: %v", err)
 }

 if err = db.AutoMigrate(&model.Post{}, &model.Comment{}); err != nil {
  log.Fatalf("failed to auto migration schema: %v", err)
 }

 http.Handle("/", playground.Handler("GraphQL playground", "/query"))
 http.HandleFunc("/query", query(db))
 http.HandleFunc("/subscriptions", subscription(db))

 log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
 log.Fatal(http.ListenAndServe(":"+port, nil))
}

func query(db *gorm.DB) http.HandlerFunc {
 return func(w http.ResponseWriter, r *http.Request) {
  srv := handler.NewDefaultServer(graph.NewExecutableSchema(graph.Config{Resolvers: &graph.Resolver{DB: db}}))
  srv.ServeHTTP(w, r)
 }

}

func subscription(db *gorm.DB) http.HandlerFunc {
 return func(w http.ResponseWriter, r *http.Request) {
  srv := handler.New(graph.NewExecutableSchema(graph.Config{Resolvers: &graph.Resolver{DB: db}}))
  srv.AddTransport( // <---- This is the important part!
   &transport.Websocket{
    Upgrader: websocket.Upgrader{
     CheckOrigin: func(r *http.Request) bool {
      return true
     },
     ReadBufferSize:  1024,
     WriteBufferSize: 1024,
    },
   },
  )
  srv.ServeHTTP(w, r)
 }
}

리졸버 구현

  • schema.resolvers.go
// CreatePost is the resolver for the createPost field.
func (r *mutationResolver) CreatePost(ctx context.Context) (*model.Post, error) {
 _, cancel := context.WithTimeout(ctx, 3*time.Second)
 defer cancel()

 post := model.Post{
  ID: uuid.UUIDv4(),
 }
 r.DB.Create(post)

 return &post, nil
}

// CreateComment is the resolver for the createComment field.
func (r *mutationResolver) CreateComment(ctx context.Context, input model.CreateCommentInput) (*model.Comment, error) {
 _, cancel := context.WithTimeout(ctx, 3*time.Second)
 defer cancel()

 comment := model.Comment{
  ID:      uuid.UUIDv4(),
  PostID:  input.PostID,
  Content: input.Content,
 }
 r.DB.Create(comment)

 return &comment, nil
}

// Comments is the resolver for the comments field.
func (r *queryResolver) Comments(ctx context.Context, where model.CommentsWhere) ([]*model.Comment, error) {
 _, cancel := context.WithTimeout(ctx, 3*time.Second)
 defer cancel()

 var comments []*model.Comment
 r.DB.Where("post_id = ?", where.PostID).Find(&comments)

 return comments, nil
}

// CommentAdded is the resolver for the commentAdded field.
func (r *subscriptionResolver) CommentAdded(ctx context.Context) (<-chan *model.Comment, error) {
 // TODO
 panic(fmt.Errorf("not implemented: CommentAdded - commentAdded"))
}

CommentAdded 리졸버는 다음 포스트에서 구현합니다.

GraphQL 테스트

$ go run server.go
connect to http://localhost:8080/ for GraphQL playground

Post Mutation 테스트

mutation CreatePost {
  createPost(input: {id: "test_post_id"}) {
    id
  }
}
{
  "data": {
    "createPost": {
      "id": "test_post_id"
    }
  }
}

Comment Mutation 테스트

mutation CreateComment {
  createComment(input: {postId: "test_post_id", content: "test_hello~"}) {
    id
    postId
    content
  }
}
{
  "data": {
    "createPost": {
      "id": "test_post_id"
    }
  }
}

Comments Query 테스트

query ListComments {
  comments(where: {postId: "test_post_id"}) {
    id
    postId
    content
  }
}
{
  "data": {
    "comments": [
      {
        "id": "0b501dff-7d61-4c4a-8f20-a75cfda64461",
        "postId": "test_post_id",
        "content": "test_hello~"
      },
      {
        "id": "2fec1731-b1a1-441d-97e6-a26c1b15145a",
        "postId": "test_post_id",
        "content": "test_hello~"
      },
      {
        "id": "6c8bd39b-a3a5-416a-b3a7-afbad931c1c2",
        "postId": "test_post_id",
        "content": "test_hello~"
      },
      {
        "id": "dade23e2-973f-4069-bb2e-d8c9d3b7a3b2",
        "postId": "test_post_id",
        "content": "test_hello~"
      }
    ]
  }
}

Reference

Github

0개의 댓글