본문 바로가기

COMPUTER SCIENCE/PYTHON

[Python] 파이썬 웹 프레임워크 입맛대로 골라보기 (Django, Flask, FastAPI 비교)

업무를 진행하다보면 좀 더 효율적으로 빠르게 작업하거나 결과를 확인할 수 있는 툴을 만들고자 하는 니즈가 자주 있었다.
그 덕분에 백엔드 개발자가 아님에도 불구하고 웹 프레임워크를 경험할 일이 꽤나 빈번하게 발생했다.
주로 demo를 만들어야 할 때는 Django를, 간단한 기능을 만들 때는 Flask를 사용하게 되었는데,
최근 FastAPI도 많이 사용되는 것을 보고 한번 써봤더니 가장 쉽게 구축할 수 있었다.
이왕 이렇게 다양하게 쓰게 된다면, 각각 어떤 장단점이 있는건지 제대로 알고 필요에 따라 적절하게 선택해서 사용해 봐야겠다는 생각이 들었다. 따라서 이번 글에서는 아래 순서대로 Python 웹 프레임워크들에 대해 살펴보고자 한다.
(1) 대표 프레임워크(Django, Flask, FastAPI) 소개 및 기술적인 측면 비교
(2) 같은 주제별 예시 API 생성 실습
(3) 2번에서의 실습 코드에 대한 각 프레임워크별 성능 평가


각 웹 프레임워크 비교해보기

Django

Django documentation | Django documentation

The web framework for perfectionists with deadlines.

docs.djangoproject.com

Django는 공식 문서에서 "마감기한이 있는 완벽주의자를 위한 웹 프레임워크(The web framework for perfectionists with deadlines)" 로 소개하고 있다. 완전한 사용성을 위해 필요한 모든 기능을 갖췄다는 의미로 batteries-included library 로도 불리는 Django는 MVT(Model - View - Template) 아키텍처로 구성되어 있다.

  • Model Layer: 데이터 구조 저장
  • View Layer: 유저 요청 처리 및 반환
  • Template Layer: 사용자에게 표시할 화면 구조(html) 처리

Model Layer를 통해 내장 ORM(Object-Relational Mapping: 객체 모델과 관계형 dbms 테이블 간 맵핑 기법)을 제공하면서, 자동으로 관리자 인터페이스 등을 제공하여 이러한 기능들을 직접 구현할 필요 없이 이용할 수 있다는 장점이 있다.

Flask

https://flask.palletsprojects.com/en/3.0.x
Flask는 Django와 약간 반대되는 결의 특성을 띄고 있는데..
(공식문서 첫 페이지만 봐도 두 프레임워크의 특성이 직관적으로 느껴진다ㅎㅎ;)

Django는 웹 개발을 위해 필요할 것으로 예상되는 틀을 기본적으로 제공하는 반면,
Flask는 마이크로 웹 프레임워크의 형식으로 최소한의 기능만 기본적으로 제공하며 직접 유연하게 필요한 기능만 플러그인 형식으로 추가하게 된다.
공식문서에 따르면 아래 3가지 기술에 의존성을 띄고 있으며, 각각 다음과 같은 기능을 처리하고 있다.

  • Werkzeug WSGI(Web Server Gateway Interface) toolkit
    : 웹 서버 및 HTTP 요청 처리
  • Jinja template engine
    : html 등 마크업 언어에 동적 컨텐츠를 삽입할 수 있는 기능 제공
  • Click CLI toolkit
    : command line interface 구현 (ex: `flask run` 등)

처음 앱을 설정해줄 때 사용하는 Flask 인스턴스를 통해서는 위 의존성 기능에 따라 routing 시스템 및 template 엔진 등을 초기화하고,
해당 인스턴스를 통해 데코레이터를 통해 어떤 url로 routing 할 지 설정해주고 render_template을 통해 html 템플릿을 반환하게 된다.

from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello, World!'

@app.route('/run_example')
def run_api():
    return render_template('run_example.html')

if __name__ == '__main__':
    app.run(debug=True)

 

FastAPI

FastAPI

FastAPI framework, high performance, easy to learn, fast to code, ready for production

