본문 바로가기
Python

플라스크는 간단하다. 하지만 간단하기만 한 것은 아니다.

by 돈민찌 2021. 10. 27.
반응형

이번에 마이크로 프레임워크인 플라스크를 가지고 스파르타코딩클럽 사이트의 클론코딩을 해보았다. 이 과정에서 느낀 몇가지 배운 점들을 기록으로 남겨두고자 한다.

대형 프로젝트에는 부적합하다?

이 부분을 꽤나 고민했다. 그러나 프로그래밍에 대한 대부분의 고민이 그렇듯 이미 누군가는 이 고민을 진작에 답까지 내린 상태였고, 나는 그걸 주워다 쓰기만 하면 됐다. 출처도 문제없다. 공식문서에서 나온 것이니까. 플라스크 애플리케이션 객체를 만들때, 

from flask import Flask

app = Flask(__name__)

이렇게 만들어왔을 것이다. 무슨 뜻인지는 알고 사용했었나? 저 __name__이 무슨 뜻인지는 알고 썼던가? __name__ == "__main__" 이라는 메인 함수를 사용할 때처럼, 저 __name__이라는 것에는 현재 이 파일을 실행하고 있는 파일(임포트를 하더라도 이 파일은 실행되니까)의 이름이 담기게 되고, 원본과 같은 파일이라면 "__main__"이라는 문자열이 저 자리를 차지하게 된다. 다르게 얘기하자면, app.py를 직접 실행하지 않고 외부에서 이 app 파일을 임포트해서 사용하면 저 자리에는 다른 문자열(아마도 app.py라는 이름)이 들어가게 될 것이라는 거다. 이게 지난번에 설명한 wsgi가 app.py 파일을 임포트해 사용하면서 플라스크가 아 이게 지금 개발모드가 아니구나 하고 인지하게 되는 부분 중에 하나다. 

첫번째 질문부터 대답하자면 그렇지 않다. 일단 폴더 구조의 복합성. 충분히 구현 가능하다. 저 Flask 라는 함수에는 사실 __name__ 뿐만 아니라 다른 파라미터도 선택적으로 넘길 수 있다. 이런 식이다.

from flask import Flask

application = Flask(__name__, static_folder='static', template_folder='templates')

플라스크 프로젝트를 만들어보았다면 이 코드의 뜻을 대강 이해하게 될 것이다. 사실 디폴트로 정해진 값을 굳이 표현한 코드인데, 바꿔 말하면 저기 static과 templates 라는 파라미터를 수정하면 내가 원하는 파일을 스태틱 폴더와 템플릿 폴더로 사용할 수 있다.

블루프린트 (BluePrint:: 청사진)

일반적으로 플라스크를 구현할 때, 라우트 어노테이션을 통해서 URL 구조를 만들기 마련이다. @app.route("/") 이런 식으로 구현되는 이 어노테이션을 더한 함수를 라우트 함수라고 하고, 이런 식으로 URL을 매핑하고는 한다. 그런데, 하나 생각해 볼만한 부분이 있다. 지금까지 작성한 대로 /home, /mypage.... /api/shop... 이런 식으로 라우트를 작성하다보면 라우팅을 담당하는 app.py 파일의 몸집이 지나치게 커지는 경향이 있고, 기능적으로 이 라우팅들을 구분하기 힘든 부분이 있다. 또, 이런 경우도 있다. app.py에서 몽고디비에 데이터를 저장하고 꺼내오는 함수들을 분리하고 싶다-라거나 그 외 데이터의 구조적 형태를 알기 쉽게 정리해놓은 인터페이스/모델 파일을 따로 두고 싶을 때, 그 파일들이 어떠한 이유로 app 객체를 참조해야 하는 경우이다. 이럴 때 플라스크는 circular import 경고를 띄운다. (하나의 파일로만 만들어왔다면 못 봤을 수 있다.)

하나의 app.py 파일로만 플라스크 프로젝트를 운영해 나가는 방식은 다양한 오류와 만났을 때 디버깅이 힘들어지고, 객체지향을 하고자 한다거나, URL을 많은 양으로 분리해야 하는 경우에 문제를 일으킬 수 있다. 이에 대해 플라스크 공식 문서에서는 큰 그림을 그릴 수 있는 "블루프린트"라는 것을 제공한다. 이것만 사용해도 api와 실제 URL로 쓰일 라우터를 구분할 수 있는 깔끔한 파일을 만들 수 있다.

일단 파이썬 파일 하나(api.py)를 따로 만든다. 위치는 app.py와 동등한 위치에 있어도 되지만 이왕 분리하는 거 apps라는 폴더에 넣어보자. 그리고 그 파일에 이렇게 작성한다.

