Project/[약올림] Final Project

[초기 세팅 및 배포] Flask-RESTX: MVC 폴더 구성 및 Flask REST API 구현하기

HJChung 2021. 1. 22. 09:01

딥러닝을 활용한 이미지 처리기능이 있는 앱 서비스를 개발하기로 하고, DB스키마와 API들을 구성하니 우리 팀이 flask로 구현해야할 데이터베이스 모델은 약 5-6개, API는 27개 이다..

이렇게 많은 API를 만들어야하고 모델도 파일을 분리하여 구현해야겠다고 생각하니

  1. 데이터베이스 모델을 한 파일이 아닌 여러 파일에 나누어서 짜되 다른 migrate = Migrate(app, db)시에는 db에 모두 불러올 수 있도록 하는 것
  2. 최대한 mvc 폴더 구성을 따라보려고 한 것

을 목표로 두고 방법을 찾아보았다. 

 

나는 일단 MVC 패턴을 최대한 구현하여 각 요청에 대한 응답까지 받아서 잘 작동하는지 확인하고 싶었다. 그래서 복잡한 우리 프로젝트 스키마 전에 계속 예시(14. Python ORM - Flask-SQLAlchemy,15. Python ORM - Flask-Migrate)로 사용해오던 simple한 이 두 스키마를 이용해서 <Flask-RESTX를 이용한 MVC 폴더 구성 및 Flask  REST API 구현하기>를 시작해보았다. 

1. 우선 Flask-RESTX을 이용해서 Flask로 간단히 REST API를 주고 받는 API Server를 만들어보자.

1) 설치

$ pip install flask-restx

2) Api 구현을 위한 Api 객체 import

from flask_restx import Api

3) Flask 객체에 Api 객체 등록

app = Flask(__name__)  # Flask 객체 선언, 파라미터로 어플리케이션 패키지의 이름을 넣어줌.
api = Api(app)  # Flask 객체에 Api 객체 등록

4) 그리고 특정 라우터에 GET RestAPI를 정의해보자. 

@api.route('/hello')  # 데코레이터 이용, '/hello' 경로에 클래스 등록
class HelloWorld(Resource):
    def get(self):  # GET 요청시 리턴 값에 해당 하는 dict를 JSON 형태로 반환
        return {"hello": "world!"} #즉, jsonify를 따로 사용하지 않아도 된다. 

이렇게 함수형태로 API에 사용될 HTTP 메소드를 사용한다. 

#app.py

from flask import Flask  # 서버 구현을 위한 Flask 객체 import
from flask_restx import Api, Resource  # Api 구현을 위한 Api 객체 import

app = Flask(__name__)  # Flask 객체 선언, 파라미터로 어플리케이션 패키지의 이름을 넣어줌.
api = Api(app)  # Flask 객체에 Api 객체 등록


@api.route('/hello')  # 데코레이터 이용, '/hello' 경로에 클래스 등록
class HelloWorld(Resource):
    def get(self):  # GET 요청시 리턴 값에 해당 하는 dict를 JSON 형태로 반환
        return {"hello": "world!"}
        
        
@api.route('/todos')
class TodoPost(Resource):
    def get(self, todo_id):
        #'/todos'uri로 get 요청시 할 처리들 코딩
        return {응답}

    def put(self, todo_id):
        #'/todos'uri로 put 요청시 할 처리들 코딩
        return {응답}
    
    def delete(self, todo_id):
    	#'/todos'uri로 delete 요청시 할 처리들 코딩
        return {응답}
        
    def post(self):
   		#'/todos'uri로 post 요청시 할 처리들 코딩
        return {응답}

※ 더 자세한 코드와 flask_restx 의 Api 객체가 없을 때의 REST API 코드를 보고싶다면 <더보기> 클릭

더보기

1. 위 예시의 더 자세한 코드

2. flask_restx 의 Api 객체 없을 때의 REST API 코드예시

