본문 바로가기
Back-end/Python

[GraphQL] 무작정 시작하기 (1) - Schema & Query

by 허도치 2020. 4. 13.
0. 서론

 최근 Javscript로 2048 웹게임을 만들고 있는데, 게임 결과를 저장할 수 있게 Database(MongoDB)를 연동하였다. 결과 조회는 평소처럼 클라이언트에서 목록을 요청하면, 서버에서는 미리 작성된 쿼리를 통해 DB를 탐색하고 결과를 반환하는 방식을 사용하였고 이를 REST API로 구현하였다.

 처음에 설계한대로 '이름, 점수, 입력시간'만 조회하면 문제가 없었다. 그런데, 게임의 완성도를 높이기위해 게임 모드를 추가하게 되면서 한가지 불편한 점을 느끼게 되었다. 화면에서 '게임모드'의 값을 추가로 가지고 오기위해는 서버에 미리 작성된 쿼리도 수정을 해야하는 번거로움이 있던 것이다. 이러한 불편함을 해소할 방법을 찾던 중 'GraphQL'에 대해서 알게 되었다.

 GraphQL은 페이스북에서 만든 데이터 질의어이며, 'gql'이라고 한다. gql은 서버에 작성된 쿼리를 통해 데이터를 조회하는 방식이 아니라, 클라이언트에서 쿼리를 작성하여 필요한 데이터만 조회하는 방식을 제공한다. 또한, 하나의 EndPoint를 가지기 때문에 개발 규모에 따라 EndPoint의 복잡도가 증가하는 REST API보다 개발이 간편하다. 여러 데이터 집합에서 데이터를 조회하는 경우 gql은 하나의 쿼리로 조회가 가능하지만, REST API는 Request를 여러번 시도해야 한다. REST API도 한번의 Request로 처리가 가능하지만, 매번 여러개의 데이터 집합을 조회하기 때문에 자원의 낭비를 초래한다.

 GraphQL데이터의 구조를 정의하는 스키마(Schema)와 데이터 조회를 위한 쿼리(Query), 데이터 위한 뮤테이션(Mutation), 조회 결과에 대한 구현을 위한 리졸버(Resolver)로 구성되며, 이 외에 API명세서의 기능을 하는 인스로펙션(Instropection)으로 구성된다.

 추가로 페이스북에서 만든 React 프레임워크에 적용하게되면 비동기로 데이터를 조회하기 위해 Redux와 Sagas 등을 대체할 수 있고, apollo를 통해 보다 쉽고 간단하게 개발할 수 있다.

 그래서, 이번 포스트에서는 Python에서 GraphQL 서버를 만드는 방법에 대해서 알아보도록 하겠다. WebApp은 Flask, Database는 MongoDB를 사용한다.

 

 

 

1. 프로젝트 준비
1-1. 프로젝트 구조 

1) [ app.py ]
   : Flask Web Application 실행 파일.

2) [ api/__init__.py ]
   : Flask Web Application 설정 파일.

3) [ api/database.py ]
   : MongoDB 연결 및 기초 데이터 셋팅 파일.

4) [ api/models.py ]
   : MongoDB의 Document들을 GraphQL에서 사용하기위한 객체로 만드는 파일.

5) [ api/mutaions.py ]
   : GraphQL에서 'insert, update, delete' 등의 작업(Mutation)을 처리하는 파일.

6) [ api/schema.py ]
   : GraphQL에서 MongoDB와 연동된 일종의 테이블(Schema)를 정의하는 파일.

 

1-2. 패키지 설치

$ pip install python-dotenv flask flask-graphql graphene graphene-mongo mongoengine mongomock

1) [ python-dotenv ]
   : 파일에 작성된 환경변수를 시스템에 설정하는 패키지.

2) [ flask ]
   : Flask Web Application 기본 패키지.

3) [ flask-graphql ]
   : Flask에서 GraphQL을 연동하기 위한 패키지.

4) [ graphene ]
   : GraphQL 기본 패키지.

5) [ graphene-mongo ]
   : GRaphQL에서 MongoDB를 연동하기 위한 패키지.
   : 내부적으로 mongoengine이 사용됨.

6) [ mongoengine ]
   : MongoDB를 조작하기 위한 패키지.
   : 내부적으로 pymongo를 사용하며 손쉬운 사용을 제공함.

7) [ mongomock ]
   : 로컬에서 MongoDB 설치없이 사용하기위한 패키지.


 

 

 

2. GraphQL 소스 작성
2-1. api/database.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# api/datdabase.py
from mongoengine import connect
from api.models import RankModel
 
MONGO_DTATBASE="graphql-example"
MONGO_HOST="mongomock://localhost"
 
# Database 연결
conn = connect(MONGO_DTATBASE, host=MONGO_HOST, alias="default")
 
