Flask App의 디렉터리는 어떤 구조로 만들어야 좋을지 고민해보았습니다.

d3fau1t·2021년 10월 30일
5

개발환경구성

목록 보기
1/5
post-thumbnail
post-custom-banner

Flask는 4~5년 전쯤 IoT(Internet of Things) 붐이 일었을 당시 라즈베리파이와 같은 SBC(Single Board Computer) 환경에서 웹 요청을 받았을 때 장치를 제어하는 용도로 사용해본 적 있다.

그 땐 웹 애플리케이션 개발에 대해 아무것도 모르던 채로 사용했고 그냥 잘 작동하니 좋은게 좋은거라고 프로젝트를 마무리하고 그 뒤로 사용하지 않았다.

그러다가 왜 갑자기 Flask를 사용하고
Django나 FastAPI도 있는데 왜 Flask를 쓰는지 묻는다면..

지난 2주간 교육 및 회고목적으로 Flask App을 생성할 필요가 있었고 제대로 써보지 않았던 프레임워크를 사용해보면서 어떻게하면 더 좋은 구조를 짤 수 있을까 고민할 기회가 생겼기 때문이다 라고 대답할 것 같다.

기존에 알고있던 방식

from flask import Flask, render_template
app = Flask(__name__)

@app.route('/')
def get_index():
    return render_template('index.html')

@app.route('/hello')
def get_hello():
    # do something here
    # whatever you want
    return {'message': 'hello'}, 200

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

app.py파일 하나에 라우팅과 모든 기능을 다 넣어놓고 코드를 실행하는 방법이다.
기능이 많지 않다면 납득되지만.. 그렇지 않다면 라인 수가 늘어나면서 어떤기능이 어디 정의되어있는지 파악이 어렵다던가 관리측면에서 난해해질 것 같다는 생각이 든다.

고민을 좀 해보았다

  • API나 기능별로 파일을 따로 관리하고싶다.
  • 서버 설정값이나 환경변수를 따로 관리하고싶다.
    • 배포시 테스트, 상용 환경을 따로 구분하고싶다.
  • 데이터베이스 연결 및 조작(DML)관련 코드는 서비스 코드와 별도 관리하고싶다.
  • 라우팅과 서비스코드를 분리하긴 어려울 것 같다.
    • 서비스코드의 일부 기능을 별도 유틸리티 모듈로 관리하면 좋을 것 같다.

새로 생성한 방식

고민을 좀 해보고 아래와 같은 디렉터리 구조가 생성되었다.

.
├── app.py		# 서비스 실행 스크립트
├── apps		# 서비스 레이어
│   ├── __init__.py	# 이 안에서 app 초기화
│   ├── home.py
│   ├── misc.py
│   └── templates
├── config.dev.json	# 서버 설정파일, 개발
├── config.real.json	# 서버 설정파일, 상용
├── config.local.json	# 서버 설정파일, 로컬
├── database		# 데이터베이스 레이어
│   ├── __init__.py
│   └── user.py
├── requirements.txt	# 의존성
├── settings.py		# 설정파일 초기화 스크립트
└── utils		# 기능 별도관리
    ├── __init__.py
    ├── auth.py
    ├── env.py
    └── timer.py

app.py를 실행하는것은 변함없지만 서비스코드를 apps 디렉터리 안에 별도 분리하여 관리하고, 일부 로직은 utils 안에 넣어서 서비스 레이어의 코드가 길어지는 것을 막고 재사용하기 편하도록 하면 유지보수가 쉬워질 것 같다.
마찬가지로 서비스 레이어세서 사용될 데이터베이스 조작기능은 별도 모듈, 함수로 작성해서 가져다 사용하는 것이 좋아보인다.

app.py

from apps import app

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

모든 서비스 코드를 apps 안에 분리해서 넣어버리니 매우 단순해졌다.
기능별로 라우팅을 분리하기위해 Blueprint 라는 컨셉을 사용했다.

Blueprint에 등록되는 route는 Flask Application 안에서 서로 공유하고 관리할 수 있다.

apps

apps/home.py

from flask import Blueprint

home = Blueprint("home", __name__, url_prefix="/")

@home.route("/", methods=["GET"])
def get_home():
    """
    메인 화면
    """
    return {"message": "hello"}, 200

apps/misc.py

from settings import WOKEUP_TIME
from utils.timer import calc_uptime
from flask import Blueprint

misc = Blueprint("misc", __name__, url_prefix="/misc")

@misc.route("/uptime", methods=["GET"])
def get_uptime():
    return {"uptime": calc_uptime(WOKEUP_TIME)}, 200

