TIL - Flask 프로젝트 초기 설계

valentin123·2020년 4월 12일
0

Trace of code

목록 보기
39/57

이번에 (주)브랜디와 기업협업을 하면서 플라스크 프레임워크를 통해서 브렌디 어드민 사이트를 클론하는 프로젝트를 하게 되었다.

우선 플라스크는 장고와 다르게 프레임워크자체에서 갖춰진 것이 거의 없기때문에 MVC(Model View Controler)패턴에 맞게 프로젝트를 초기설계를 해야했다.

이번 포스팅에서는 플라스크 프레임워크의 작동과 MVC패턴에 맞는 레이어드 아키텍처 초기설계에 대해서 알아보자.

우선 백앤드 api의 아키텍처에는 여러가지가 있을것이지만 가장 널리 사용되는 레이어드 아키텍처에 맞게 프로젝트를 3가지 section으로 나눈다.

view

프로젝트에서 backend endpoint경로를 처리해주는 부분이다. 플라스크에서는 Flask 클래스가 route라는 매서드를 가지고 있고 이를 통해서 클라이언트에게서 온 request를 처리한다.

service

api에서 로직을 담당하는 영역이다. 데이터베이스에 300자이상의 글을 저장시키지 않겠다고 하면 service영역에서 이를 걸러주는 로직이 추가되어야 한다.

model

데이터베이스와 직접적으로 통신하는 영역이다.

이렇게 세가지 영역이 있다는 것을 알고 본격적인 플라스크 프로젝트 초기 설계에 들어가보도록 하자. 하나의 프로젝트 안에는 많은 application이 들어갈 수 있다. 이번프로젝트에서는 셀러, 상품, 기획전 이라는 크게 3개의 영역이 모델링에서 나왔기 때문에 모델링에서 나온 데이터의 성격을 기준으로 application을 나눴다.

첫번째 application인 seller를 통해서 프로젝트 초기설계를 알아보자.

Blueprint

블루프린트를 이해하기위서는 api의 경로를 설정해주는 백앤드의 가장 앞쪽에 위치하는 view를 살펴봐야한다.
seller 앱의 가장 앞에 위치하는 seller_view는 위에서 설명한 레이어드 아키텍처에서 view 부분에 해당한다. 클라이언트에게서 온 요청을 처리하는 영역으로 엔드포인트를 정의하는 곳이다. 플라스크에서는 Blueprint라는 기능을 지원한다.
Blueprint는 청사진이라는 뜻을 가지는 단어로 우선적으로 엔드포인트 경로를 설정해준다.(아직 경로가 run은 되지 않은 상태)

from flask import Blueprint

class SellerView:
[1] seller_app = Blueprint('seller_app', __name__, url_prefix='/seller')

[2] @seller_app.route('/<int:parameter_account_no>/password', methods=['PUT'], endpoint='change_password')
    @login_required
    def change_password(*args):

[1] Blueprint클래스 안에 속성들을 넣어주고 seller_app이라는 인스턴스를 만들어준다. 여기서 url_prefix에 들어가는 값을 넣어주면 주소에 '/seller'이 고정되어 적용된다.
[2] 만들어진 seller_app인스턴스 안에있는 route라는 매서드를 사용해서 url을 라우팅 해준다. 이 경우에는 /seller/1 과같이 셀러안에 1번리소스를 의미하며 PUT매서드만 받아들인다. route라는 라우팅을 해주는 매서드는 원래 플라스크 클래스 자체에 존재하지만 이를 사용하지않고 blueprint클래스(플라스크 코드를 전부 들여다보지 않아서 모르지만 Blueprint클래스가 Flask클래스를 상속받아서 route매서드를 사용하는 것으로 추정함.)를 사용하는 이유는 flask 인스턴스의 의존성 때문이다. flask인스턴스는 프로젝트의 가장 중앙에서 프로젝트의 속성(?)을 가지고있는 프로젝트 그 자체이기 때문에 flask인스턴스 자체가 하위의 앱에 route를 위해서 import되지 않아야 하기 때문이다. 그래서 '청사진'이라는 뜻을 가진 Blueprint를 사용하여 프로젝트가 실재로 라우팅되기 전에 그 경로를 전부 엔트포인트 별로 연결시켜놓고 실재로 프로잭트가 실행(run) 되었을 때 이미 blueprint로 생성된 경로를 라우팅 해준다.

app.py

app.py에서는 서버가 실행되면 플라스크 프로젝트를 만들어주는 역할을 한다.

def create_app():
    # set flask object
