Flask로 REST API 구현하기 - 2. 파일 분리, 문서화
flask-restx의 파일 분리와 문서화 2020-07-09
주의! Caution!
해당 게시글은 Archive된 게시글 입니다.
Archive된 사유는 다음 중 하나에 해당 됩니다.
  • 작성 된지 너무 오랜 시간이 경과 하여, API가 변경 되었을 가능성이 높은 경우
  • 주인장의 Data Engineering으로의 변경으로 질문의 답변이 어려워진 경우
  • 글의 퀄리티가 좋지 않아 글을 다시 작성 할 필요가 있을 경우
이 점 유의하여 게시글을 참고 하시길 바랍니다.

저번 시간에는 Flask-RESTX 에 대한 기본적인 사용 법을 알아보고, 이를 이용하여 간단한 API Server를 만들어 보았습니다.

모두가 스파게티 코드를 원하지 않잖아요.

여러분은 당신의 코드가 스파게티 코드가 되는 것을 원치 않을 것 입니다. 그러므로 파일 분리는 우리가 무슨 어플리케이션을 만들던 필수적인 과정입니다.

저번 시간에 구현한 간단한 API 서버가 여러 가지 기능을 동시에 구현 한다고 가정 해 보겠습니다. 간단한 게시판을 위해 API 서버를 만든다고 했을 때, 로그인, 회원 가입, 게시글, 댓글, 대댓글, 사용자 수정 등등... 대충 어림잡아 몇 백줄을 넘길 것 입니다. 코드 하나에 문제가 발생했을 때 대처 하기도 어려울 뿐더러, 가독성도 매우 떨어집니다. 그럼 Flask RESTX 에서는 파일 분리를 어떻게 실시 할까요?

add_namespace()

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()

일단 가장 위에 있는 설명부터 만지는 게 좋아 보입니다. 위 설명은 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()

Namespace 객체도 생성자 파라미터를 조정하여, 내용을 수정 할 수 있습니다. 한 번 볼까요?

  • title: Namespace의 이름을 명시합니다.
  • description: Namespace의 설명을 명시합니다.

한번 todo.py 에 있는 Namespace 객체의 생성자 파라미터를 수정 해 볼까요?

todo.py

...

Todo = Namespace(
    name="Todos",
    description="Todo 리스트를 작성하기 위해 사용하는 API.",
)

...

Todos 옆에 설명이 추가 되었어요!

"""설명"""

PythonComment를 이용하여 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.doc(), Namespace.expect(), Namespace.response()

참고: 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()

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.expect()

말 그대로, 특정 스키마가 들어 올것을 기대 한다. 라고 보면 됩니다. Namespace.Model 객체를 등록하면 됩니다.

Namespace.response()

말 그대로, 특정 스키마가 반환 된다. 라는 것을 알려 준다고 보면 됩니다.

첫 번째 파라미터로 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를 참고 해 주세요!