from flask import Blueprint

bp = Blueprint('api', __name__, url_prefix='/api')

블루프린트는 애플리케이션을 구조화시킬 때 아주 유용한 함수다. 위의 코드를 해석하자면 블루프린트 하나를 만들건데, 이름은 __name__ (api.py) 으로 할거다. 그리고 기본적으로 여기의 모든 URL에는 앞에 /api가 붙을 것이다! 이렇게 선언해준 것이다. 그리고 다음 줄부터

@bp.route('/enrollment', methods=['GET', 'POST'])
def post_enrollment():

이렇게 작성할 수 있다. app.route를 bp.route로 바꾼 것이다. 원래는 이 주소가 /api/enrollment 였는데 이렇게 코드를 줄일 수 있게 됐다. 또 하나 눈치가 빠른 분들은 methods에 저렇게 여러가지 http 요청 메소드를 넣을 수 있다는 것도 눈치채셨을 것이다(사실 methods라는 단어부터가 복수단어지만).

그리고 이 다음 줄 부터는 이렇게 요청의 메소드에 따라 분기해서 라우팅의 값을 리턴해 줄 수 있다. 

@bp.route('/enrollment', methods=['GET', 'POST'])
def post_enrollment():
    if request.method == 'GET':
        title = request.args.get("title")
        course = courses.find_one({"title": title}, {"_id": False})
        return jsonify(course)
    elif request.method == "POST":
        response = #
        return response

여기서 끝나면 안되고, 원래의 app.py 파일에 가서, 이 블루프린트 객체를 애플리케이션에 소속되게 해야한다. 이것도 어렵지 않다.

# -*- coding: utf-8 -*-
from flask import Flask
from apps import apis

application = Flask(__name__, static_folder='static', template_folder='templates')
application.register_blueprint(apis.bp)

애플리케이션에 해당 블루프린트를 쏙 집어넣었다. 간단하게 파일 분리와 URL 프리픽스 문제까지 해결했다. 이런 식으로 에러 페이지가 담긴 errors.py를 따로 둔다거나, 유저에 관련된 메소드만 담은 users.py를 따로 두고 프로젝트를 개발할 수도 있다. 훨씬 그럴듯해 보이지 않는가?

flask_mongoengine

이미 pymongo로 편하게 몽고디비를 사용하고 있는데 뭐가 문제냐고 할 수도 있을 것 같다. 하지만 플라스크에서 함께 개발해서 공식문서도 잘 나와있는 플라스크용 몽고디비 엔진이 있으니 한번 써보는 것을 추천한다. 어떤 식으로 쓰냐면 이런 식으로 쓴다.

from flask import Flask
from pymongo import MongoClient
from flask_mongoengine import MongoEngine

application = Flask(__name__, static_folder='static', template_folder='templates')
application.register_blueprint(apis.bp)
client = MongoEngine()
client.init_app(application)

앞의 코드랑 합치면 이렇게 된다. 몽고엔진이라는 플라스크 익스텐션 패키지를 설치하고 이것을 애플리케이션과 연결했다. 그러면 이 다음에 이런 작업을 할 수 있다.

import uuid
import mongoengine as me


class Course(me.Document):
    price = me.IntField(required=True)
    discount_ratio = me.FloatField(required=True)
    title = me.StringField(max_length=32)
    tutor = me.StringField(min_length=2)
    week = me.IntField(min_value=1, max_value=18)

이렇게 하나의 몽고디비 도큐먼트를 객체지향적으로 설계할 수 있다. Mongoengine의 Document라는 클래스를 상속받기만 하면, 이렇게 다양한 형태의 필드를 사용할 수 있다. 몽고디비의 자유로운 형태에 맞게 ListField, UUIDField 같은 특수한 필드도 갖춰져있고, Int, String, Float과 같은 자료형의 필드도 갖출 수 있다. 그리고 여기에 맞게 max_length나 required, unique 같은 속성도 부여할 수 있다.

굳이 장고도 아니고 플라스크에서 이렇게까지 해야하나? 의문이 들 수도 있는데, 이렇게 하면 코드가 복잡하고 길어져 객체화 되지 않은 데이터를 몽고디비에 삽입하려는 과정에서 이를 미리 사전차단할 수 있게 되고, 문자열 형태를 띈 숫자 데이터 등을 알아서 형태변환을 해서 저장해주는 기능도 누릴 수 있다. 이렇게 만든 객체는 to_json()으로 제이슨 형태로 바꿀 수도 있고, 파이몽고와 병행하기 위해  to_mongo() 메소드도 사용할 수 있다.

 

반응형

댓글