[1] app = Flask(__name__)
    app.json_encoder = CustomJSONEncoder
    make_config(app)
    CORS(app)
[2] app.register_blueprint(SellerView.seller_app)
    app.register_blueprint(ProductView.product_app)
    app.register_blueprint(ImageView.image_app)
    app.register_blueprint(EventView.event_app)

    return app

[1] 플라스크 클래스를 인스턴스로 만들어준다. 그리고 프로젝트가 실행될 때 꼭 들어가야할 json_encoder, make_config, CORS와 같은 함수, 매서드들을 실행해준다.
[2] 이단계에서 각각의 앱에서(여기서는 seller_view) blueprint로 연결해주었던 url경로를 실재로 플라스크 클래스의 register_blueprint 매서드를 통해서 실행해준다. 이제 app.py에서 create_app함수가 호출되는 순간 만들어 준 모든 앱에서 라우팅이 동시에 이루어지게 되었다.

manage.py

플라스크 프로젝트가 실행되는 곳이다.

from app import create_app

if __name__ == "__main__":                    [1]
    app = create_app()			      [2]
    app.run(host="0.0.0.0", port=5000)        [3]

[1] 파이썬에서 name 는 지금 이 name 이 타입된 모듈의 이름이다. 그리고 이 모듈이 다른파일에서 import되어 실행되지 않고 이 모듈 자체를 실행하면 namemain 이 된다. 즉, 이 뜻은 '이 모듈을 직접(어딘가에서 import되어서 실행하지 않고)실행하면 ~' 이라는 뜻이다.
[2] app.py에 있는 create_app함수를 실행해서 나온 리턴값인 플라스크 인스턴스를 변수에 담아준다.
[3] 플라스크 인스턴스를 로컬호스트 5000번 포트로 run해준다.

결론적으로, manage.py를 shell에서 직접 실행하면 app.py에 있는 create_app 함수가 5000번포트로 로컬에서 실행된다.

connection.py

각존 연결을 관리하는 모듈을 따로 만들어준다.

# make s3 connection
[1] def get_s3_connection():
        s3_connection = boto3.client(
          's3',
          aws_access_key_id=S3_CONFIG['AWS_ACCESS_KEY_ID'],
          aws_secret_access_key=S3_CONFIG['AWS_SECRET_ACCESS_KEY'],
          region_name=S3_CONFIG['REGION_NAME'],
    )
    	return s3_connection


# make mysql database connection
class DatabaseConnection:

[2] def __init__(self):
            self.db_config = {
            'database': DATABASES['database'],
            'user': DATABASES['user'],
            'password': DATABASES['password'],
            'host': DATABASES['host'],
            'port': DATABASES['port'],
            'charset': DATABASES['charset'],
            'collation': DATABASES['collation'],
        }
        
        self.db_connection = mysql.connector.connect(**self.db_config)
 ......

[1] s3와 연결을 담당하는 함수로 s3와 연결된 객체를 리턴한다
[2] 데이터베이스와의 연결을 담당하는 함수로 리턴값으로 데이터베이스와 연결된 객체를 가진다.

이렇게 커넥션 자체를 리턴값으로 가지는 모듈을 만들어서 필요에 따라서 하나의 커넥션을 만들어주는 방식의 설계를 하는것이 모든 연결을 독립적으로 관리하기 좋을 것이다.

seller_view.py

위에서 설명한 Blueprint를 통해서 api경로설정이 끝난 상태이다. 이제 클라이언트에게 요청을 받으면 그 요청을 다음 레이어로 넘겨주어야한다.

from connection import get_db_connection

from seller.service.seller_service import SellerService

    def get_seller_list(*args):
[0]   	db_connection = get_db_connection()
        if db_connection:
            try:
                seller_service = SellerService()
                changing_password_result = seller_service.change_password(account_info, db_connection)
                return changing_password_result

            except Exception as e:
                return jsonify({'message': f'{e}'}), 400

[1]     	seller_service = SellerService()
[2]                     seller_list_result = seller_service.get_seller_list(request, user, db_connection)
[3]         finally:
                try:
                    db_connection.close()
[4]                     return seller_list_result

