본문 바로가기

데이터 분석/Flask

Flask - 패키지 구조화

728x90

2021.03.27 - [분류 전체보기] - Flask - SQLAlchemy(ORM)

이전 포스트 이후의 내용을 다룹니다


이전까지와 같이 모든 기능을 하나의 파일에서 구현한다면

기능이 많아질수록 코드가 복잡해지고, 그에 따라 의사소통 문제가 발생할 가능성이 높아집니다.

 

직접 만든 코드도 시간이 지난 뒤에 보면 이 코드가 어떤 작동을 하는지 주석과 기억을 더듬어서 이해하는데

다른 개발자들이 내 코드를 본다면 더욱 이해하기 힘들겠죠?

그래서 코드의 기능에 따라 파일을 나누고, 개발자가 알아보기 쉽게만들기 위해

패키지 구조화(Package Structure)를 해줍니다.

 

우리는 이전까지 만들던 Flask_app을 구조화 해보겠습니다.

1. 먼저 메인 스크립트인 Flask_app.py를 기능에 따라 나눠주겠습니다.

from datetime import datetime
from flask import Flask, render_template, url_for, flash, redirect
from flask_sqlalchemy import SQLAlchemy
from forms import RegistrationForm, LoginForm

# forms.py 에서 Form 클래스들을 호출

app = Flask(__name__) 

app.config['SECRET_KEY'] = '5791628bb0b13ce0c676dfde280ba245'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'
db = SQLAlchemy(app)

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(20), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    image_file = db.Column(db.String(20), nullable=False, default='default.jpg')
    password = db.Column(db.String(60), nullable=False)
    posts = db.relationship('Post', backref='author', lazy=True)

    def __repr__(self):
        return f"User('{self.username}', '{self.email}', '{self.image_file}')"

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), nullable=False)
    date_posted = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
    content = db.Column(db.Text, nullable=False)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)

    def __repr__(self):
        return f"Post('{self.title}', '{self.date_posted}')"
        



post1 = [
    {
        'author': 'Sooho Kim',
        'title': 'Post 1',
        'content': 'First post content',
        'date_posted': 'March 23, 2021'
    },
    {
        'author': 'Jane Doe',
        'title': 'Post 2',
        'content': 'Second post content',
        'date_posted': 'March 25, 2021'
    }
]


@app.route('/')
@app.route('/home')
def home():
    return render_template('home.html', posts=post1)

@app.route('/about')
def about():
    return render_template('about.html', title='About')


@app.route('/register', methods=['GET', 'POST'])
# register 페이지에서 발생하는 함수, 이 페이지에서는 get과 post 명령을 사용할 수 있음
def register():
    # register 페이지에 RegistrationForm() 클래스가 작동하게 만들어줌
    form = RegistrationForm()
    # 여기의 폼은 forms.py에서 선언한 Regist~~form이고,
    if form.validate_on_submit():
    # 만약 제출한 폼이 유효성 검사에 통과했다면 아래의 코드를 실행한다.
        flash(f'Account created for {form.username.data}!', 'success')
        return redirect(url_for('home'))
        # 유저 등록이 성공적으로 될시에 위와같은 일회성 문구를 전송하고, 홈페이지로 돌아갑니다.
    return render_template('register.html', title='Register', form=form)



@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
    # 만약 제출한 폼이 유효성 검사에 통과했다면 아래의 코드를 실행한다.
        if form.email.data == 'admin@blog.com' and form.password.data == 'password':
        # 데이터 베이스를 얻기 전까지 임시적인 방법입니다.
        # 만약 admin@blog.com으로 로그인 양식을 제출하고, password에 'password'를 입력한다면
            flash('You have been logged in!', 'success')
            return redirect(url_for('home'))
        else:
            flash('Login Unsuccessful. Please check username and password', 'danger')
    return render_template('login.html', title='Login', form=form)
# login 페이지에 LoginForm() 클래스가 작동하게 만들어줌


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

메인 스크립트는 크게 4가지 역할을 했습니다.

1) app을 선언하고, 데이터베이스를 생성하는 기능

2) User와 Post 등 모델을 선언하는 기능

3) 페이지마다 표시할 내용을 설정해준 기능

4) app 실행 기능

역할을 나누는 기준은 개발자마다 다를 수 있습니다.

 

위 네가지 기능을 __init__.py, models.py, routes.py, run.py 파일로 나눠준 후, run.py파일을 제외하고

나머지 파일들은 flaskapp 이라는 폴더안에 넣어서 모두 하나의 패키지로 담아주겠습니다.

 