Blueprint로 라우팅을 모듈화 할 수 있다.
기존에 app.route로 관리하던걸 home.route, misc.route와 같이 별도 관리하고 다른 모듈에서 가져다 사용할 수 있도록 작성 가능하다.

apps/__init__.py

from settings import *
from flask import Flask
from flask_cors import CORS
from utils.env import get_database_uri
from . import home, misc
from flask_pymongo import PyMongo
from flask_oauthlib.client import OAuth

oauth = OAuth()
google = oauth.remote_app( ... )

app = Flask(__name__)
app.secret_key = urandom(12)
app.config["MONGO_URI"] = get_database_uri(DATABASES)
mongo = PyMongo(app)
app.register_blueprint(home.home)
app.register_blueprint(misc.misc)
CORS(app, support_credentials=True)

앱 설정, 데이터베이스 초기화, 라우팅 기능들을 한곳에서 관리할 수 있다.

새로운 구조로 변경하고 발생한 이슈

별도 분리하면서 순환 참조 문제를 겪었는데 apps에서 app을 다시 import하는 경우가 생기기 때문이었다.

순환참조 이슈 발생 예시

apps/home.py

from apps import google

@home.route("/login")
def get_login():
    if session.get("auth"):
        return redirect(url_for("home.get_home"))
    return google.authorize(callback=url_for("home.callback", _external=True))

이런 Blueprint 구조에서 apps/__init__.py에 작성해둔 google 객체가되던 뭐가되던간에 참조하는경우 라우팅 영역을 벗어난 전역범위에서 import 할 경우 순환참조 오류를 보게된다.

Traceback (most recent call last):
  File "app.py", line 1, in <module>
    from apps import app
  File "../flask-skeleton/apps/__init__.py", line 8, in <module>
    from . import home, misc
  File "../flask-skeleton/apps/home.py", line 3, in <module>
    from apps import google
ImportError: cannot import name 'google' from partially initialized module 'apps' (most likely due to a circular import) (../flask-skeleton/apps/__init__.py)
apps/__init__.py
├─ app = Flask(__name__)
├─ google = oauth.remote_app(...)
├─ ...
└─ app.register_blueprint(home.home)

순환참조 이슈가 수정된 예시

apps/home.py

@home.route("/login")
def get_login():
    from apps import google
    if session.get("auth"):
        return redirect(url_for("home.get_home"))
    return google.authorize(callback=url_for("home.callback", _external=True))

결국 route 영역 안에서 import해서 사용하여 해결했지만 utils 혹은 database 모듈로 분리하여 사용하거나 구조를 조금 더 손봐야 할 것 같다는 생각을 하였다.

신경써서 사용한다면 그렇게 크리티컬한 이슈는 아닌 것 같아서 이대로 써도 괜찮다고 판단하였다.

결론

기존 방식을 버리고 Blueprint 컨셉을 적용하여 새로 생성한 구조로 DB연동이나 서드파티 미들웨어도 붙이고 API 서버도 띄워보고 이런저런 작업을 해볼 수 있었다.

  • 각 기능별로 코드를 분리하니 관리나 협업이 편했다.
  • 의도하진 않았지만 누군가에게 작업을 맡길 때도 해당 기능만 구현하는데 집중하게 할 수 있었다.
    • 작업 지시가 좀 더 수월했다
  • 패키지 순환참조 이슈를 겪어보았다.
    • route 영역 안에서 패키지를 불러오면 된다.
    • 그래도 이정도면 쓸만하다고 판단하여 Flask를 사용하는 경우 종종 이런 구조를 사용할 것 같다.

작업했던 코드의 템플릿을 공유하기위해 Git repo를 생성하였다.
https://github.com/dev4hobby/flask-skeleton

후일담

Application Factory Pattern 을 사용하면 더 좋은 구조가 만들어질 것 같다.

https://flask-docs-kr.readthedocs.io/ko/latest/patterns/appfactories.html

주말에 할 일이 늘었다
작업예정..

profile
웹 백엔드 합니다.
post-custom-banner

4개의 댓글

comment-user-thumbnail
2021년 11월 7일

장고에 비해 자유로운 구조 설계는 플라스크의 장점 중 하나라고 생각합니다.

https://github.com/gureuso/flask

저 같은 경우는 위 처럼 해결했는데 지금 돌이켜 보면 비즈니스로직도 분리할 수 있지 않을까 싶네요.

1개의 답글
comment-user-thumbnail
2021년 11월 8일

플라스크의 애플리케이션 팩토리 패턴과 플라스크 2.0의 뷰 어노테이션, 도메인 주위의 디렉토리에 대해 고민해보시면 더 도움되실거 같습니다!

1개의 답글