실제로 코드를 작성하다보면, 엄청 길어지게 되고, 하나의 파일에 코드가 무한하게 길어진다.
이를 스파게티 코드라고 하는데, 스파게티 코드가 되는 것을 막기 위해 각 api 모듈별로 폴더 및 파일분리 작업을 해줘야 한다.
Blueprint 라이브러리로 구조 짜기
MVC 패턴과 blueprint에 대해서는 이미 정리를 해두었지만, 실제로 파일 분리 작업을 하려니 헷갈려서 프로젝트 api 기준으로 파일 분리 작업한 과정을 정리해보고자 한다.
우선 프론트구성은 별도 폴더에서 구현할 것이므로, templates와 static 폴더는 해당 구조에서 빠졌다
├── app.py(메인 서버 코드)
├── view
│ └── user.py
├── controller
│ └── user.py
└── model
app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
from flask import Flask
from controller.user import User
from view import user
import os
from flask_cors import CORS
# https 를 쓰지 않을 경우, 보안 이슈로 에러가 남 (다음 설정을 통해 http 에서도 에러가 나지 않도록 함)
# os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
app = Flask(__name__)
# app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0
CORS(app)
app.secret_key = 'dave_server1'
app.register_blueprint(blog.blog_abtest, url_prefix='/blog')
#view를 메인의 bluprint에
|
cs |
controller/user.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
|
from model.user import User
#실제 model과 통신하는 역할을 하는 controller
class User(UserMixin):
def __init__(self, user_id, user_email, blog_id):
self.id = user_id
self.user_email = user_email
self.blog_id = blog_id
def get_id(self):
return str(self.id)
@staticmethod
def get(user_id):
mysql_db = conn_mysqldb()
db_cursor = mysql_db.cursor()
sql = "SELECT * FROM user_info WHERE USER_ID = '" + \
str(user_id) + "'"
db_cursor.execute(sql)
user = db_cursor.fetchone()
if not user:
db_cursor.close()
return None
user = User(user_id=user[0], user_email=user[1], blog_id=user[2])
db_cursor.close()
return user
|
cs |
->DB 세팅이 완료되어 model이 생긴다면 그곳에서 해당하는 DB를 불러오고 모델과 통신하는 코드를 작성
왼쪽의 그림에서 알 수 있듯이, controller은 모델과 view 사이를 연계해주는 역할을 한다. 이 그림을 보고 해당 코드의 구조를 보면 좀 더 이해가 쉬울 것 같다.
view/user.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
|
from flask import Flask, Blueprint
from controller.user import User
#controller에서 전달받은 데이터를 브라우저에 보여주기 위함?
user = Blueprint('user', __name__)
# 복잡한 소스파일에서는 코드를 분리하여 MVC 패턴을 정확하게 따라서,
# 리턴할 view 데이터 생성을 위한 함수들을 control 에 넣을 수 있음
# 간단한 코드일 경우에는, 기능별로 모아놓는 것이 여러 파일을 왔디갔다하지 않아서, 더 유용함
# MVC 패턴은 케이스에 따라 사용하는 것이 합리적임
@blog_abtest.route('/auth')
@login_required
def auth_test():
return 'auth'
@blog_abtest.route('/set_email')
def set_email():
user_email = request.args.get('user_email')
blog_id = request.args.get('blog_id')
user = User.create(user_email, blog_id)
login_user(user, remember=True, duration=datetime.timedelta(days=365))
# return render_template(get_blog_page(blog_id), user_email=user_email)
# return redirect(url_for('blog.blog', blog_id=blog_id, user_email=user_email))
return redirect(url_for('blog.blog'))
|
cs |
Blueprint 구조로 짜니 의문점은,
view 폴더(파일)이 꼭있어야 할까? 구글링해서 돌아다니는 모든 글들에 templates와 static이 있고 view는 이를 렌더링해주는 역할을 하는데, 우린 실제로 프론트와 백을 구분해서 코드를 짤 것이라서, 필요가 없기 때문이다. 그러면 view가 없어도 되지 않을까?
flask-restx 라이브러리로 구조 짜기
-> 이는 프로젝트 같은 팀원인 현정님이 구현해주신 것이고 이를 바탕으로 공부해보려 한다.
flask-restx를 이용한 MVC 패턴
├── app
│ ├── init.py
│ ├── main
│ │ ├── controller: 모델과 관련된 모든 수신, HTTP요청을 처리
│ │ │ └── init.py
│ │ │ └── user_controller.py
│ │ ├── model: 모델 정의
│ │ │ └── init.py
│ │ │ └── user.py
│ │ └── service: DB 테이블 관련 쿼리문 작성
│ │ │ └── init.py
│ │ │ └── user_service.py
│ │ ├── util( 해당 파일의 역할이 아직 헷갈린다,)
│ │ │ └── decorator.py
│ │ │ └── dto.py
│ │ ├── init.py(create.app 실행)
│ │ ├── config.py (환경변수 설정)
│ └── migrations
│ └── manage.py(main 서버파일)
└── .gitignore
app/__init__.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
from flask_restx import Api
from flask import Blueprint
from .main.controller.user_controller import api as users_ns
-> 각 controller에서 만든 api 함수를 가져와서 아래 blueprint에 등록해서 사용가능하도록 blueprint = Blueprint('api', __name__)
api = Api(blueprint,
title='FLASK RESTPLUS(RESTX) API BOILER-PLATE WITH JWT',
version='1.0',
description='a boilerplate for flask restplus (restx) web service'
)
api.add_namespace(users_ns, path='/user')
|
cs |
app/main/__init__.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_bcrypt import Bcrypt
from .config import config_by_name
db = SQLAlchemy()
flask_bcrypt = Bcrypt()
def create_app(config_name):
-> 실제 실행 app = Flask(__name__)
app.config.from_object(config_by_name[config_name])
db.init_app(app)
flask_bcrypt.init_app(app)
return app
|
cs |
app/manage.py
-> 실제로 서버가 실행되는 파일
let’s create our application entry point. In the root directory of the project
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
36
37
38
39
|
import os
import unittest
from flask_migrate import Migrate, MigrateCommand
from flask_script import Manager
from app import blueprint
from app.main import create_app, db
app = create_app(os.getenv('BOILERPLATE_ENV') or 'dev')
# 환경 변수에서 필요한 매개 변수를 사용하여 응용 프로그램 인스턴스를 생성 초기에 만들어진 기능 - dev, prod, test. 환경 변수에 아무것도 설정되지 않은 경우 기본값 dev이 사용됩니다.
app.register_blueprint(blueprint)
app.app_context().push()
manager = Manager(app) #Flask-Script를 통해 모든 데이터베이스 마이그레이션 명령을 노출. dbMigrateCommandadd_commandmanager
migrate = Migrate(app, db)
manager.add_command('db', MigrateCommand)
@manager.command
def run():
app.run()
@manager.command
def test():
"""Runs the unit tests."""
tests = unittest.TestLoader().discover('app/test', pattern='test*.py')
result = unittest.TextTestRunner(verbosity=2).run(tests)
if result.wasSuccessful():
return 0
return 1
if __name__ == '__main__':
manager.run()
|
cs |
app/main/controller/user_controller.py
->http 통신을 보여주며, namespace.model에 대해 설명하는 파일인 것이다.
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
36
37
38
39
40
41
42
43
44
|
# user 모델과 관련된 모든 수신, HTTP요청을 처리.
from flask import request
from flask_restx import Resource
from ..util.dto import UserDto
from ..service.user_service import save_new_user, get_all_users, get_a_user
api = UserDto.api -> 모델 자체를 호출
_user = UserDto.user -> 모델의 각 칼럼을 호출
@api.route('/')
class UserList(Resource):
@api.doc('list_of_registered_users')
@api.marshal_list_with(_user, envelope='data')
def get(self):
"""List all registered users"""
return get_all_users()
@api.expect(_user, validate=True)
@api.response(201, 'User successfully created.')
@api.doc('create a new user')
def post(self):
"""Creates a new User """
data = request.json
return save_new_user(data=data)
@api.route('/<id>')
@api.param('id', 'The User identifier')
@api.response(404, 'User not found.')
class User(Resource):
@api.doc('get a user')
@api.marshal_with(_user)
def get(self, id):
"""get a user given its identifier"""
user = get_a_user(id)
if not user:
api.abort(404)
else:
return user
|
cs |
위의 코드에서 api 부분(util에서 import해온 것)을 namespace로 보고 각 메소드의 관계를 이해하면 된다.
Namespace.doc() 데코레이터를 이용하여, Status Code 마다 설명을 추가하거나, 쿼리 파라미터에 대한 설명을 추가 할 수 있습니다.
>params: dict 객체를 받으며, 키로는 파라미터 변수명, 값으로는 설명을 적을 수 있습니다.
>responses: dict 객체를 받으며, 키로는 Status Code, 값으로는 설멍을 적을 수 있습니다.
Namespace.expect() : 말 그대로, 특정 스키마가 들어 올것을 기대 한다. Namespace.Model 객체를 등록하면 된다
Namespace.response() : 말 그대로, 특정 스키마가 반환 된다. 라는 것을 알려 준다고 보면 됩니다.
첫 번째 파라미터로 Status Code, 두 번째 파라미터로 설명, 세 번째 파라미터로 Namespace.Model 객체가 들어 갑니다.
app/main/service/user_service.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
36
37
38
39
40
|
#user 모델에 관련된 쿼리문 작성 파일
from app.main import db
from app.main.model.user import User
#회원가입한 회원 정볼를 user모델(즉, user테이블에 넣기)
def save_new_user(data):
user = User.query.filter_by(email=data['email']).first()
if not user:
new_user = User(
email=data['email'],
username=data['username'],
password=data['password'],
)
save_changes(new_user)
response_object = {
'status': 'success',
'message': '회원가입 되었습니다. '
}
return response_object, 201
else:
response_object = {
'status': 'fail',
'message': '이미 가입된 회원입니다.',
}# 체를 JSON으로 포맷 하기 위해 jsonify 를 사용할 필요가 없습니다 . Flask-restplus가 자동으로 수행합니다.
return response_object, 409
def get_all_users():
return User.query.all()
def get_a_user(id):
return User.query.filter_by(id=id).first()
def save_changes(data):
db.session.add(data)
db.session.commit()
|
cs |
app/main/util/dto.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
from flask_restx import Namespace, fields
class UserDto:
api = Namespace('user', description='user related operations')
user = api.model('user', {
'email': fields.String(required=True, description='user email address'),
'username': fields.String(required=True, description='user username'),
'password': fields.String(required=True, description='user password'),
})
|
cs |
다시 서버구조의 종류와 특징들에 대해서 알아보았다.
server architecture 종류
1. MVC 패턴
: 블로그 내에 정리해두었으니 참고하면 될 것 같다.
2. layered architecture
www.quora.com/Is-MVC-different-from-a-3-layered-architecture
-> 엄연히 MVC 패턴과 layered architecture은 다르지만 어떻게 보면 그 뿌리는 비슷한 것 같다.
위의 그림에서 보면 알 수 있듯이, MVC 패턴은 좀더 유기적으로 역할을 서로 공유하는 것이고 layered는 이러한 MVC 패턴에 체계화와 분리성을 준 개념인 듯하다.
server architecture 라이브러리 종류
1. blueprint
위에서 실제로 구현해본 라이브러리이다.
2. flask-restx 라이브러리 활용하기
justkode.kr/python/flask-restapi-2
-> 두 라이브러리를 구분하려 했는데, 이 두개는 경쟁하는 개념이 아니라 별개의 라이브러리이며 실제로 혼용돼서 사용되기도 하는것 같다.
flask-restx.readthedocs.io/en/latest/scaling.html
-> blueprint의 주 역할은
registering the blueprint with the app takes care of setting up the routing for the application
Using a blueprint will allow you to mount your API on any url prefix and/or subdomain in you application:
즉, 라우팅의 역할에 있는 것이고,
-> flask-restx의 주 역할은
Sometimes you need to maintain multiple versions of an API. If you built your API using namespaces composition, it’s quite simple to scale it to multiple APIs.
즉, 다중 api의 사용 시에 유용하게 쓰일 수 있다.
www.youtube.com/watch?v=pjVhrIJFUEs
exploreflask.com/en/latest/blueprints.html
flask-docs-kr.readthedocs.io/ko/latest/blueprints.html
'SW Engineering > BE' 카테고리의 다른 글
Flask 서버_flask-migrate (0) | 2020.11.20 |
---|---|
Flask 서버구축하기_DB 설정하기(feat.Mysql/SQLAlchemy) (0) | 2020.11.20 |
Flask 서버_JWT활용한 인증기능 (0) | 2020.11.19 |
Flask 서버_flask-bycrypt로 비밀번호 암호화 (0) | 2020.11.19 |
Flask 서버구축하기_MVC 패턴(feat. blueprint) (1) | 2020.11.14 |