인생은 고통의 연속

지옥의 Flask... 본문

프로그래밍/Flask

지옥의 Flask...

gnidoc 2020. 12. 22. 22:17
    반응형

    1년 반만에 쓰는 첫 글이 안좋은 소리부터 시작해서 좀 그렇지만

    당장 떠오르는 이 분노를 정리해야겠다

     


    Flask 소개

    일단 Flask 란 무엇인가? 공식 홈페이지를 보면 이렇다

    # 출처 - palletsprojects.com/p/flask/
    Flask is a lightweight WSGI web application framework
    그렇다! 가.볍.게 쓸 수 있는 웹 프레임워크이다

    업계에서도 가장 많이 쓰고 python 개발툴인 pycharm 유료 버전에서
    Django와 함께 공식 지원되는 웹프레임워크 중 하나이다

    www.jetbrains.com/ko-kr/pycharm/features/web_development.html

     

    Full-stack Web Development - Features | PyCharm

    Python and Django IDE with refactorings, code completion, on-the-fly code analysis and coding productivity orientation

    www.jetbrains.com

     

     


    awesome flask!

    프레임워크별로 awesome 한 라이브러리를 정리해둔 레포들이 있다
    flask도 마찬가지로 존재하는데 아래 링크와 같다

    github.com/humiaozuzu/awesome-flask

     

    humiaozuzu/awesome-flask

    A curated list of awesome Flask resources and plugins - humiaozuzu/awesome-flask

    github.com

     

    난 주로 flask + sqlalchemy + flask-restless 를 통해서 DB Table 단위로 CRUD를 제공하는 API를 만들때 사용했었는데
    아래 링크와 같이 코드 몇 줄이면 API를 쓸 수 있다
    flask-restless.readthedocs.io/en/stable/quickstart.html

     

    Quickstart — Flask-Restless 0.17.0 documentation

    Quickstart For the restless: 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 45 46import flask import flask.ext.sqlalchemy import flask.ext.restless # Create the Flask application a

    flask-restless.readthedocs.io

     

    그 외에도 Task Queue로 쓸 수 있게 celery 와 integration을 제공하거나
    Zappa 와 같이 AWS Lambda에서도 Flask가 되게끔 Serverless Application 지원하거나
    AWS S3에 에셋들을 관리하기 쉽도록 지원해주는 flask-s3 등 다양한 라이브러리 등이 있다

     

     


    문제점

    일단 가.볍.게 쓸 수 있는 프레임워크다보니 flask 자체엔 기능이 그렇게 많진 않다

    개인적으로 경험해본 결과, 아래와 같은 문제점이 가장 크게 와닿았다

    1. 버전관리(유지보수)
    2. docs(공식문서)
    3. Integration
    4. Ecosystem(Extensions)

    사실 4개의 문제가 합쳐져서 복합적으로 문제를 발생시키고 있는데 자세하게 적어보자면...


    1. 버전관리(유지보수)

    일단 버전부터 얘길해보자면

    무려 10년 전에 발표된 프레임워크지만 이제야 1.1.2버전이고
    첫 1.x인 1.0.1버전은 18년 4월 30일에 릴리즈되었다.

    버전업에 대해서 매우 부담감을 느끼는지 0.x 버전을 엄청 오래 유지했고
    릴리즈도 1년에 한번 정도 되는데
    옆동네인 Django는 한달에 2~3개의 릴리즈가 있는거와 비교하면 참이상하다

    그리고 그마저도 최근 change log를 보면 버전별로 크게 바뀌진 않지만 릴리즈 기간이 매우 긴걸 볼 수 있다
    더 이상 기능을 추가하기보단 bug fix와 같이 정말 유지보수만 하고 있는 상태이고
    python 3.5 이하 버전에 대해서 지원을 중단하고 deprecate 된 것들을 제거하는게 목적으로 보인다

    따라서 사실상 뭔가 기능이 필요하면 다음 버전을 기대하기 보단 Extension을 무조건 사용해야되는 상황이다
    (그래도 다행인건 풀리퀘는 잘받아주는듯하다)

    https://flask.palletsprojects.com/en/1.1.x/changelog/#version-1-1-x


    2. docs(공식문서)

    공식 문서의 서두를 보면 flask는 micro framework라고 나와있다. 여기서 micro의 의미란 아래와 같은데

    “Micro” does not mean that your whole web application has to fit into a single Python file (although it certainly can), nor does it mean that Flask is lacking in functionality

    나와있는대로 정말 기본적인 기능은 제공해주기 때문에 기능적으로 부족한건 아니다
    문제는 0.x -> 1.x 버전으로 넘어가면서 많은점이 바뀌었기 때문에 이를 문서에서 설명을 해줘야되는데
    예전에 쓰던 사람이 1.x버전을 쓰면 좀 난감한 점들이 몇가지 있다

    일단 Django나 flask 둘 다 auto reload 기능을 지원해서
    python 코드를 고칠때마다 굳이 서버를 재시작할 필요없이 개발이 가능하다
    이는 특히 디버깅할때 강점인데,
    나처럼 pycharm을 유료로 쓰는 경우에는 기본으로 제공되는 디버그 템플릿을 쓰면 되지만
    무료로 pycharm을 쓰는 경우에는 제공되지 않아서 억지로 되게 끔 써야되는데 방식이 2가지가 있다

    하나는 아래 링크와 같이 run/debug configurations에서 venv에 들어있는 flask script(module)를 직접 호출해서 쓰는 방법이 있고

    flask.palletsprojects.com/en/1.1.x/cli/#pycharm-integration

     

    Command Line Interface — Flask Documentation (1.1.x)

    The flask command is implemented using Click. See that project’s documentation for full information about writing commands. This example adds the command create-user that takes the argument name. This example adds the same command, but as user create, a

    flask.palletsprojects.com

    두번째는 매우 간단하게 in code에서 flask app을 직접 호출하는 방식이 있다

    from flask import Flask
    app = Flask(__name__)
    
    @app.route('/')
    def hello_world():
        return 'Hello, World!'
        
    if __name__ == '__main__':
        app.run()    

     

    첫번째 방법은 공식적으로 지원되지 않기 때문에 문제가 발생하면 해결할 수 없는데
    두번째는 공식적으로 지원을 하고 있다
    하지만.....

    공식문서에 따르면, 아래와 같이 In Code 방식으로 사용시
    개발 환경에서 문제가 있거나 auto reload가 잘 안될 수 있다고 한다

    https://flask.palletsprojects.com/en/1.1.x/server/#in-code

    flask method 사용이 강제가 되야한다는 이런 중요한 내용이 한참 뒤에 나온다

    난 이걸 직접 당했는데, pytest를 통한 테스트코드 작성시 위의 In Code 방식으로 인해서 문제가 발생했었는데
    구체적인 상황은 아래 문서와 같이 pytest를 통해서 flask API를 테스트하는 상황이었다
    flask.palletsprojects.com/en/1.1.x/testing/

     

    Testing Flask Applications — Flask Documentation (1.1.x)

    The origin of this quote is unknown and while it is not entirely correct, it is also not far from the truth. Untested applications make it hard to improve existing code and developers of untested applications tend to become pretty paranoid. If an applicati

    flask.palletsprojects.com

    In Code방식의 문제를 인지하지 못한채로 pytest와 flask.client fixture를 사용하여 flask API를 테스트하려고 했는데
    app에 테스트용 config가 적용되지 않고 client가 없다는 에러가 계속 발생하였다.

    위 문서에서 코드만 가져와서 보면, fixture로 정의한 client가 넘어오지 않는다는 문제였다

    @pytest.fixture
    def client():
        db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp()
        flaskr.app.config['TESTING'] = True
    
        with flaskr.app.test_client() as client:
            with flaskr.app.app_context():
                flaskr.init_db()
            yield client
    
        os.close(db_fd)
        os.unlink(flaskr.app.config['DATABASE'])
        
    
    def test_empty_db(client):
        """Start with a blank database."""
    
        rv = client.get('/')
        assert b'No entries here so far' in rv.data

    데코레이터로 인자를 넘겨주지 않고 pytest에서 알아서 client 값을 넘겨주다보니
    pytest로 직접 코드를 실행하지 않고는 뭐가 문제인지 알 수 없었고
    실제로 pytest 코드를 직접 접근해서 확인하기에는 flask + pytest 모두 확인해야되기 때문에 파악하기 쉽지 않았다

    물론 app.test_client()를 매번 호출해서 client를 대체해서 써도 되지만,
    이런 Boilerplate code 를 최대한 지양하려고 flask를 택했던거라 쉽게 결단이 서지 않았다.

    결국, 고민 끝에 In Code 방식을 버리고 Application Factory 패턴으로 변경하니 슬프게도 잘되었고
    pytest를 도입하기 위해서 난 create_app 에 갇혀버리게 되었다(이후로 create_app이라고 통일하겠다)

    그리고 create_app은 flask ecosystem에서 지옥을 맛보여주게 되는데...

    그리고 large application에 대한 문서도 있지만 구체적으로 어떻게 쓰면 된다란 예시가 없었다
    application factories를 보면 대충 예측은 가능하지만
    python package별로 config, route, model 등을 나누는 구체적인 방식은 나와있지 않았다
    뭐 결국 LGTM(looks good to me)이면 된다지만 기본적인 가이드가 없다는게 좀 불편했다
    (심지어 알아서 쓰다보면 또 귀찮은 경우가 있음)

    마지막으로 config도 문제가 있었는데, In Code 방식에서는 아래와 같이 port, host 등이 변경이 가능했다
    그래서 FLASK_ENV만 다르게 해서 config.py에 개발/스테이징/테스트/운영을 분리해서 사용할 수 있었다

    # In Code 방식에선 이렇게 사용
    
    # config.py
    class Dev:
    	port = 8000
        
    class Prod:
        port = 9000
    
    # main.py
    if FLASK_ENV='dev':
        app.config.from_object('config.Dev')
    elif FLASK_ENV='prod':
        app.config.from_object('config.Prod')
    else:
    	print(f'FLASK_ENV = {FLASK_ENV}')
    	exit(1)
    
    if __name__ == '__main__':
        app.run(host="localhost", port=app.config.port)

    하지만 create_app 을 사용하면서 코드에서 값을 가져와서 port를 변경할 수 없고 환경변수로만 가능했다
    이런 것들이 쌓이다보니 결국 환경별로 다르게 실행될 수 있도록 별도의 실행 스크립트와 문서 작성이 필요해졌다

    더 길어질까봐 다 쓰진 않았지만(fabric, setup, module, multi application 등등)
    이렇게 뭔가 기능이 강제화되지만 기본 가이드(문서)에 명시가 안되거나 직접해봐야 알 수 있는 문제가 있어서
    어떤 선택을 해야되는 경우엔 매우 골치아픈 경우가 있었다
    (나 같은 경우엔 In Code 방식의 선택이었다)


    3. Integration

    4번에서도 해당되는 문제인데,
    flask 자체는 routes, template(Jinja)과 같은 웹 기본 기능만 있기 때문에
    test, swagger UI, user, login, CORS 등등이 필요하다면 직접 구현하거나 extension의 힘을 빌려야된다
    이를 4번에서 모두 언급하자면...


    4. Ecosystem(Extensions)

    내 생각에는 지금 flask는 extension 때문에 흥했었고 extension 때문에 망했다고 본다
    아래는 순수 경험담이다...

     

    - 유지관리관련

    일단 위에 awsome flask에 가서 extension의 github 을 가보면 대게 이런 상황이다

    https://github.com/jfinkels/flask-restless
    https://github.com/noirbizarre/flask-restplus
    https://github.com/mattupstate/flask-security

    extension 들이 더 이상 유지보수 상태가 아니다!!

    셋 다 github star 가 1k가 넘는데 현재는 관리가 안되고 있다...(심지어 아직도 사용하는 사람이 많다)
    물론 READMD에 써있는대로 fork하여 다른 사람이 관리하고 있지만
    과연 처음 코드를 짠 당사자보다 책임감있게 할 수 있을까...?

     

    - Integration 1

    난 이 생각을 별로 유심히 하지 않고 아래와 같이 섞어서 썼다(사실 flask-user를 쓸때 감이 왔는데...)
    이 프로젝트에서 flask는 단순히 백엔드용 API 서버라서 별도의 프론트는 따로 있었다

    • flask-restx(flask-restplus forked) : swagger UI로 사용
    • flask-security-too(flask-security forked) : 유저, 권한(role), 회원 정보(가입/탈퇴) 관리
    • flask-login : 로그인 처리 관련(security와 integration)
    • flask-WTF : Form/csrf 관련(security와 integration)
    • sqlachemy : 나중에 독립적으로 DB에 대한 스크립트를 실행하기 위해서 flask extension을 사용하지 않음
    • flask-admin : admin 페이지
    • flask-CORS : cors 처리(security와 integration)
    • flask-accepts : input validation

    여기서 봐야될 특징은
    restx에선 oauth2만 지원되고 security는 integration 이 안된다
    admin도 마찬가지라서 admin/restx/security 는 서로 integration이 없다

    그러다보니 swagger에서 로그인 버튼이나 role별로 API 노출을 통제해야되는데
    이를 직접 구현하거나 별도로 문서화가 필요했다

    그래서 로그인페이지를 백엔드/프론트엔드 양쪽에 구현을 해야되는데
    flask에는 cors관련 설정이 없기 때문에 이를 위해서 flask-cors를 추가해야됐고 여기까진 다행히도 적용가능했다
    그리고 flask-login이 flask-security에서 integration되기 때문에 로그인/회원가입/탈퇴/비밀번호 변경까지 적용했다

    마지막으로 flask-restx에서 swagger 화면에 로그인시 role 별로 권한 관리가 되는지를 확인했는데...
    결국 integration 되지 않는 다수의 extension을 사용한 죄를 받게 되었다

    일단 발생했던 문제는 flask-restx에도 있는 이슈이다
    github.com/python-restx/flask-restx/issues/96

     

    CSRF token support in Swagger UI is broken · Issue #96 · python-restx/flask-restx

    I'm using flask-jwt-extended for JWT handling and enabled CSRF protection. Code app.py from flask_restx import Api, Resource from flask import Flask, Blueprint from flask_jwt_extended import JW...

    github.com

    401 Error: UNAUTHORIZED
    Response body:
    { "msg": "Missing CSRF token" }

    flask-security-too + flask-wtf가 연동되고서 CSRF_PROTECT = True 로 켜지니
    flask-restx에서 제공하는 swagger에서 login 후에 login이 필요한 API를 호출하면 위와 같이 401에러가 발생했다

    위의 문제가 원본인 flask-restplus에는 PR된 상태인데 유지보수가 안되고 있기 때문에 PR이 반영되지 않았고
    flask-restx에서는 모두 그 PR만을 기다리고 있는 상태였다

    하지만 flask-restx에 이슈는 오늘만 121개다... 이걸 하나하나 기다려주진 않을거 같고
    csrf token 문제라서 이건 직접 PR해볼려고 한다
    휴가 중에 이걸 보고 있는 내가 참 그렇지만... ㅠㅠ

     

    - Integration 2

    그리고 restx-swagger에서 좀 더 편하게 input validation을 하기 위해서 flask-accepts를 썼는데
    당연 한국사람이다보니 query string에서 한글을 처리하기 위해서 urlencode가 필요했고
    key값이 중복되는 경우가 있어서 서버에서는 이걸 무시하지 않고 모두 읽어서 리스트로 처리해야될 필요가 있었는데
    pytest에서 아래와 같은 코드를 썼다

    # https://docs.python.org/ko/3/library/urllib.parse.html#urllib.parse.urlencode
    from urllib import parse
    
    query = [('key', 'value'), ('key', '한글')]
    parse.urlencode(query, encoding='UTF-8', doseq=True) #key=value&key=%ED%95%9C%EA%B8%80

    당연히 테스트는 통과되고 내가 짠코드도 문제가 없고 정상적으로 동작하는데
    flask-restx의 swagger에서는 doseq 옵션이 없다...
    그래서 프론트엔드쪽에서 doseq가 필요한 API를 swagger에서 테스트할때 안되거나 이상한 값이 나온다는 것이다
    즉, flask-restx/accepts/pytest 모두 정상적으로 동작하지만 swagger UI에서만 지원이 되지 않는다는 것이다

    코드야 key가 다르게끔 바꾸면 된다지만 굳이 작업이 한번 더 필요했고
    마지막에 flask ecosystem에 대한 신뢰성이 와장창 깨졌다

     

    - Integration 3

    위에서 언급된 create_app 관련 내용이다.

    In Code 방식을 쓰면 장점이 아래처럼 extension에서 간단하게 app을 맘대로 가져다 쓸 수 있다는 것이다

    from flask import Flask
    from flask_cors import CORS
    
    app = Flask(__name__)
    CORS(app)
    
    @app.route("/")
    def helloWorld():
      return "Hello, cross-origin-world!"

    문제는 create_app 방식은 create_app을 통해서만 app이 반환되므로 구현이 달라지는데
    아래와 같이 local import를 해야 순환 참조 문제에서 벗어나면서 extension이 app에 대한 의존성이 떨어진다

    # cors.py
    cors = CORS()
    
    # flaskr/__init__.py
    def create_app():
        app = Flask(__name__)
        
        # local import
        from cors import cors
        cors.init_app(app)
        return app

    그렇지만 모든 extensions에서 cors처럼 init_app method를 모두 제공하지 않는다
    그리고 init_app이 제대로 안되는 녀석들도 있는데 대표적인게 flask-user, flask-security-too, flask-login, flask-cors 이다

    처음엔 flask-user + flask-login으로 관리하려고 했는데 아무리해봐도 flask-user가 제대로 동작을 안했다
    처음엔 내가 잘못쓰는건가 했는데 이제와서 보니 그건 아니었고...

    그래서 flask-user를 대체할 수 있는게 있나해서 알아본게 flask-security-too였다.
    forked 된거지만 flask-user보다 기능이 더 많았고 flask-login과 integration되서 좋았는데...

    init_app할때 args로 넘긴 녀석들을 제대로 인식을 못했다
    일단 예제코드에서 주석으로 문제가 발생한 부분을 나타내면 아래와 같다

    # auth.py
    # User, Role은 sqlalchemy model table
    user_datastore = SQLAlchemySessionUserDatastore(session, User, Role)
    security = Security(datastore=user_datastore,
                         register_form=CustomRegisterForm)
    login_manager = LoginManager()
    csrf_protect = CSRFProtect()
    cors = CORS() 
     
    # flaskr/__init__.py
    def create_app()
        app = Flask(__name__)
        
        csrf_protect.init_app(app)
        
        # 문제1
        security.init_app(app)
        
        # 문제2
        login_manager.init_app(app)
        login_manager.login_view = '/security/login'
    
        # 문제3
        cors.init_app(app,
                       support_credentials=True,
                       resources='/*',
                       allow_headers='*',
                       origins='*',
                       expose_headers='Authorization,Content-Type,Authentication-Token,XSRF-TOKEN')
    
    
    • 문제1
      • 공식 문서를 참고했을땐 security.init_app(app, datastore) 이렇게 들어가야되는데
        실제론 security 객체 생성할때 datastore를 넣어줘야함
      • 원인은 security에 flask-sqlalchemy가 아니고 sqlalchemy를 사용해서
        db session이 먼저 초기화 되기 전에 datastore가 불려서 DB연결은 없는데 app + datastore 가 연동되서 발생
      • 위의 코드는 datastore를 먼저 생성해서 db session을 먼저 초기화할 수 있도록 순서를 바꾼 상태
        (위의 맨 첫줄 session이 최초로 불려질때 db session이 생성됨)
    • 문제2
      • 1과 비슷한 문제인데 app에 아직 security 관련 초기화가 진행되지 않아서 LoginManager 객체 생성시 login_view를 추가해도 적용되지 않고 security.init_app이 호출될때 overwrite 되버렸음
      • 그래서 init_app 이후에 login_view를 overwrite함
    • 문제3
      • 2와 같은 문제로 cors 설정을 overwrite 하기 위해서 create_app에서 초기화
      • flask-security-too에서 SPA를 지원하기 위해선 필수인 설정들

     

    - flask-restx 이슈

    flask-restx 를 쓰다보니 발견했는데 default namespace가 안지워진다...
    원본인 flask-restplus에는 반영됐지만 restx에선 되지 않는다

    github.com/noirbizarre/flask-restplus/issues/460

     

    Remove default namespace? · Issue #460 · noirbizarre/flask-restplus

    This issue has confused me for several days, and I still cannot find a solution to resolve it. Just wanted to see if somebody here has some bright ideas. For example, I make a namespace called '...

    github.com

    그리고 models를 re-design 한다고 한다.
    flask-accepts를 도입했던 이유가 여기에 해당하는데 개인적으로 매우 빨리 적용됐으면 하는 이슈이다

    github.com/python-restx/flask-restx/issues/59

     

    Flask-RESTX Models Re-Design · Issue #59 · python-restx/flask-restx

    For quite some time there have been significant issues around data models, request parsing and response marshalling in flask-restx (carried over from flask-restplus). The most obvious of which is t...

    github.com

     

    - flask-login 이슈

    github.com/Flask-Middleware/flask-security/issues/385

     

    Proposal: flask_login should be vendored into flask_security and removed as a dependency · Issue #385 · Flask-Middleware/flask

    On my work we have had more than a few issues with both packages and I would like to share with you that I think keeping flask_login as a dependency is not in the best interest of clarity. Both pro...

    github.com

    기껏 flask-security랑 flask-login 섞어서 만들어놨더니 이런 얘기도 오간다...

    사실 login에서 제공하는 login_required랑 security에서 제공하는 auth_required 데코레이터 모두 동일하게 동작해서
    솔직히 쓰면서 flask-login 없어도 될거 같은데 굳이...란 생각이 들긴했다
    로그인 페이지 구현하는게 어려운 것도 아니고 WTF에서 form도 제공해주는데....


    결론

    사실상 더 이상 flask를 쓸 필요/이유가 없다

    기능이 필요할때마다 extension을 모두 검토할 것도 일이고
    막상 괜찮네!하고 썼다가 다른 extension과 충돌나서 직접 코드까보면서 이슈해결하는건 너무 리소스 낭비다
    그렇다고 매번 필요한 기능을 직접 구현하는것도 귀찮은 일이고...

    Django도 뭐 같네하면서 욕했는데 flask를 다시 써보니 Django가 선녀같다...

    물론 Django라고 ecosystem이 잘 유지되는건 아닌거 같다

    대표적으로 swagger를 생성해주는 drf-yasg도 최신버전의 Django만 지원하고
    django-rest-swagger도 drf-yasg에게 역할을 넘긴 상태다

    Django 자체도 버전이 바뀌면 directory나 setting 값이 바뀌기 때문에 쉽게 레거시화 되고 있다

    대신 장점은 web에서 필요할만한 기능은 기본적으로 탑재되어있다는 점...?

    web에 대해서는 따로 추가해본적은 없고 django model로 배치 스크립트를 짜려고 RunScript extension 정도는 써봤다

    그래도 불편하긴 한데, 굳이 flask vs Django 라면 Django를 쓰는 것을 추천한다

    정말... 정말 DB에 API만 만들어서 쓰겠다면 flask가 좋겠지만
    jwt, 인증, 세션, 캐시 등등이 필요해질 예정이라면 절대 flask는 안쓰는게 좋을거 같다.

    반응형
    Comments