# 기초 데이터 Insert 함수
def init_db():
  rank = RankModel(name="kim", mode="3x3", score=2, isMobile=False, reg_dttm="20200413170848")
  rank.save() # Insert
 
  rank = RankModel(name="choi", mode="3x3", score=128, isMobile=False, reg_dttm="20200413170848")
  rank.save() # Insert
 
  rank = RankModel(name="lee", mode="4x4", score=1024, isMobile=False, reg_dttm="20200413170848")
  rank.save() # Insert
  
  rank = RankModel(name="heo", mode="4x4", score=16, isMobile=False, reg_dttm="20200413170848")
  rank.save() # Insert
 
cs

1) [ 5~6 ln ] - 로컬 MongoDB 설정.
   : 나중에 실제 MongoDB의 주소와 Database를 입력해주면 됨.

2) [ 9 ln ] - MongoDB 연결.

3) [ 13~13 ln ] - 기초 데이터 입력.
   : 임의의 기초 데이터를 입력.
   : save() 함수는 mongoengine에서 제공하는 insert 기능.

 

2-2. api/models.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# api/models.py
import datetime
 
from mongoengine import Document
from mongoengine.fields import StringField, BooleanField, IntField
 
from graphene_mongo import MongoengineObjectType
 
# MongoDB Model
class RankModel(Document):
  meta = {'collection''ranking'}
  mode = StringField()
  name = StringField()
  score = IntField()
  is_mobile = BooleanField()
 reg_dttm = StringField()
  upd_dttm = StringField()
  
 
# Schema Type
class RankType(MongoengineObjectType):
  class Meta:
    model = RankModel
  
  # reg_dttm을 출력할 때 실행되는 로직. ( 날짜형식 변경 )
  def resolve_reg_dttm(parent, info, **kwargs):
    return datetime.datetime.strptime(parent.reg_dttm, "%Y%m%d%H%M%S").strftime("%Y-%m-%d %H:%M:%S")
 
  # upd_dttm을 출력할 때 실행되는 로직. ( 날짜형식 변경 )
  def resolve_upd_dttm(parent, info, **kwargs):
    return datetime.datetime.strptime(parent.upd_dttm, "%Y%m%d%H%M%S").strftime("%Y-%m-%d %H:%M:%S")
    
cs

1) [ 10~17 ln ] - Document 객체 생성.
   : Document는 MongoDB에서 RDBMS의 database 역할.
   : Collection은 MongoDB에서 RDBMS의 Table의 역할.
   : 즉, RankModel은 'graphql-example' database에 있는 'ranking' Table에 대한 객체.

2) [ 21~31 ln ] - Graphql 객체 생성.
   : [ 22~23 ln ] - Graphql Model로 Document 객체를 설정.
   : [ 26~27, 30~31 ln ] - 'resolve_필드명' 함수는 해당 필드가 출력될 때, 처리되는 로직.

 

2-3. api/schema.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# api/schema.py
import graphene
 
from api.models import RankModel, RankType
 
# Query 설정
class Query(graphene.ObjectType):  
  # 모든 랭킹 목록.
  ranks = graphene.List(RankType)
  
  # 특정 모드에 대한 랭킹 목록.
  ranks_for_mode = graphene.List(RankType, mode=graphene.String(required=True))
  
  # 특정 랭킹에 대한 정보.
  rank = graphene.Field(RankType, id=graphene.String(required=True))
 
  # MongoDB에서 모든 랭킹 목록을 조회
  def resolve_ranks(parent, info):
    return RankModel.objects.all()
    
  # MongoDB에서 특정 모드의 모든 랭킹 목록을 조회
  def resolve_ranks_for_mode(parent, info, mode):
    return RankModel.objects(mode=mode).all()
 
  # MongoDB에서 특정 랭킹을 조회.
  def resolve_rank(parent, info, id):
    return RankModel.objects.get(id=id)
 
# Schema 생성
schema = graphene.Schema(
  query=Query,
  types=[
    RankType
  ]
)
cs

1) [ 4 ln ] - 앞서 작성한 Model과 ObjectType을 가져오기.
   : Model은 실제 MongoDB를 조작할 때 사용.
   : ObjectType은 반환값에 대한 타입을 지정할 때 사용.

2) [ 7~27 ln ] - Query 설정.
   : Query는 데이터를 조회하는데 사용되며, 각 필드는 데이터의 집합임.
   : [ 9~15 ln ] - 데이터의 집합을 정의.
     : graphene.Field 한 개의 결과를 반환.
     : graphene.List는 여러개의 결과를 반환.
     : 각 함수의 첫번째 인자는 결과값에 대한 ObjectType임.
     : 각 인자는 Resolver로 전달됨.

3) [ 18~27 ln ] - 각 Field에 대한 Resolver 설정.
   : Resolver는 조회 결과에 대한 상세 구현함.

4) [ 30~35 ln ] - Schema 생성.
   : Schema는 GraphQL의 전체 데이터 구조를 의미함.
   : ObjectType, Query, Mutation 등을 포괄함.

 

 

 

3. Flask 작성
3-1. api/__init__.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# api/__init__.py
from flask import Flask
from flask_graphql import GraphQLView
from api.schema import schema
 