1-1) __init__.py

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__) 

app.config['SECRET_KEY'] = 'e89733e7694e28f0c3e4d79de2268d70'
# app에서 2가지 폼을 사용하기 위해 시크릿 키를 사용합니다.
# 시크릿 키는 쿠키 수정(cookie modify)과, 교차 사이트 요청 위조과 같은 간단한 해킹을 막아줍니다.
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'
# ///은 상대경로이고, 위의 코드상 프로젝트 디렉토리에 site.db가 생성됩니다.

db = SQLAlchemy(app)
# 데이터베이스 인스턴스 생성

from flaskapp import routes
# impoort 순환오류를 방지하기 위해 아래에서 import

메인스크립트에서 app을 선언하고, 데이터베이스를 생성하는 기능을 구현한 코드를 가져왔고, 

이 코드에서 필요한 Flask와 SQLAlchemy 를 import 하는 코드도 같이 가져왔습니다.

 

마지막 from flaskapp import routes 코드는 위에서 생성한 flaskapp 패키지안에 있는

routes.py 파일을 import 해온것입니다.

이 코드가 제일 마지막에 있는건 import 순환오류 때문인데 이는 마지막에 다시 설명하겠습니다.

1-2) models.py

from datetime import datetime
from flaskapp import db
# __main__에서 가져올 필요없이 flaskapp에서 가져올 수 있다.

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True) # int형 고유키 선언
    username = db.Column(db.String(20), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    # 유저 이름과 이메일은 글자수 제한(20, 120)이 있으며, 유니크하며, notnull 공백이 없습니다.
    image_file = db.Column(db.String(20), nullable=False, default='default.jpg')
    # 유저의 프로필 사진으로 20자 길이의 해시값으로 이미지를 나타낸것입니다.
    # 기본사진을 사용할 수 있으므로 유니크는 삭제하고, 기본값을 설정해주겠습니다.
    password = db.Column(db.String(60), nullable=False)
    # 비밀번호 또한 60자의 해시값으로 나타낼것이고, 공백은 없습니다
    posts = db.relationship('Post', backref='author', lazy=True)
    # 한 유저가 여러 게시물을 작성할 수 는 있지만 한 게시물에는 하나의 작성자만 있을 수 있기때문에 backref로 관계설정
    # lazy 파라미터는 데이터 베이스를 불러오는 시기를 정할 수 있습니다. True일때는 필요한 데이터를 한번에 로드합니다.

    # 객체를 사용자가 이해할 수 있는 문자열로 반환하는 함수
    # https://shoark7.github.io/programming/python/difference-between-__repr__-vs-__str__
    def __repr__(self):
        return f"User('{self.username}', '{self.email}', '{self.image_file}')"

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), nullable=False)
    date_posted = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
    # 게시물이 작성된 날짜를 담는 열로 기본값은 datetime.utcnow로 뒤에 괄호를 붙이지 않은 이유는
    # 현재 시간이 아닌 데이터베이스에 저장할 때의 시간을 유지하기 위해서 입니다.
    content = db.Column(db.Text, nullable=False)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    # 사용자 아이디를 외래키로 설정
    # 관계를 설정할때는 클래스의 이름을 Post 대문자 그대로 표현했지만 외래키를 설정할때는 user.id로 소문자로 바꿔서 표현했다.
    # 이는 외래키를 설정할때는 tablename을 가져와서, 관계를 설정할때는 클래스를 가져와서 대소문자가 바뀌는것이다.
    # 참고로 tablename을 따로 설정하지 않으면 class 이름에서 모두 소문자인 형태가 tablename이 된다.

    def __repr__(self):
        return f"Post('{self.title}', '{self.date_posted}')"

메인스크립트에서 User와 Post 등 모델을 선언하는 기능을 가져왔습니다.

이 코드에서 필요한 datetime를 import하는 코드를 가져왔고,

flaskapp패키지 중 __init__.py파일에 있는 db도 import 했습니다.

1-3) routes.py

from flask import render_template, flash, redirect, url_for
from flaskapp import app
from flaskapp.forms import RegistrationForm, LoginForm
from flaskapp.models import User, Post
# flaskapp 패키지에 모두 옮겼기 때문에 flaskapp.forms, flaskapp.models가 된다.

post1 = [
    {
        'author': 'Sooho Kim',
        'title': 'Post 1',
        'content': 'First post content',
        'date_posted': 'March 23, 2021'
    },
    {
        'author': 'Jane Doe',
        'title': 'Post 2',
        'content': 'Second post content',
        'date_posted': 'March 25, 2021'
    }
]

