저번 시간에는 Flask-RESTX 에 대한 기본적인 사용 법을 알아보고, 이를 이용하여 간단한 API Server를 만들어 보았습니다.
여러분은 당신의 코드가 스파게티 코드가 되는 것을 원치 않을 것 입니다. 그러므로 파일 분리는 우리가 무슨 어플리케이션을 만들던 필수적인 과정입니다.
저번 시간에 구현한 간단한 API 서버가 여러 가지 기능을 동시에 구현 한다고 가정 해 보겠습니다. 간단한 게시판을 위해 API 서버를 만든다고 했을 때, 로그인, 회원 가입, 게시글, 댓글, 대댓글, 사용자 수정 등등... 대충 어림잡아 몇 백줄을 넘길 것 입니다. 코드 하나에 문제가 발생했을 때 대처 하기도 어려울 뿐더러, 가독성도 매우 떨어집니다. 그럼 Flask RESTX 에서는 파일 분리를 어떻게 실시 할까요?
flask-restx
내부의 Api
객체의 add_namespace()
는 flask-restx.Namespace
객체를 특정 경로에 등록 할 수 있게 해줍니다. 그러면 flask-restx.Namespace
는 무엇이냐? 어떻게 보면 Flask
객체에 Blueprint
가 있다면, Api
객체에는 Namespace
가 있는 격입니다. Blueprint
를 모른다고요? 백문이 불여일견, 코드로 보시겠습니다.
namespace = Namespace('hello') # 첫 번째
@namespace.route('/')
class HelloWorld(Resource):
def get(self):
return {"hello" : "world!"}, 201, {"hi":"hello"}
api.add_namespace(namespace, '/hello')
@api.route('/hello') # 두 번째
class HelloWorld(Resource):
def get(self):
return {"hello" : "world!"}, 201, {"hi":"hello"}
그럼 이야기는 간단해 졌습니다! 외부에서 클래스를 구현하고, 이를 import 한 다음 add_resource()
를 통해 클래스를 등록 해 주면 됩니다.
다음은 Flask로 REST API 구현하기 - 1. Flask-RESTX 에서 구현한 Todo API Server 와 같은 기능을 합니다.
app.py
from flask import Flask
from flask_restx import Resource, Api
from todo import Todo
app = Flask(__name__)
api = Api(app)
api.add_namespace(Todo, '/todos')
if __name__ == "__main__":
app.run(debug=True, host='0.0.0.0', port=80)
todo.py
from flask import request
from flask_restx import Resource, Api, Namespace
todos = {}
count = 1
Todo = Namespace('Todo')
@Todo.route('')
class TodoPost(Resource):
def post(self):
global count
global todos
idx = count
count += 1
todos[idx] = request.json.get('data')
return {
'todo_id': idx,
'data': todos[idx]
}
@Todo.route('/<int:todo_id>')
class TodoSimple(Resource):
def get(self, todo_id):
return {
'todo_id': todo_id,
'data': todos[todo_id]
}
def put(self, todo_id):
todos[todo_id] = request.json.get('data')
return {
'todo_id': todo_id,
'data': todos[todo_id]
}
def delete(self, todo_id):
del todos[todo_id]
return {
"delete" : "success"
}
자! 대충 파일 분리도 하고, 유지 보수 문제도 조금은 해결 했습니다. 문제는 무엇이냐면, 우리가 만든 API들을 어떻게 프론트앤드 개발자에게 전달 할 수 있을까요? 정답은 문서화 입니다. 한번, 방금 짠 API 서버를 실행 한 후, 'http://localhost/' 에 들어가 볼까요?
우리가 몰랐던 API 문서
우리는 이런 홈페이지를 만든 기억이 없습니다. 그렇습니다. 이는 flask-RESTX
의 기본 기능으로 제공하는 Swagger 기반의 홈페이지 입니다.
이를 제대로 활용 하면, 죽여주는 API 웹 문서를 만들어 줄 수 있을 것 같습니다. 지금은 API에 대한 설명, 예시, 데이터 타입 아무것도 명시 되어 있지 않는 상황입니다. 우리는 어떻게 하면 좋을까요?
일단 가장 위에 있는 설명부터 만지는 게 좋아 보입니다. 위 설명은 Api
객체의 생성자를 호출 할 때, 해당하는 파라미터로 값을 넣어 주면 수정 가능합니다. 파라미터는 다음과 같습니다.
version
: API Server의 버전을 명시합니다.title
: API Server의 이름을 명시합니다.description
: API Server의 설명을 명시합니다.terms_url
: API Server의 Base Url을 명시합니다.contact
: 제작자 E-Mail 등을 삽입합니다.license
: API Server의 라이센스를 명시 합니다.license_url
: API Server의 라이센스 링크를 명시 합니다.한번 app.py
에 있는 Api
객체의 생성자 파라미터를 수정 해 볼까요?
app.py
...
api = Api(
app,
version='0.1',
title="JustKode's API Server",
description="JustKode's Todo API Server!",
terms_url="/",
contact="justkode@kakao.com",
license="MIT"
)
...
전보다 조금 괜찮아졌어요!
Namespace
객체도 생성자 파라미터를 조정하여, 내용을 수정 할 수 있습니다. 한 번 볼까요?
title
: Namespace의 이름을 명시합니다.description
: Namespace의 설명을 명시합니다.한번 todo.py
에 있는 Namespace
객체의 생성자 파라미터를 수정 해 볼까요?
todo.py
...
Todo = Namespace(
name="Todos",
description="Todo 리스트를 작성하기 위해 사용하는 API.",
)
...
Todos 옆에 설명이 추가 되었어요!
Python의 Comment를 이용하여 Document에 설명을 추가 할 수 있습니다.
todo.py
...
@Todo.route('')
class TodoPost(Resource):
def post(self):
"""Todo 리스트에 할 일을 등록 합니다."""
global count
global todos
idx = count
count += 1
todos[idx] = request.json.get('data')
return {
'todo_id': idx,
'data': todos[idx]
}
@Todo.route('/<int:todo_id>')
class TodoSimple(Resource):
def get(self, todo_id):
"""Todo 리스트에 todo_id와 일치하는 ID를 가진 할 일을 가져옵니다."""
return {
'todo_id': todo_id,
'data': todos[todo_id]
}
def put(self, todo_id):
"""Todo 리스트에 todo_id와 일치하는 ID를 가진 할 일을 수정합니다."""
todos[todo_id] = request.json.get('data')
return {
'todo_id': todo_id,
'data': todos[todo_id]
}
def delete(self, todo_id):
"""Todo 리스트에 todo_id와 일치하는 ID를 가진 할 일을 삭제합니다."""
del todos[todo_id]
return {
"delete" : "success"
}
...
각 API 옆에 설명이 추가 되었어요!
참고: Namespace
객체가 아닌 Api
객체에서도 작동합니다.
일단 많은 것들을 설명 하기 전에 Namespace.model()
에 대해 설명 하고자 합니다. Namespace.model()
은 입력, 출력에 대한 스키마를 나타내는 객체 입니다. flask_restx
내의 field
클래스를 이용하여, 설명, 필수 여부, 예시를 넣을 수 있습니다.
또한 Namespace.inherit()
을 이용하여, Namespace.model()
을 상속 받을 수 있습니다.
todo.py
...
todo_fields = Todo.model('Todo', { # Model 객체 생성
'data': fields.String(description='a Todo', required=True, example="what to do")
})
todo_fields_with_id = Todo.inherit('Todo With ID', todo_fields, { # todo_fields 상속 받음
'todo_id': fields.Integer(description='a Todo ID')
})
...
스키마 모델이 추가 됐어요!
Namespace.doc()
데코레이터를 이용하여, Status Code 마다 설명을 추가하거나, 쿼리 파라미터에 대한 설명을 추가 할 수 있습니다.
params
: dict
객체를 받으며, 키로는 파라미터 변수명, 값으로는 설명을 적을 수 있습니다.responses
: dict
객체를 받으며, 키로는 Status Code, 값으로는 설멍을 적을 수 있습니다.todo.py
...
@Todo.route('/<int:todo_id>')
@Todo.doc(params={'todo_id': 'An ID'})
class TodoSimple(Resource):
[...]
@Todo.doc(responses={202: 'Success'})
@Todo.doc(responses={500: 'Failed'})
def delete(self, todo_id):
"""Todo 리스트에 todo_id와 일치하는 ID를 가진 할 일을 삭제합니다."""
del todos[todo_id]
return {
"delete" : "success"
}, 202
...
파라미터와 Status Code에 대한 설명이 추가 됐어요!
말 그대로, 특정 스키마가 들어 올것을 기대 한다. 라고 보면 됩니다. Namespace.Model 객체를 등록하면 됩니다.
말 그대로, 특정 스키마가 반환 된다. 라는 것을 알려 준다고 보면 됩니다.
첫 번째 파라미터로 Status Code, 두 번째 파라미터로 설명, 세 번째 파라미터로 Namespace.Model
객체가 들어 갑니다.
todo.py
...
class TodoPost(Resource):
@Todo.expect(todo_fields)
@Todo.response(201, 'Success', todo_fields_with_id)
def post(self):
"""Todo 리스트에 할 일을 등록 합니다."""
global count
global todos
idx = count
count += 1
todos[idx] = request.json.get('data')
return {
'todo_id': idx,
'data': todos[idx]
}, 201
...
Parameters와 Responses에 대한 설명이 추가 됐어요!
app.py
from flask import Flask
from flask_restx import Resource, Api
from todo import Todo
app = Flask(__name__)
api = Api(
app,
version='0.1',
title="JustKode's API Server",
description="JustKode's Todo API Server!",
terms_url="/",
contact="justkode@kakao.com",
license="MIT"
)
api.add_namespace(Todo, '/todos')
if __name__ == "__main__":
app.run(debug=True, host='0.0.0.0', port=80)
todo.py
from flask import request
from flask_restx import Resource, Api, Namespace, fields
todos = {}
count = 1
Todo = Namespace(
name="Todos",
description="Todo 리스트를 작성하기 위해 사용하는 API.",
)
todo_fields = Todo.model('Todo', { # Model 객체 생성
'data': fields.String(description='a Todo', required=True, example="what to do")
})
todo_fields_with_id = Todo.inherit('Todo With ID', todo_fields, {
'todo_id': fields.Integer(description='a Todo ID')
})
@Todo.route('')
class TodoPost(Resource):
@Todo.expect(todo_fields)
@Todo.response(201, 'Success', todo_fields_with_id)
def post(self):
"""Todo 리스트에 할 일을 등록 합니다."""
global count
global todos
idx = count
count += 1
todos[idx] = request.json.get('data')
return {
'todo_id': idx,
'data': todos[idx]
}, 201
@Todo.route('/<int:todo_id>')
@Todo.doc(params={'todo_id': 'An ID'})
class TodoSimple(Resource):
@Todo.response(200, 'Success', todo_fields_with_id)
@Todo.response(500, 'Failed')
def get(self, todo_id):
"""Todo 리스트에 todo_id와 일치하는 ID를 가진 할 일을 가져옵니다."""
return {
'todo_id': todo_id,
'data': todos[todo_id]
}
@Todo.response(202, 'Success', todo_fields_with_id)
@Todo.response(500, 'Failed')
def put(self, todo_id):
"""Todo 리스트에 todo_id와 일치하는 ID를 가진 할 일을 수정합니다."""
todos[todo_id] = request.json.get('data')
return {
'todo_id': todo_id,
'data': todos[todo_id]
}, 202
@Todo.doc(responses={202: 'Success'})
@Todo.doc(responses={500: 'Failed'})
def delete(self, todo_id):
"""Todo 리스트에 todo_id와 일치하는 ID를 가진 할 일을 삭제합니다."""
del todos[todo_id]
return {
"delete" : "success"
}, 202
다음 시간에는 Flask를 통해 사용자 인증을 해보는 방법을 알아 보겠습니다. 혹시, 이 포스트의 설명이 부족하다면, 댓글을 달아 주시거나, Flask-RESTX Document를 참고 해 주세요!