[0] 데이터베이스의 연결이 필요할 때 마다 connection.py에서 get_db_connection함수를 호출하여 커넥션을 만들어준다. 한번 만들어진 데이터베이스 커넥션 객체는 한번의 api 요청이 끝날 때([4]의 커넥션 close가 실행되기 전 까지) 까지 유효하다. finally를 통해서 어떠한 경우에도 한번의 scope가 끝나면 데이터베이스 커넥션을 닫아주도록 설계하는 것이 클라이언트로부터의 요청을 처리하고 데이터베이스의 커넥션의 독립성을 확보하는데 중요하다.
[1] seller_service에 있는 클래스의 인스턴스를 만들어준다.
[2] 들어온 요청과 데이터베이스 커넥션을 다음 레이어로 넘겨주고 그 결과가 변수에 담긴다.
[4] 서비스레이어에서 나온 리턴값을 리턴해준다.

결론적으로 각각의 앤드포인트가 독립적으로 다음 레이어에 대한 관계를 정의 해 주도록 설계한다.

seller_service.py

서비스 레이어로서 api에 필요한 로직을 만들어준다.

    def get_seller_list(self, request, user, db_connection):
[1] 	seller_dao = SellerDao()
[2]     auth_type_id = user.get('auth_type_id', None)

        # 마스터 유저이면 dao에 db_connection 전달
[3]     if auth_type_id == 1:
[4]         seller_list_result = seller_dao.get_seller_list(request, db_connection)
[5]         return seller_list_result

[6]     return jsonify({'message': 'AUTHORIZATION_REQUIRED'}), 403

[1] 다음 레이어인 dao에 있는 클래스의 인스턴스를 만들어준다.
[2][3] 서비스레이어의 역할인 api에서 필요한 로직을 수행해주는 잘 보여준다. 여기서는 권한 타입이 1(마스터)인 경우에만 다음 레이어로 값을 넘긴다. 그리고 그 다음레이어인 model에서의 결과값을 변수로 담아주고 [5]에서 리턴한다.
권한 타입 유효성검사에서 걸러지면 [6]에서 error를 리턴한다.

seller_dao.py

레이어드 아키텍처에서 모델에 해당하는 부분으로 데이터베이스와 직접적으로 소통하는 영역이다. 여기서 dao는 database acess object를 뜻한다.

    def get_account_password(self, account_info, db_connection):
        try:
[1]         with db_connection.cursor() as db_cursor:

                # SELECT 문에서 확인할 데이터
[2]             account_info_data = {
                    'account_no': account_info['parameter_account_no']
                }

                # accounts 테이블 SELECT
[3]             select_account_password_statement = """
                    SELECT account_no, password 
                    FROM accounts 
                    WHERE account_no = %(account_no)s
                """

                # SELECT 문 실행
[4]             db_cursor.execute(select_account_password_statement, account_info_data)

                # 쿼리로 나온 기존 비밀번호를 가져옴
[5]             original_password = db_cursor.fetchone()
[6]             return original_password

[1] 서비스로 부터 넘어온 데이터베이스 커넥션에 있고 데이터베이스와 직접적으로 소통하는 역할을 하는 curosr매서드를 실행하여 데이터베이스에 command를 보낸다.
[3] sql명령문을 문자열로 나열한다.
[4] 데이터베이스와의 연결에서 실행자 역할을 하는 cursor의 excute 문을 사용해서 sql명령문을 직접 데이터베이스에 가서 실행하고 값을 cursor에 저장시킨다.
[5] 저장된 cursor에 있는 값을 fetchone 매서드를 통해서 가져온다.
[6] 결과값을 리턴한다.

결국 리턴된 결과값은 레이어의 반대방향 즉, 모델 -> 서비스 -> 뷰를 통해서 클라이언트에게 리턴된다.

MVC 패턴에 맞게 프로젝트의 구조를 짜면서 중요한점을 요약하자면

  • 각각의 레이어드에는 주어진 역할이 있고, 이전의 레이어드가 이후의 레이어드에 의존한다. 예를들면 seller_view는 seller_service에 의존하며 seller_service는 seller_view의 존재를 모른다.
  • 한번의 api호출은 다른 api호출에 영향을 주지 않고 독립성을 가진다. 데이터베이스 커넥션이 한번의 api호출이 시작할때 열리고, 그 scope가 끝나면 finally로 닫아줘서 다음 api호출에 영향이 가지 않도록 하는것과 같은 맥락이다.
  • 각각의 api는 독립성을 가진다. 첫번째로 언급한 내용과 같은 맥락인데 이전의 레이어가 이후의 레이어에 의존하면서 결국 뷰, 모델, 서비스가 하나의 선이 되어 독립적으로 존재하게 되어 api끼리 구분이 이루어진다. 그리고 그 api는 요청이들어와서 처리될때만 (view에서 dao로 이어지는)하나의 선으로 이어지게 된다.
profile
Quit talking, Begin doing

0개의 댓글