[FastAPI] 3. SQLAlchemy + Graphene ์กฐํฉ์ผ๋ก GraphQL ์๋ฒ ๋ง๋ค๊ธฐ
์ด๋ฒ ํฌ์คํธ์์๋ GraphQL์ ๋ํ ์ด์ผ๊ธฐ๋ฅผ ํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
What is GraphQL ?
GraphQL์ ๋ํ ์ด์ผ๊ธฐ๋ ๊ทธ๋ฆฌ ๊ธธ์ง ์๊ธฐ ๋๋ฌธ์ ์ฌ๊ธฐ์ ๊ฐ๋จํ๊ฒ ๋ค๋ค๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
GraphQL์ Facebook์์ ๊ฐ๋ฐํ ๋ฐ์ดํฐ ํํ ์ธ์ด๋ก ์ฌ๊ธฐ์ QL์ด Query Language์ ํด๋นํฉ๋๋ค. ์ฐ๋ฆฌ๋ ์๋ฒ์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๊ธฐ ์ํด REST API๋ฅผ ์ฌ์ฉํ์๊ณ , REST API๋ ๋ฉ์๋์ ์์ฒญ ๋ฐ์ดํฐ ํน์ ์๋ต ๋ฐ์ดํฐ๋ก ๊ตฌ๋ถํ์ฌ ์ํ๋ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๊ฑฐ๋ ๋ฐ์ดํฐ๋ฅผ ์ ์ฌํ๋ ๋ฑ์ ์ํํ์์ต๋๋ค.
๊ทธ๋ฌ๋ GraphQL์ ๋ฉ์๋ ์์ด ์ค์ง Query Language๋ง์ ์ด์ฉํ์ฌ ์ํ๋ ๋ชจ๋ธ์ ์ ์ฌํ๊ณ , ๊ฐ์ ธ์ฌ ์ ์์ต๋๋ค. ๋ํ ์ํ๋ ํญ๋ชฉ์ ํด๋ผ์ด์ธํธ๊ฐ ์ง์ ๋ช ์ํจ์ผ๋ก์จ ์๋ฒ์์ ์ ๊ณตํด์ฃผ๋ ๋ฐ์ดํฐ๋ง์ ์์กดํ์ง ์๊ณ ์ํ๋ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ฌ ์ ์๋ ์ฅ์ ์ด ์๊ธฐ๋ฉฐ ์ด ์ฅ์ ์ ์ฐจํ ๋คํธ์ํฌ ๋์ญํญ์ ์ ์ฝ์ํค๋ ๋ฐ ๋์์ ์ฃผ๊ธฐ๋ ํฉ๋๋ค.
์ด ์ธ์๋ ๋ค์ํ ์ฅ์ ์ด ์กด์ฌํฉ๋๋ค.
- UnderFetching
HTTP ์์ฒญ 1๋ฒ์ ์ํ๋ ๋ฐ์ดํฐ๋ฅผ ๋ชจ๋ ๊ฐ์ ธ์ฌ ์ ์์. - OverFetching
ํด๋ผ์ด์ธํธ๊ฐ ์ํ๋ ํญ๋ชฉ๋ง ๋ช ์ํ๋ฉด ํด๋น ๋ฐ์ดํฐ๋ง์ ๋ฐํ (๋์ญํญ ์ ์ฝ, ๊ทธ๋ฌ๋ ์ฝ๊ฐ์ ์ค๋ฒํค๋ ๋ฐ์) - Type Specification
์์ฒญ๊ณผ ์๋ต์ ๋ํด ์๋ฃํ์ ์ง์ ํ๋ ๊ฒ์ ํ์๋ก ํ๋ฉฐ ์ด๋ก ์ธํ ์ฒ๋ฆฌ๋ฅผ ์ ์ฐํ๊ฒ ํ ์ ์์. - Cross Platform
Web, Mobile์ ๊ตฌ๋ถํ์ง ์๊ณ , ํ๋์ API๋ก ๋ชจ๋ ์์ ์ ์ฒ๋ฆฌํจ.
ํ์ ์ ๋ช ์ํ๋ ๊ฒ์ ๋ํด์๋ ์กฐ๊ธ ์์ํ ์ ์๊ฒ ์ง๋ง Python์์๋ ๊ธฐ๋ณธ์ ์ผ๋ก DTO๋ฅผ ๊ฐ์ถ์ง ์์๋ ๋ฉ๋๋ค. ๊ทธ๋ฌ๋ DTO๋ฅผ ๊ฐ์ถ์ง ์์ผ๋ฉด ๊ฐ API๋ณ๋ก ์ด๋ป๊ฒ ์์ฒญ์ ํด์ผํ ์ง, ์๋ต์ ์ฃผ๋์ง๊ฐ ๋ช ์๋์ด ์์ง ์์ ์ฌ์ฉํ๋ ์ฌ๋ ์ ์ฅ์์๋ ์คํ๋ ค ํผ๋์ ์ผ๊ธฐํ ์๋ ์๋ ๋ถ๋ถ์ ๋๋ค.
๊ทธ๋ฌ๋ GraphQL์์๋ ์ด๋ฌํ ํ์ ์ ์๊ฐ ๊ธฐ๋ณธ์ด๋ฉฐ ํ๋ผ๋ฏธํฐ ๊ฐ์ ์๋ฃํ์ด ๋ค๋ฅผ ๊ฒฝ์ฐ 422 ์ค๋ฅ๋ฅผ ๋ฐํํฉ๋๋ค.
Graphene
Graphene๋ Python์์ GraphQL์ ์ฌ์ฉํ๊ธฐ ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋๋ค. ์๋ง Node.js์์ GraphQL์ ์จ๋ณด์ ๋ถ๋ค์ Apollo ๋ผ๋ ๊ฒ์ ๋ค์ด๋ณด์ จ์ ๊ฒ์ ๋๋ค. Python์์ ์ด์ ๋น์ทํ Ariadne๋ ์คํค๋ง ์ฐ์ ์ ๊ทผ ๋ฐฉ์์ ์ฌ์ฉํจ์ผ๋ก์จ Python ์ฝ๋๊ฐ ์๋ SDL(Schema Definition Language)์ ์์กดํ์ฌ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ์ผ๋ฐ์ ์ผ๋ก๋ ์ ๊ทผํ๊ธฐ ์ด๋ ค์ด ๋ฐฉ์์ ๋๋ค.
๊ทธ๋ฌ๋ Graphene๋ ํ๋ก๊ทธ๋๋ฐ ์ฝ๋ ์ฐ์ ์ ๊ทผ ๋ฐฉ์์ ์ฌ์ฉํจ์ผ๋ก์จ GraphQL์ ์ฒ์ ์ฌ์ฉํด๋ณด์๋ ๋ถ๋ค์๊ฒ ์ ๋ฆฌํฉ๋๋ค. Schema์ ๋ํ ๊ฐ๋ ์ด๋ ์ฝ๋๋ฅผ ์ ๋ชฐ๋ผ๋ Python ์ฝ๋๋ง ์งค ์ค ์๋ค๋ฉด GraphQL์ ์ฌ์ฉํ ์ ์๊ณ , ๊ฑฐ๊ธฐ์ SQLAlchemy๋ผ๋ ORM๊ณผ๋ ์ฝ๊ฒ ํตํฉํ ์ ์์ด, ๋งค์ฐ ๋น ๋ฅธ ๊ฐ๋ฐ์ ์ฉ์ดํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋๋ค.
Graphene๋ ๋ณด๋ค์ํผ ๋จ๋ ์ผ๋ก ์ฌ์ฉํ ์๋ ์๊ณ , Flask์ ํตํฉํ๊ฑฐ๋, SQLAlchemy์ ํตํฉํ๋ ๋ฑ ๋ค์ํ ๋ฐฉ๋ฒ์ผ๋ก ์ฌ์ฉํ ์ ์์ต๋๋ค.
์ด ์ค์์๋ ์ด ํฌ์คํธ์์๋ SQLAlchemy์ ํตํฉํ์ฌ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ ๋ํด ์์๋ณด๊ฒ ์ต๋๋ค.
SQLAlchemy + Graphene
์ง๋ ํฌ์คํธ์์ ์ฌ์ฉํ๋ ํ๋ก์ ํธ์ ์ฝ๋๋ฅผ ๊ทธ๋๋ก ์ฌ์ฉํ์ฌ ์๋์ฒ๋ผ graphene-sqlalchemy ๋ํ๋์๋ง ์ถ๊ฐํด์ฃผ๋๋ก ํ๊ฒ ์ต๋๋ค.
[tool.poetry]
name = "fastapiexample"
version = "0.1.0"
description = ""
authors = ["Neon K.I.D <contact@neonkid.xyz>"]
[tool.poetry.dependencies]
python = "^3.8.5"
fastapi = "^0.63.0"
uvicorn = "^0.13.2"
SQLAlchemy = "^1.3.22"
psycopg2-binary = "^2.8.6"
graphene-sqlalchemy = "^2.3.0"
[tool.poetry.dev-dependencies]
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
๊ธฐ์กด์ ์ฝ๋๋ฅผ ์ด์ด์ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ REST API ์ฝ๋์ ๊ฒน์น ๊ฒ์ ๋๋ค. ํ์ง๋ง ์๊ด์์ต๋๋ค. ์ด ํฌ์คํธ๋ฅผ ๊ทธ๋๋ก ๋ฐ๋ผํ๋ค๋ฉด GraphQL๊ณผ REST API๋ฅผ ๋์์ ์ฌ์ฉํ ์๋ ์์ต๋๋ค.
์ฐ๋ฆฌ๋ ์ง๋ ์๊ฐ์ ๊ฐ๋จํ ๋ฉ๋ชจ๋ฅผ ํ ์ ์๋ DB ์คํค๋ง๋ฅผ ๋ง๋ค์๊ณ , ์ฌ๊ธฐ์ ๋ฉ๋ชจ์ ์ ๋ชฉ๊ณผ ์ปจํ ์ธ , ์ฆ๊ฒจ์ฐพ๊ธฐ ์ฌ๋ถ ๋ฑ์ ํ์ธํ ์ ์๋๋ก ํ์์ต๋๋ค.
์ด๋ฅผ ํ ๋๋ก GraphQL ๊ธฐ๋ฐ์ API๋ฅผ ๋ง๋ค์ด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
Resolver
ObjectType์ graphene์์ GraphQL์ ๊ตฌํํ๊ธฐ ์ํ ๊ธฐ๋ณธ์ ์ธ Query ํด๋์ค๋ก GraphQL API๋ฅผ ๋ง๋ค๊ธฐ ์ํด ๋ฐ๋์ ๋ง๋ค์ด์ผ ํ ํ์ ์์์ ๋๋ค.
from graphene import ObjectType
class Query(ObjectType):
relay๋ฅผ ์ฌ์ฉํ์ฌ DB ์๋ฒ์ ์ฐ๊ฒฐํ ์ ์์ต๋๋ค.
from graphene import ObjectType, relay
class Query(ObjectType):
node = relay.Node.Field()
๋จผ์ Query์ node๋ผ๋ ๋ณ์๋ฅผ ๋ด๊ณ , relay ํ๋์์ ๋ช ์ํฉ๋๋ค. ์ด๋ ๊ฒ ํ๋ฉด ์์ผ๋ก GraphQL์์ node ๋ฐ์ผ๋ก DB์์ ๋ถ๋ฌ์จ ๋ฐ์ดํฐ๊ฐ ๋ค์ด์ค๊ฒ ๋ฉ๋๋ค.
๊ทธ๋ฆฌ๊ณ ์ด DB์ ์ฐ๊ฒฐํ ์ ์๋ ์ปค๋ฅ์ ์ ํ๋ ๋ง๋ค์ด์ค๋๋ค.
from graphene import relay
from graphene_sqlalchemy import SQLAlchemyObjectType
class MemoModel(SQLAlchemyObjectType):
class Meta:
model = Memo
interfaces = (relay.Node,)
class MemoConnection(relay.Connection):
class Meta:
node = MemoModel
relay์ ์๋ Connection์ ์์๋ฐ๊ณ , ์ด์ ํด๋นํ๋ ๋ชจ๋ธ์ node์ ๋ฃ์ด์ค๋๋ค.
from graphene import ObjectType, relay, String
from graphene_sqlalchemy import SQLAlchemyObjectType, SQLAlchemyConnectionField
class Query(ObjectType):
node = relay.Node.Field()
memo = SQLAlchemyConnectionField(MemoConnection, id=String())
memo_list = SQLAlchemyConnectionField(MemoConnection, sort=MemoModel.sort_argument())
def resolve_memo(self, info, **kwargs):
id = kwargs.get('id')
memos_query = MemoModel.get_query(info)
if id is not None:
return memos_query.filter_by(id=id)
def resolve_memo_list(self, info, **kwargs):
return MemoModel.get_query(info).all()
๋ง์ง๋ง์ผ๋ก ์๊น ์์์ ๋ง๋ Query์ memo์ memo_list์ ๋ํ ๋ด์ฉ์ ๋ฃ์ด์ค๋๋ค. memo๋ ๋จ์ผ ๋ฉ๋ชจ๋ฅผ ๊ฐ์ ธ์ค๊ธฐ ์ํ ํ๋ผ๋ฏธํฐ์ด๊ณ , memo_list๋ ๋ณต์ ๊ฐ์ ๋ฉ๋ชจ๋ฅผ ๊ฐ์ ธ์ค๊ธฐ ์ํ ํ๋ผ๋ฏธํฐ์ ๋๋ค. ์ฐ๋ฆฐ ID๋ก ์กฐํ๋ฅผ ํ๋ REST API๋ฅผ ๋ง๋ค์๊ธฐ ๋๋ฌธ์ id ๊ฒ์์ ์ํ id ํ๋ผ๋ฏธํฐ ๊ฐ์ ๋ง๋ค์ด์ค๋๋ค.
๊ทธ๋ฆฌ๊ณ ์ ์ฒด ๋ฉ๋ชจ ์กฐํ์ ์ ๋ ฌ ์๊ณ ๋ฆฌ์ฆ์ ์ฌ์ฉํ๊ธฐ ์ํด memo_list์๋ sort ์๊ท๋จผํธ๋ฅผ ํ๋ ์ถ๊ฐํด์ค๋๋ค.
from graphene import Schema
from starlette.graphql import GraphQLApp
app.add_route('/graphql', GraphQLApp(schema=Schema(query=Query)))
์ด์ /graphql ์ด๋ผ๋ ์๋ํฌ์ธํธ์ GraphQL API๋ฅผ ๋ฌถ์ด์ฃผ๋ฉด graphql ์๋ํฌ์ธํธ ํ๋๋ก ๋ฐ์ดํฐ๋ฅผ READ ํ ์ ์์ต๋๋ค.
Mutation
Mutation์ GraphQL์์ Collection์ ์๋ก ๋ง๋ค๊ณ , ์์ ํ ๋ ์ฌ์ฉํ๋ ObjectType์ ๋๋ค. graphene์์๋ GraphQL๋ก ๋ฐ์ดํฐ๋ฅผ INSERT, UPDATEํ ๋ ์ด๋ฅผ ์ฌ์ฉํฉ๋๋ค.
from graphene import String, Mutation, Boolean
class InsertMemo(Mutation):
class Arguments:
title = String(required=True)
content = String()
is_favorite = Boolean()
memo = Field(lambda: MemoModel)
def mutate(self, info, **kwargs):
memo = Memo(**kwargs)
db_session.add(memo)
db_session.commit()
return InsertMemo(memo)
๋จผ์ ๊ธฐ๋ณธ์ ์ผ๋ก ๋ฉ๋ชจ๋ฅผ ์ฝ์ ํ ๋๋ Mutation์ ์์ ๋ฐ๊ณ , ๊ทธ์ ์ฌ์ฉํ Argument๋ฅผ ์ ์ํด์ค๋๋ค. ํ์ ์ graphene ๋ผ์ด๋ธ๋ฌ๋ฆฌ์์ ์ ๊ณต๋๋ ๊ฒ์ผ๋ก ์ฌ์ฉํฉ๋๋ค.
๊ทธ๋ฆฌ๊ณ SQLAlchemy์ ์ธ์ ์ ์ด์ฉํ์ฌ DB์ ์์ํ๋๋ก ํฉ๋๋ค.
from graphene import String, Mutation, Boolean, Field
class UpdateMemo(Mutation):
class Arguments:
id = String(required=True)
title = String()
content = String()
is_favorite = Boolean()
memo = Field(lambda: MemoModel)
def mutate(self, info, **kwargs):
memo = db_session.query(Memo).filter_by(id=kwargs.get('id')).first()
for key, value in kwargs.items():
setattr(memo, key, value)
db_session.commit()
return UpdateMemo(memo)
๋น์ทํ ๋ฐฉ๋ฒ์ผ๋ก ๋ฉ๋ชจ๋ฅผ ์ ๋ฐ์ดํธ ํ ๋ ์์ ๊ฐ์ด ๋ง๋ค์ด์ค๋๋ค.
from graphene import String, Mutation, Boolean
class DeleteMemo(Mutation):
class Arguments:
id = String(required=True)
def mutate(self, info, id):
memo = db_session.query(Memo).filter_by(id=id)
db_session.delete(memo)
return ""
์ญ์ ๋ฅผ ํ ๋๋ ๋์ผํ๊ฒ ๊ฐ๋ฐํ๋ฉด ๋ฉ๋๋ค.
from graphene import ObjectType
class Mutation(ObjectType):
create_memo = InsertMemo.Field()
update_memo = UpdateMemo.Field()
delete_memo = DeleteMemo.Field()
GraphQL App ํด๋์ค์ ๋ด์ Mutation ObjectType์ ํ๋ ๋ง๋ค์ด์ฃผ๊ณ , ์๊ท๋จผํธ์ ๊ฐ๊ฐ ๋ง๋ค์ด์ค API๋ฅผ ๋ฃ์ต๋๋ค.
from graphene import Schema
from starlette.graphql import GraphQLApp
app.add_route('/graphql', GraphQLApp(schema=Schema(query=Query, mutation=Mutation, types=[MemoModel])))
๋ง์ง๋ง์ผ๋ก GraphQLApp ํด๋์ค์ Mutation๊ณผ ์ฌ์ฉํ Type์ ๋ฃ์ต๋๋ค. ์ฌ์ฉํ Type์ ๋ฃ์ ๋๋ grpahene ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์๋ ํด๋์ค๋ก ์์ ๋ฐ์ ๊ฒ์ ๋ฃ์ด์ผ ํฉ๋๋ค.
Test
ํ ์คํธ์ ๋ํด์๋ ๊ฐ๋จํ๊ฒ Resolver์ Mutation๋ง์ ๊ฐ์ง๊ณ ์งํํ๋๋ก ํ๊ฒ ์ต๋๋ค. ์ด ์ญ์ Postman์ ์ฌ์ฉํฉ๋๋ค.
mutation {
createMemo(title: "", content: "") {
memo {
id
title
content
}
}
}
Mutation์ ๋ํ ํ ์คํธ์ ๋๋ค. GraphQL์ ์ฌ์ฉํ ๋ mutation์์ ์ ์ธํ๊ณ , ์ฝ๋์์ ์ ์ํ ํจ์๋ฅผ ํธ์ถํฉ๋๋ค. ๊ทธ๋ฆฌ๊ณ ๋ฐํํ ๋ฐ์ดํฐ ํ์ ์ ๋ถ๋ฅธ ํ ๋ฐํํ๊ณ ์ ํ๋ ํ์ ์ ๋ช ์ํ๋ฉด ๋ฉ๋๋ค.
query {
memoList {
edges {
node {
id
title
content
}
}
}
}
๋ฐ๋๋ก DB์ ์ ์ฌ๋ ๋ฐ์ดํฐ๋ฅผ ํธ์ถํ๊ณ ์ ํ ๋๋ query๋ฅผ ์ ์ธํด์ค๋๋ค. ๊ทธ๋ฆฌ๊ณ ๋ฉ๋ชจ ๋ฆฌ์คํธ๋ฅผ ๋ถ๋ฌ์ค๋ memoList๋ฅผ ์ ์ํ๊ณ ๊ทธ์ ํด๋นํ๋ ํ์ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฅด๊ธฐ ์ํด edges์ node๋ฅผ ์ ๋ ฅํ ๋ค์ ์ํ๋ ์๊ท๋จผํธ๋ฅผ ๋ด๋ฆฌ๋ฉด ๋ฉ๋๋ค.
๋ง์น๋ฉฐ...
์ฌ๊ธฐ๊น์ง FastAPI + SQLAlchemy + Grpahene ์กฐํฉ์ผ๋ก ๊ฐ๋จํ GraphQL API๋ฅผ ๋ง๋๋ ๋ฒ๊น์ง ์์๋ดค์ต๋๋ค. GraphQL์ ์์ฃผ ๊ฐ๋จํ๊ฒ ์ฅ๋จ๊ณผ ์ฐจ์ด์ ๋ง ๋ํด์ ์ค๋ช ํ๊ณ ๋์ด๊ฐ๊ธฐ ๋๋ฌธ์ ์ด๋ฉด์ ๋ฐ๋ก ์ฌ์ฉํ๋ ๊ฒ์ ์ด๋ ค์ธ ์๋ ์์ ๊ฒ์ ๋๋ค.
๊ทธ๋ฌ๋ ๋ณด๋ค์ํผ REST API์ ๋นํด API ๊ตฌ์กฐ๊ฐ ๊ฐ๋จํ๊ณ , ํด๋ผ์ด์ธํธ๊ฐ ์ํ๋ ๋ฐ์ดํฐ๋ฅผ ๋ฝ์ ์ ์๋ค๋ ๊ฒ์ ๋์ญํญ์ ์ค์ด๊ณ , ํธ์ถ ํ์๊ฐ ๋ฎ์์ง๋ค๋ ์ ์ ์์ด ๋งค์ฐ ํฐ ์ด๋์ ๋ณผ ์ ์์ต๋๋ค.
FastAPI์์ ๊ธฐ์กด์ ์ฌ์ฉํ๋ REST API ๊ทธ๋๋ก GraphQLApp ์ธ์คํด์ค๋ง์ ๋ง๋ค์ด GraphQL API๋ฅผ ๋ง๋ค ์ ์๊ธฐ ๋๋ฌธ์ ๊ธฐ์กด์ REST API๋ฅผ ๋ง๋ค์ด ์ฌ์ฉํ๋ค๊ฐ GraphQL๋ก ์ ํ์ ์ฝ๊ฒ ํ ์ ์์ต๋๋ค.