1. 위 예시의 더 자세한 코드

from flask import Flask, request
from flask_restx import Resource, Api

app = Flask(__name__)
api = Api(app)

todos = {}
count = 1

@api.route('/todos/<int:todo_id>')
class TodoSimple(Resource):
    def get(self, todo_id):
        return {
            'todo_id': todo_id,
            'data': todos[todo_id]
        }
        
    def post(self):
        global count
        global todos
        
        idx = count
        count += 1
        todos[idx] = request.json.get('data')
        
        return {
            'todo_id': idx,
            'data': todos[idx]
        }

    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"
        }

2. flask_restx 의 Api 객체 없을 때의 REST API 코드예시

from flask import Flask, request, make_response, jsonify
from flask_cors import CORS

app = Flask(__name__)
CORS(app)


@app.route("/test", methods=['GET', 'POST', 'PUT', 'DELETE'])
def test():
    if request.method == 'POST':
        print('POST')
        data = request.get_json() #POST는 이렇게 요청메서드에 함께 오는 파라미터 값을 추출한다. data에는 dict 형식으로 값이 들어가게 된다. 
        print(data)
        print(data['email'])
    if request.method == 'GET':
        print('GET')
        user = request.args.get('email')
        print(user)
    if request.method == 'PUT':
        print('PUT')
        user = request.args.get('email')
        print(user)
    if request.method == 'DELETE':
        print('DELETE')
        user = request.args.get('email')
        print(user)

    return make_response(jsonify({'status': True}), 200)

url pattern 으로 라우팅 시키는 것은 3. flask와 REST API 개념 이해하기(ex. GET)의 <1. routing 더 해보기>에서 정리해 둔 것 처럼 전달되는 값의 데이터 타입도 지정해서 url pattern을 사용할 수 있다.. <>에 들어가는 변수 앞에 테이터 타입을 함께 입력해주면 된다. 

(지정된 데이터타입이 없으면 문자열로 인식한다.)

from flask import Flask
from flask_restx import Resource, Api

app = Flask(__name__)
api = Api(app)


@api.route('/hello/<string:name>')  # url pattern으로 name 설정
class Hello(Resource):
    def get(self, name):  # 멤버 함수의 파라미터로 name 설정
        return {"message" : "Welcome, %s!" % name}
    
if __name__ == "__main__":
    app.run(debug=True, host='0.0.0.0', port=80)

6) 그리고 Flask 객체를 싱행하고 싶다면, 아래 실행을 위한 코드를 작성한다. 

if __name__ == "__main__":
    app.run(debug=True, host='0.0.0.0', port=80)

여기까지 진행하면  가장 기본적인 형태인 아래의 코드가 완성된다. 

#app.py

from flask import Flask  # 서버 구현을 위한 Flask 객체 import
from flask_restx import Api, Resource  # Api 구현을 위한 Api 객체 import

app = Flask(__name__)  # Flask 객체 선언, 파라미터로 어플리케이션 패키지의 이름을 넣어줌.
api = Api(app)  # Flask 객체에 Api 객체 등록


@api.route('/hello')  # 데코레이터 이용, '/hello' 경로에 클래스 등록
class HelloWorld(Resource):
    def get(self):  # GET 요청시 리턴 값에 해당 하는 dict를 JSON 형태로 반환
        return {"hello": "world!"}
        
@api.route('/todos')
class TodoPost(Resource):
    def get(self, todo_id):
        #'/todos'uri로 get 요청시 할 처리들 코딩
        return {응답}

    def put(self, todo_id):
        #'/todos'uri로 put 요청시 할 처리들 코딩
        return {응답}
    
    def delete(self, todo_id):
    	#'/todos'uri로 delete 요청시 할 처리들 코딩
        return {응답}
        
    def post(self):
   		#'/todos'uri로 post 요청시 할 처리들 코딩
        return {응답}

if __name__ == "__main__":
    app.run(debug=True, host='0.0.0.0', port=80)