def create_app():
  # Flask Application 생성
  app = Flask(__name__)
  
  # /graphql EndPoint 설정
  app.add_url_rule(
    '/graphql',
    view_func=GraphQLView.as_view(
      'graphql',
      schema=schema,
      graphiql=True   # gql 테스트 페이지 제공
    )
  )
  
  return app
  
cs

1) [ 3 ln ] - Flask에서 GraphQL을 제공하기위한 GraphQLView 모듈

2) [ 4 ln ] - 앞서 작성한 GraphQL Schema.

3) [ 8 ln ] - Flask Application 생성.

4) [ 11~18 ln ] - GraphQL을 위한 EndPoint 설정.
   : [ 16 ln ] - True로 설정시 gql을 테스트 할 수 있는 페이지를 제공함.


 

3-2. app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# app.py
import dotenv
from api import create_app
from api.database import init_db
 
 
if __name__ == '__main__':
  # 환경변수 설정
  dotenv.load_dotenv(dotenv_path=".env")
  
  # MongoDB 접속 및 기초 데이터 입력
  init_db()
  
  # Flask App 실행
  app = create_app()
  app.run(host="localhost", port=3000)
  
cs

1) [ 9 ln ] - 환경변수 설정 파일.

2) [ 12 ln ] - MongoDB 접속 및 기초 데이터 입력.

3) [ 15~16 ln ] - Flask App 실행.
   : http://localhost:3000/graphql 로 접속.

 

3-3. .env
1
2
3
# .env                                                                                    
FLASK_ENV=devlopment
FLASK_DEBUG=true
cs

1) [ 2 ln ] - Flask App을 개발(devlopment) 모드로 실행.
   : 배포시 production 또는 생략.
   : devlopment로 실행시 소스가 수정되면 App이 자동으로 재실행됨.

2) [ 3 ln ] - Flask App을 Debug 모드로 실행.
   : 로그가 Debug Level로 출력됨.

 

 

 

4. Flask App 실행
4-1. 실행

$ python app.py

 

4-2. 실행 결과

 

4-3. GraphQL View 실행

1) 브라우저 실행 후 http://localhost:3000/graphql 로 접속.

2) 오른쪽 상단에 Docs를 클릭하면 Schema의 정보를 확인 할 수 있음.
   : Query가 등록된 것이 확인됨.

 

 

 

5. Query 테스트.
5-1. 전체 랭킹 조회

1
2
3
4
5
6
7
8
9
10
# 전체 랭킹 조회                                                        
query {
  ranks {
    mode
    name
    score
    isMobile
    regDttm
  }
}
cs

1) [ 2 ln ] - 데이터 조회를 위한 query 시작.

2) [ 3 ln ] - 데이터 집합 설정.

3) [ 4~8 ln ] - 데이터 집합에서 가지고 올 필드 설정.
   : regDttm은 RankType에서 구현한 Resolver를 통해 데이터의 형식이 변경되었음.
   : 각 필드는 카멜케이스(Camel Case)로 표기함.

 

5-2. 특정 모드의 랭킹 조회

1
2
3
4
5
6
7
8
9
10
11
# 4x4 모드의 랭킹 조회                                                        
query {
  ranksForMode(mode: "4x4") {
    id
    mode
    name
    score
    isMobile
    regDttm
  }
}
cs

1) [ 2 ln ] - 데이터 조회를 위한 query 시작.

2) [ 3 ln ] - 데이터 집합 설정.
   : 괄호 안에 조건을 입력.

3) [ 4~9 ln ] - 데이터 집합에서 가지고 올 필드 설정.
   : 각 필드는 카멜케이스(Camel Case)로 표기함.

 

5-3. 특정 랭킹 조회

1
2
3
4
5
6
7
8
9
10
11
# 특정 랭킹의 정보 조회                                                             
query {
  rank(id: "조회된ID입력") {
    id
    mode
    name
    score
    isMobile
    regDttm
  }
}
cs

1) [ 2 ln ] - 데이터 조회를 위한 query 시작.

2) [ 3 ln ] - 데이터 집합 설정.
   : 괄호 안에 조건을 입력.
   : id는 [ 5-2 ]에서 조회된 값을 입력.

3) [ 4~9 ln ] - 데이터 집합에서 가지고 올 필드 설정.
   : 각 필드는 카멜케이스(Camel Case)로 표기함.

 

 

 

마치며.

- 2048게임의 랭킹을 구현하는게 번거로워서 시작한 GraphQL인데, 오히려 새로 배워야하는게 많아서 더 번거로웠다. 하지만, 사용해보고나서 매우 만족하고 있다. 이제 3일정도 사용해봤기 때문에 이렇다할 단점을 찾지는 못하였다.
- 지금도 내용이 생각보다 길어졌는데 뮤테이션(Mutation)까지 다루면 끝이 없을것 같아서 우선 생략했다. Mutation은 다음 포스트에서 다루도록 하겠다.

 

댓글