Flask에서 유효성 검사를 어떻게하면 좋을지 고민해보았습니다.

d3fau1t·2021년 10월 31일
0

개발환경구성

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

Flask Framework를 사용하여 API서버를 만들어보는 과정에서 HTTP 통신을 수행할 때, 전달되는 QueryParams 혹은 Body의 데이터를 어떻게 검증할 수 있을까 생각할 수 있었다.
서비스코드에서 제한하고있는 범위를 벗어난 사용자 정보가 들어올 경우 의도하지 않은 상황이 벌어질 수 있고, 예외처리가 안되어있을 경우 높은 확률로 서비스 장애가 발생할 수 있다.

유저가 항상 올바른 값을 줄 것이라는 생각은 배제하고 시작한다

위와 같은이유로 클라이언트로부터 전달되는 데이터의 유효성 검증을 위해 Flask-WTF 확장 패키지를 사용하게되었고 전달되는 데이터를 좀 더 안전하게 다룰 수 있기까지의 과정을 문서로 작성합니다.

파이썬 웹 개발환경에서 Form을 유연하게 생성하거나 유효성검사를 하기위한 목적으로 생성된 WTForms 패키지를 Flask에서 편하게 사용할 수 있도록 배포된 패키지이다

예시

API를 통해 사용자 정보를 받아야하는 상황에서 아래의 요구사항이 있다.

fieldtyperequireddescription
nameStringTrue이름
ageIntegerTrue나이
emailStringTrue이메일

이름과 나이는 필수로 입력되어야하며, 이메일은 아무래도 상관없다.

유효성 검사를 위한 기존의 방식

from email.utils import parseaddr

@app.route('/user', methods=["POST"])
def get_user():
    body = request.get_json()
    name = str(body.get("name"))
    age = body.get("age")
    email = str(body.get("email"))
    if (not name) or (not age):
        return {"error": "name or age is missing"}, 400
    try:
        age = int(age)
    except ValueError:
        return {"error": "age is not a number"}, 400
    email = parseaddr(email)[1]
    
    # 대략 여기서 유저정보를 저장하던 뭘하던 지지고 볶는
    # 서비스 로직이 들어가면 되지 않을까?

    response = {
        "name": name,
        "age": age,
        "email": email,
    }

    return response, 200

요구사항을보고 나름대로 예외처리 하면서 코드를 작성하고 실제 작동하는 것 까지 확인했지만 이 코드도 완벽하지 않을 수 있다.

만약 받아야하는 데이터가 많아지거나 조건이 까다로워지면 로직이 복잡해지고, 서비스코드에서 유효성검사를 위한 코드 라인 수가 늘어나버리니 정작 중요한 서비스 로직을 보기도 전에 머릿속이 복잡해질 것 같다.

유효성 검증을 위한 새로운 방식

Flask-WTF를 사용해보았는데 생각보다 괜찮다.
FlaskForm Class를 정의하고 View 영역 안에서 요청정보를 간편하게 검증할 수 있다.

  • 웹 페이지의 Form 데이터를 검증
  • API 요청시 Body나 QueryParam의 검증

위와 같은 상황에 따라 사용법이 조금 달라지는데, 예시로 다뤄보려고 한다.

Flask-WTF 설치

pip install Flask-WTF

Validation Class 생성

from flask_wtf import FlaskForm
from wtforms import StringField, IntegerField
from wtforms.validators import DataRequired

class MyValidator(FlaskForm):
    name = StringField("name", validators=[DataRequired()])
    age = IntegerField("age", validators=[DataRequired()])
    email = StringField("email")

아래에서 설명될 예시에서 유효성 검사시 공통으로 사용할 클래스를 생성해준다.

API 요청, GET

import json
from flask import request

@app.route("/user-with-wtf", methods=["GET"])
def get_user_with_wtf():
    body = MyValidator(request.args, meta={"csrf": False})
    if body.validate():
        return json.dumps(body.data), 200
    return json.dumps(body.errors), 400

API 요청, POST

import json
@app.route("/user-with-wtf", methods=["POST"])
def get_user_with_wtf():
    body = MyValidator(meta={"csrf": False})
    if body.validate():
        return json.dumps(body.data), 200
    return json.dumps(body.errors), 400

Form Action, Submit

클라이언트 영역까지 생성해줘야하기 때문에, API 서버를 만들 때보다 조금 더 번거롭다.

app.py: Form 생성 부분

from flask import render_template
@app.route("/lab", methods=["GET"])
def render_wtf_lab_page():
    return render_template("wtf-lab.html", form=MyValidator())

이전에 생성한 MyValidator 클래스를 form 인자로 전달하면 form을 동적으로 생성할 수 있다.

templates/wtf-lab.html

<html>
  <head>
    <title>wtf-lab</title>
  </head>
  <body>
    <form method="POST" action="/wtf/user-form-with-wtf">
      {{form.csrf_token}}
      {{form.name.label}} {{form.name(size=20)}}
      <br>
      {{form.age.label}} {{form.age}}
      <br>
      {{form.email.label}} {{form.email(size=128)}}
      <br>
      <input type="submit">
    </form>
  </body>
</html>

페이지에 접속할 때 Pre-flight 연결이 완료되면 클라이언트의 헤더에는 CSRF 토큰이 생성되는데, Form 데이터를 전달하는 사람이 데이터를 들고올 때 미리 줬던 토큰을 들고오는지 인증하기 위함이다.
결국 전송되는 데이터의 무결성 검증을 위해 CSRF 토큰을 전달하는 것으로 봐도 무관한데.. 자세한 내용은 이글의 주제와 벗어나기 때문에 링크로 대신한다.

introduction-to-csrf

app.py: Form 요청 받는 부분

import json
@app.route("/user-form-with-wtf", methods=["POST"])
def get_user_form_with_wtf():
    form = MyValidator(request.form)
    if form.validate_on_submit():
        return json.dumps(form.data), 200
    return json.dumps(form.errors), 400

어찌되던간에 위에서 만든 Form에서 데이터가 들어오는 경우 미리 만들어둔 MyValidator를 기준으로 데이터를 검증한다.
API 요청을 받을 때와 다르게 meta={"csrf": False} 는 사용하지 않는다.

결론

  • Flask-WTF를 사용하여 유효성 검증을 하면 얻을 수 있는 장점이 많다.
    • 받고싶은 데이터 형식을 미리 정의해두고 조건에 맞지 않는 데이터가 들어오면 거를 수 있다.
    • API 요청, Form 전송에 유연하게 대응할 수 있다.
    • 서비스 로직과 데이터 검증에 필요한 로직을 분리할 수 있다.
      • 서비스 로직에 좀 더 집중할 수 있다.

Flask를 꼭 써야하는 환경이라면 Flask-WTF는 정말 좋은 선택인 것 같다.

여담이지만 FastAPI는 이러한 기능이 자체적으로 내장되어있고 API 문서 자동생성, 타입정의 및 기타 부가기능이 더 강력하기 때문에 Flask를 사용하는 개발자들이 FastAPI로 마이그레이션 하고있지 않을까 생각이 든다.

작업하면서 작성된 코드는 Git Repo에 올려두었다.
apps/wtfLab.py
flask-skeleton/ PR

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

0개의 댓글