※ You can also use the automatic documentation on you API root (by default). In this case: http://127.0.0.1:5000/. See Swagger UI for a complete documentation on the automatic documentation.

이를 Swagger UI를 통해 HTTP통신 요청과 응답 처리를 postman을 사용한 것처럼 볼 수 있다!


여기까지 Flask-RESTX로 REST API를 구현하는 방법에 대해서 알아보았다.

그러나 여기서 끝이 아니다. 앞서 말했듯이 우리 팀이 flask로 구현해야할 데이터베이스 모델은,, 약 5-6개, API는... 27이닿ㅎㅎㅎㅎ

2. 그래서 이제는 Flask-RESTX로 파일 분리하는 것에 대해 정리해보고자 한다. 

대규모 프로젝트 진행시 스파게티 코딩을 막기 위해 Flask RESTX 에서는 파일 분리를 어떻게 할까? 공식문서의 <Scaling your project> 내용을 기준으로 정리해보자. 해당 공식문서에는  'Flask-RESTX 앱을 구성하는 방법에는 여러 가지가 있지만 여기서는 확장성과 유지보수가 용이한 방법을 설명한다.' 고 언급하고 있다.

주요 아이디어는 코드를 재사용 가능한 Namespace로 분할하는 것이다.

1) Namespace와 add_namespace()

앞에서

app = Flask(__name__)  # Flask 객체 선언, 파라미터로 어플리케이션 패키지의 이름을 넣어줌.
api = Api(app)  # Flask 객체에 Api 객체 등록

이렇게 선언한 Api 객체에는 add__namespace() 메서드가 있다.이는 flask-restx.Namespace 객체를 특정 경로에 등록 할 수 있게 해준다. 

※ Namespace? Flask 객체에 Blueprint가 있다면, Api 객체에는 Namespace가 있다. 

Namespace 객체도 생성자 파라미터를 조정하여, 내용을 수정 할 수 있다.

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

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

더보기

파이썬의 네임스페이스

출처: hcnoh.github.io/2019-01-30-python-namespace

네임스페이스(namespace, 이름공간)란 프로그래밍 언어에서 특정한 객체(Object)를 이름(Name)에 따라 구분할 수 있는 범위를 의미한다. 파이썬 내부의 모든것은 객체로 구성되며 이들 각각은 특정 이름과의 매핑 관계를 갖게 되는데 이 매핑을 포함하고 있는 공간을 네임스페이스라고 한다.

네임스페이스가 필요한 이유는 다음과 같다. 프로그래밍을 수행하다보면 모든 변수 이름과 함수 이름을 정하는 것이 중요한데 이들 모두를 겹치지 않게 정하는 것은 사실상 불가능하다.

따라서 프로그래밍언어에서는 네임스페이스라는 개념을 도입하여, 특정한 하나의 이름이 통용될 수 있는 범위를 제한한다. 즉, 소속된 네임스페이스가 다르다면 같은 이름이 다른 개체를 가리키도록 하는 것이 가능해진다. (참고)

파이썬의 네임스페이스

출처: hcnoh.github.io/2019-01-30-python-namespace

네임스페이스(namespace, 이름공간)란 프로그래밍 언어에서 특정한 객체(Object)를 이름(Name)에 따라 구분할 수 있는 범위를 의미한다. 파이썬 내부의 모든것은 객체로 구성되며 이들 각각은 특정 이름과의 매핑 관계를 갖게 되는데 이 매핑을 포함하고 있는 공간을 네임스페이스라고 한다.

네임스페이스가 필요한 이유는 다음과 같다. 프로그래밍을 수행하다보면 모든 변수 이름과 함수 이름을 정하는 것이 중요한데 이들 모두를 겹치지 않게 정하는 것은 사실상 불가능하다.