@app.route('/')
@app.route('/home')
def home():
    return render_template('home.html', posts=post1)

@app.route('/about')
def about():
    return render_template('about.html', title='About')

@app.route('/register', methods=['GET', 'POST'])
# register 페이지에서 발생하는 함수, 이 페이지에서는 get과 post 명령을 사용할 수 있음
def register():
    # register 페이지에 RegistrationForm() 클래스가 작동하게 만들어줌
    form = RegistrationForm()
    # 여기의 폼은 forms.py에서 선언한 Regist~~form이고,
    if form.validate_on_submit():
    # 만약 제출한 폼이 유효성 검사에 통과했다면 아래의 코드를 실행한다.
        flash(f'Account created for {form.username.data}!', 'success')
        return redirect(url_for('home'))
        # 유저 등록이 성공적으로 될시에 위와같은 일회성 문구를 전송하고, 홈페이지로 돌아갑니다.
    return render_template('register.html', title='Register', form=form)

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
    # 만약 제출한 폼이 유효성 검사에 통과했다면 아래의 코드를 실행한다.
        if form.email.data == 'admin@blog.com' and form.password.data == 'password':
        # 데이터 베이스를 얻기 전까지 임시적인 방법입니다.
        # 만약 admin@blog.com으로 로그인 양식을 제출하고, password에 'password'를 입력한다면
            flash('You have been logged in!', 'success')
            return redirect(url_for('home'))
        else:
            flash('Login Unsuccessful. Please check username and password', 'danger')
    return render_template('login.html', title='Login', form=form)
# login 페이지에 LoginForm() 클래스가 작동하게 만들어줌

flaskapp 패키지에서 app을 import해 왔고,

기존에 from forms import RegistrationForm, LoginForm 였던 코드를

from flaskapp.forms import RegistrationForm, LoginForm 코드로 바꿨습니다.

이제 forms.py 파일이 flaskapp패키지 안으로 옮기면서 경로도 바뀐것입니다.

1-4) run.py

from flaskapp import app

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

app을 실행하는 역할을 하는 코드로 이 파일은 패키지 밖에 둬야합니다.

 

1-5) import 순환오류

앞서 __init__.py에서 from flaskapp import routes를 마지막에 둔 이유를 설명하겠습니다.

 

만약 import를 위에 뒀다면

# on __init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flaskapp import routes

app = Flask(__name__) 

app.config['SECRET_KEY'] = 'e89733e7694e28f0c3e4d79de2268d70'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'

db = SQLAlchemy(app)

# on routes.py
from flask import render_template, flash, redirect, url_for
from flaskapp import app
from flaskapp.forms import RegistrationForm, LoginForm
from flaskapp.models import User, Post

# on models.py

from datetime import datetime
from flaskapp import db

class User(db.Model):

1. run.py 파일 실행을 위해 app을 import하면 __init__으로 가게됩니다.

2. init 파일에서는 routes를 불러와야합니다.

3. routes를 부르면 models에서 user와 post를 불러와야 합니다.

4. models로 가보면 db를 불러와야합니다.

 

그러기 위해선 다시 2번 상황으로 가게되고 2-3-4 상황이 계속 반복됩니다.

이를 해결하고자 db 데이터 뒤에 routes를 import하게 했습니다.

 

패키지 설정을 끝낸 후 db를 다시 생성해주면 flaskapp 패키지 안에 site.db 파일이 생기게 되고,

당연히 그 데이터는 비어있습니다.

$ python
Python 3.7.10 (default, Feb 26 2021, 13:06:18) [MSC v.1916 64 bit (AMD64)] :: Anaconda, Inc. on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> from flaskapp import db
C:\Users\ksh\.conda\envs\pjt327\lib\site-packages\flask_sqlalchemy\__init__.py:873: FSADeprecationWarning: SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and will be disabled by default in the future.  Set it to True or False to suppress this warning.
  'SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and '
>>> from flaskapp.models import User, Post
>>> db.create_all()
>>> User.query.all()
[]

'데이터 분석 > Flask' 카테고리의 다른 글

JSON (JavaScript Object Notation)  (0) 2021.03.30
Flask - SQLAlchemy(ORM)  (0) 2021.03.27
HTTP  (0) 2021.03.26
Flask - 2. Forms and User Input  (0) 2021.03.25
Flask - 1. 기본설정, 템플릿, 부트스트랩  (0) 2021.03.23