fastapi.tiangolo.com

마지막으로 FastAPI 는 이름에서와 같이 속도 측면에서 우위를 보이는 웹 프레임워크이다.
ASGI(Asynchronous Server Gateway Interface)를 통해 비동기를 가능하게 해줌으로써 기존 WSGI를 사용하던 프레임워크보다 빠른 성능을 보여주고 있다.
사용법은 Flask와 같이 간편하게 decorator로 라우팅 설정을 해주며, async를 통해 비동기 설정을 해줄 수 있다.

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def hello_world():
    return {"message": "Hello, World!"}

또한 아래와 같이 API에 대해 자동으로 문서를 생성해준다는 장점을 가지고 있다.

 

각 프레임워크별 특징 및 다운로드 수 비교

위 내용에 따라 프레임워크별 특징을 표로 비교해보자면 다음과 같다.

 출시연도특징ORM문서화 기능최신 출시 버전 (2024. 10 기준)
Django2005all-in-one 프레임워크자체 제공미제공5.1.2 (2024. 10)
Flask2010micro 프레임워크외부 라이브러리 이용미제공3.0.3 (2024. 4)
FastAPI2018비동기 지원외부 라이브러리 이용제공0.115.2 (2024. 10)

추가로 piptrends(https://piptrends.com/compare/Flask-vs-Django-vs-fastapi)를 통해 위 3가지 프레임워크를 비교해본 결과,
6개월 내 다운로드 수를 확인했을 때 Flask > fastapi > Django 순서로 많이 이용되고 있음을 알 수 있다.
평소 Django를 많이 사용하고 있었기에 Django 다운로드 수가 가장 적다는 점이 의외이기도 했고,
6개월 간의 추세라서 정확한 판단은 어렵지만 fastapi가 Django보다 10년 이상 늦게 출시되었음에도 불구하고 다운로드 수가 2배 이상 많은 것을 볼 수 있다.
(물론 Django는 이미 설치해두고 사용하는 케이스도 많을 것이라 이러한 점은 염두해두고 봐야할 것으로 생각된다..
+ 중간에 fastapi 다운로드 수가 과도하게 많은 것은 outlier로 봐야하지 않을까 추측된다 🧐)

 

예제로 비교해보기: URL → QR 코드 변환

한번 직접 같은 기능을 하는 API를 만들었을 때, 각각 구현하는 방법은 어떻게 되는지 살펴보고자 한다.
특정 url이 주어졌을 때 QR 코드로 변환해주는 간단한 API를 구현해보고 비교해보고자 한다.
(해당 예시 코드는 아래 Github repo에서도 확인할 수 있다.)

GitHub - heehehe/url2qrcode

Contribute to heehehe/url2qrcode development by creating an account on GitHub.

github.com

 

API 설계

API는 간단히 아래와 같이 url을 받으면 response_type에 따라 QR code를 반환하는 구조로 설계하였다.

  • GET /get_qr_code?url={url}&response_type={response_type}
    • url: QR code로 변환할 url 주소 (default: "")
    • response_type: 결과 반환 형식 (default: "json", choices: ["json", "html"])

예를 들어, response_type = "json" 이면
(ex: http://localhost:8000/get_qr_code?url=https://heehehe-ds.tistory.com&response_type=json)
아래와 같이 문자열 형식으로 결과가 나오는 것이고,

response_type = "html" 이면
(ex: http://localhost:8000/get_qr_code?url=https://heehehe-ds.tistory.com&response_type=html)
아래와 같이 QR code 사진이 반환되는 것이다.

작고 소중한 나의 블로그 QR

 

URL → QR code 변환 모듈 생성

모든 웹 프레임워크에서 URL → QR코드 변환은 공통적으로 필요할 것이기에, 공통으로 사용할 모듈 스크립트를 생성해준다.
이는 qrcode 패키지를 기반으로 url을 받았을 때 qr code 이미지 string으로 변환해주며,
html에 표시할 때는 <img src=""> 를 통해 이미지로 나타내줄 수 있다.

import qrcode
from io import BytesIO
import base64


def generate_qr_code(url: str) -> str:
    """ Generates QR code based on requested url
    :param url: URL to make QR code
    :return: QR code
    """

    if not url:
        return ""

    qr = qrcode.QRCode(
        version=1,
        error_correction=qrcode.constants.ERROR_CORRECT_L,
        box_size=10,
        border=4,
    )
    qr.add_data(url)
    qr.make(fit=True)

    img = qr.make_image(fill_color="black", back_color="white")

    buffered = BytesIO()
    img.save(buffered)
    img_str = base64.b64encode(buffered.getvalue()).decode()

    return f"data:image/png;base64,{img_str}"

 

Django

django는 MVT 구조에 맞춰 구성해주기 때문에, 초기 세팅이 다른 프레임워크보다 공수가 조금 더 들어가게 되었다.
먼저 프로젝트 및 앱을 생성해주면 아래와 같이 파일들이 생성된다.

# 프로젝트 생성
django-admin startproject url2qr .

.
├── manage.py
└── url2qr
    ├── __init__.py
    ├── asgi.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py
# 앱 생성
python3 manage.py startapp api

.
├── api
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── migrations
│   │   └── __init__.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── db.sqlite3
├── manage.py
└── url2qr
    ├── __init__.py
    ├── asgi.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py

 
추가로 아래와 같이 app에 대한 설정을 진행해주어 url 연결을 진행해준다.

url2qr/settings.py
url2qr/urls.py
# api/urls.py (신규 생성)

from django.contrib import admin
from django.urls import path
from . import views

urlpatterns = [
    path('', views.main),
]

 
마지막으로 template에 대한 설정을 위해 api/templates 경로를 생성해준 뒤 html 파일을 생성해주며,
settings.py 에 TEMPLATES 경로 설정도 진행해준다.

# api/templates/qr_code.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>QR Code Generator</title>
</head>
<body>
    {% if qr_code %}
        <img src="{{ qr_code }}" alt="QR Code">
    {% endif %}
</body>
</html>
url2qr/settings.py

이렇게 사전작업들을 진행해준 뒤.. 이제 본격적인 처리 로직을 views.py 에 추가해주면 끝이다!

from django.shortcuts import render
from django.http import JsonResponse
from .modules.generate_qr_code import generate_qr_code


def home(request):
    return JsonResponse({'status': 'succeed'})


def get_qr_code(request):
    url = request.GET.get("url", "")
    response_type = request.GET.get("response_type", "json")
    qr_code = generate_qr_code(url)
    context = {'qr_code': qr_code}
    if response_type == "html":
        return render(request, 'qr_code.html', context)

    return JsonResponse(context)

최종 API 실행은 아래와 같이 manage.py 파일을 이용하여 runserver로 진행해주게 된다.

python manage.py runserver

 

Flask

Flask는 Django에 비해 사전작업이 매우 간단하다.
우선 template 파일을 django와 동일하게 templates 경로를 생성하여 추가해준 뒤,
아래와 같이 app.py 파일을 만들어주면 된다!

from flask import Flask, request, jsonify, render_template
from modules.generate_qr_code import generate_qr_code

app = Flask(__name__)


@app.route('/')
def home():
    return jsonify({'status': 'succeed'})


@app.route('/get_qr_code')
def generate_qr():
    url = request.args.get('url', '')
    response_type = request.args.get('response_type', 'json')

    qr_code = generate_qr_code(url)

    if response_type == 'html':
        return render_template('qr_code.html', qr_code=qr_code)
    return jsonify({'qr_code': qr_code})


if __name__ == '__main__':
    app.run(debug=True, port=8001)

실행은 위의 app.py 파일을 실행해주면 되며, 부가적으로 port 파라미터를 통해 port 설정을 진행해줄 수 있다.

python app.py --port 8081

 

FastAPI

FastAPI도 Flask와 매우 유사한 방식으로 구현이 진행되지만,
html 페이지를 보여주기 위해 jinja2templates를 사용한다는 차이점이 있다.
아래와 같이 main.py 파일을 만들어준 뒤,

from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates
from modules.generate_qr_code import generate_qr_code

app = FastAPI()
templates = Jinja2Templates(directory="templates")


@app.get('/')
def home():
    return {'status': 'succeed'}


@app.get("/get_qr_code")
async def get_qr_code(request: Request, url="", response_type="json"):
    qr_code = generate_qr_code(url)
    context = {"qr_code": qr_code}

    if response_type == "html":
        context["request"] = request
        return templates.TemplateResponse("qr_code.html", context)
    return context

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8002)

실행은 uvicorn을 통해 진행해주게 된다.
Flask와 마찬가지로 python main.py 로 진행할 수도 있지만, uvicorn을 써야 ASGI 기반으로 비동기 처리가 진행되기 때문에 uvicorn 사용이 권장되고 있다.

uvicorn main:app --reload --port 8002

추가로 FastAPI가 보유하고 있는 장점으로 자동 문서화 기능이 있었는데,
이는 /docs 경로로 접근하면 아래와 같이 유용하게 확인할 수 있었다 👍

 

성능 비교 (feat. locust)

위와 같이 Django, Flask, FastAPI로 구현된 동일한 기능의 API에 대해 성능 비교를 해보고자 한다.
테스트는 python 기반의 성능 평가 도구인 locust를 기반으로 진행되었다.

Locust.io

An open source load testing tool. Define user behaviour with Python code, and swarm your system with millions of simultaneous users.

locust.io

테스트 코드는 다음과 같이 내 블로그 주소에 대해 random으로 추출하여 qr code를 생성하는 방식으로 구현했다.

from locust import HttpUser, task, between
import random


class QRCodeAPIUser(HttpUser):
    wait_time = between(1, 3)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.urls = [
            f"https://heehehe-ds.tistory.com/{i}" for i in range(1, 212)
        ]

    @task
    def generate_qr_code(self):
        url = random.choice(self.urls)
        print(url)
        with self.client.get(f"/get_qr_code?url={url}", catch_response=True) as response:
            if response.status_code == 200:
                response.success()
            else:
                response.failure(f"QR code generation failed: {response.status_code}")


def run():
    import os
    os.system("locust -f test_with_locust.py")

if __name__ == "__main__":
    run()

위 스크립트를 실행해주게 되면 localhost:8089 를 통해 locust 테스트 UI 페이지에 접근할 수 있는데,
테스트 사용자 수, ramp up 시간 (전체 사용자 수에 도달하기까지의 시간), host 주소, 테스트 Run time 까지 설정할 수 있다.

위와 같이 설정한 뒤 START를 눌러주게 되면,
아래와 같이 각 테스트 uri별 request 수 및 응답 시간이 표 형태로 나타나게 된다.

아래와 같이 CHARTS를 통해 시간 경과에 따른 RPS(Requests per Second), 응답 시간 및 테스트된 유저 수를 확인할 수 있다.
(아래 결과는 Run 순서대로 Django, Flask, FastAPI 가 테스트된 결과이다.)

평가 결과, FastAPI > Django > Flask 순서로 빠른 성능을 나타내는 것을 볼 수 있으며,

 RPS응답시간 (ms)
Django72129.03
Flask70.8127.91
FastAPI76.1122.94
  • (RPS 기준) FastAPI는 Django보다 약 5.7%, Flask보다 약 7.5% 더 많은 요청을 처리하며,
  • (응답시간 기준) FastAPI는 Django보다 약 4.7%, Flask보다 약 3.9% 더 빠른 응답 시간을 보이는 것을 확인할 수 있다.

 


이와 같이 Python의 대표 웹 프레임워크에 대해 기능적인 차이와 실제 구현할 때의 차이점 및 성능 평가까지 진행해 보았다.
Django는 복잡한 구조의 데모를 구현해야 할 때, Flask는 간단한 데모를 구현할 때, 마지막으로 FastAPI는 빠른 성능이 요구되는 API를 구현할 때 사용하면 좋을 것으로 보인다. (다만 FastAPI는 아직 버전이 0.xx라는 점에서 불안정하다는 견해도 있다)
위의 필요한 상황에 따라 적절한 프레임워크를 선택해서 유용하게 사용해보면 좋을 것 같다 :)
 

반응형