따라서 프로그래밍언어에서는 네임스페이스라는 개념을 도입하여, 특정한 하나의 이름이 통용될 수 있는 범위를 제한한다. 즉, 소속된 네임스페이스가 다르다면 같은 이름이 다른 개체를 가리키도록 하는 것이 가능해진다. (참고)

그러면 add_namespace()를 사용해서 아래의 코드의 TodoPost에 해당하는 부분을 파일 분리 시켜보자.

app.py

#app.py

from flask import Flask  # 서버 구현을 위한 Flask 객체 import
from flask_restx import Api, Resource  # Api 구현을 위한 Api 객체 import

app = Flask(__name__)  # Flask 객체 선언, 파라미터로 어플리케이션 패키지의 이름을 넣어줌.
api = Api(app)  # Flask 객체에 Api 객체 등록
        
@api.route('/todos')
class TodoPost(Resource):
    def get(self, todo_id):
        #'/todos'uri로 get 요청시 할 처리들 코딩
        return {응답}

    def put(self, todo_id):
        #'/todos'uri로 put 요청시 할 처리들 코딩
        return {응답}
    
    def delete(self, todo_id):
    	#'/todos'uri로 delete 요청시 할 처리들 코딩
        return {응답}
        
    def post(self):
   		#'/todos'uri로 post 요청시 할 처리들 코딩
        return {응답}

if __name__ == "__main__":
    app.run(debug=True, host='0.0.0.0', port=80)

 파일 분리하면?! 아래처럼 된다!

자 여기까지 해봤으니,

좀 어렵겠지만 MVC로 파일 구조를 나눠본 내 코드를  보면서 namespace와 Flask-RESTX로 파일분리하기 에 대해서 다시 정리해보자. 

2) Namespace 모듈에는 데이터베이스 model 및 resource 선언을 해줄 수도 있다. 

from flask_restx import Namespace, Resource, fields

api = Namespace('cats', description='Cats related operations')

cat = api.model('Cat', {
    'id': fields.String(required=True, description='The cat identifier'),
    'name': fields.String(required=True, description='The cat name'),
})

그래서 나의 코드에서도 이렇게 user관련 Api를 Namespace에 선언해주면서 user model도 선언해준 것이다. 

3) Blueprint와 함께 사용 

flask-restx는 Blueprint와 함께 사용할 수 있다 나 역시 최상위 실행 폴더에서는 Blueprint로 모든 것을 묶어주었다.

 

이 외에도 Namespace에는 Namespace.doc(), Namespace.expect(), Namespace.response() 등의

많은 기능들이 있다. Blueprint도 마찬가지이다. 

 

reference

Flask로 간단히REST API를 주고 받는API Server만드는 것에 대한 내용은

Flask로 REST API 구현하기 - 1. Flask-RESTX

Flask-RESTX 공식문서의 Quick start

 

Flask-RESTX로 파일 분리하는 것 에 대한 내용은 

Flask로 REST API 구현하기 - 2. 파일 분리, 문서화

Flask-RESTX 공식문서의 Scaling your project

 

전체적인 내가 VS코드 작성한 것은

How to structure a Flask-RESTPlus web service for production builds

github.com/cosmic-byte/flask-restplus-boilerplate

Working with APIs using Flask, Flask-RESTPlus and Swagger UI

 

을 참고하였다. 

그래서 아래와 같은 파일 구성으로 완성되었다. 

 

Flask-RESTX.

├── app

│ ├── __init__.py

│ ├── main

│ │ ├── controller

│ │ │ └── __init__.py

│ │ │ └── post_controller.py

│ │ │ └── user_controller.py

│ │ ├── model

│ │ │ └── __init__.py

│ │ │ └── user.py

│ │ │ └── post.py

│ │ └── service

│ │ │ └── post_service.py

│ │ │ └── user_service.py

│ │ ├── util

│ │ │ └── decorator.py

│ │ │ └── dto.py

│ │ ├── __init__.py

│ │ ├── config.py

│ └── migrations

│ └── manage.py

└── .gitignore

 

하.. 힘들어