책 '점프 투 플라스크'를 공부하면서 정리한 내용입니다.
출처 : https://wikidocs.net/book/4542
앞으로 만들 파이보 프로젝트의 전체 구조
├── pybo/
│ ├─ __init__.py
│ ├─ models.py
│ ├─ forms.py
│ ├─ views/
│ │ └─ main_views.py
│ ├─ static/
│ │ └─ style.css
│ └─ templates/
│ └─ index.html
└── config.py
파이보 프로젝트는 ORM(object relational mapping)을 지원하는 파이썬 데이터베이스 도구인 SQLAlchemy를 사용한다. SQLAlchemy는 모델 기반으로 DB를 처리한다. 따라서 모델 클래스들을 정의할 models.py 파일이 필요하다
웹 브라우저에서 서버로 전송된 폼을 처리할 때 WTForms라는 라이브러리를 사용한다. WTForms 역시 모델 기반으로 폼을 처리하기 때문에 폼 클래스를 정의할 forms.py 파일이 필요하다.
pybo.py 파일에 작성한 hello_pybo 함수의 역할은 화면 구성이었다. views 디렉토리에 바로 이런 함수들이 작성된 여러 가지 뷰 파일을 저장한다.
스타일시트(.css), 자바스크립트(.js), 이미지 파일(.jpg, .png)등을 저장
파이보의 질문 목록 조회, 질문 상세 조회 등의 HTML 파일 저장.
파이보 프로젝트의 환경변수, 데이터베이스 등의 설정을 저장
앞서 살펴본 플라스크 앱(Flask 클래스로 만든 객체)
app = Flask(__name__)
플라스크는 app 객체를 사용해 여러 가지 설정. but 이런 방식으로 app 객체를 전역으로 사용하면 프로젝트 규모가 커질수록 순환 참조(circular import) 오류와 같은 문제가 발생할 확률이 높아진다.
순환 참조 : A 모듈이 B 모듈을 참조. B 모듈이 다시 A 모듈 참조.
app 객체를 전역으로 사용할 때 발생하는 문제를 예방하기 위해 애플리케이션 팩토리(app 객체를 생성하는 함수)를 사용한다.
myproject/pybo 디렉토리 만든 후 move 명령어로 변경
(myproject) c:\projects\myproject> mkdir pybo
(myproject) c:\projects\myproject> move pybo.py pybo/__init__.py
주의 ! flask run은 꼭 C:/projects/myproject 디렉터리에서 실행해야한다.
__init__.py 파일을 열고 create_app 함수가 app 객체를 생성해 반환하도록 수정
from flask import Flask
def create_app():
app = Flask(__name__)
@app.route('/')
def hello_pybo():
return 'Hello, Pybo!'
return app
app 객체가 함수 안에서 사용되므로 hello_pybo 함수를 create_app 함수 안에 포함했다. 여기서 사용된 create_app 함수가 애플리케이션 팩토리이다.
실습 도중 flask.cli.NoAppException 오류가 발생했다. move 명령을 할 때에 \하나를 빠느려 생긴 오류였다. 간단한 실수였지만 찾기까지 오래걸렸다. 주의해야겠다.
create_app 함수 안 hello_pybo 함수는 URL에서 /에 매핑되는 함수인데 그 매핑을 @app.route('/')라는 어노테이션이 만들어준다. 이 어노테이션으로 매핑되는 함수가 라우트 함수이다.
새로운 URL이 생길 때 라우트 함수를 create_app 함수 안에 계속 추가해야 하는 불편함을 블루프린트(Blueprint) 클래스로 해결한다.
플라스크의 블루프린트를 이용하면 라우트 함수를 구조적으로 관리할 수 있다.
블루프린트는 URL과 호출되는 함수의 관계를 확인할 수 있는 Blueprint 클래스이다.
view 디렉토리 만든 후 main_views.py 파일 생성
(myproject) c:\projects\myproject> cd pybo
(myproject) c:\projects\myproject\pybo> mkdir views
main_views.py 파일 생성
from flask import Blueprint
bp = Blueprint('main', __name__, url_prefix='/')
@bp.route('/')
def hello_pybo():
return 'Hello, Pybo!'
hello_pybo 함수를 어노테이션이 @app.route가 아닌 @bp.route로 변경해서 작성한 것이다. @bp.route에서 bp는 Blueprint 클래스로 생성한 객체이다. Bluprint 클래스로 객체를 생성할 때에는 이름, 모듈명, URL 프리픽스(url_prefix)값을 전달해야 한다.
블루프린트 객체의 이름인 main은 나중에 함수명으로 URL을 찾아주는 url_for 함수에서 사용할 예정이다.
URL 프리픽스틑 접두어 URL을 정할 때 사용한다. 특정 파일(main_views.py)에 있는 함수의 어노테이션 URL 앞에 기본으로 붙일 접두어 URL을 의미한다. 예를 들어 url_prefix='/main'이라면 hello_pybo 함수를 호출하는 URl은 localhost:5000/이 아니라 localhost:5000/main/이 된다.
블루프린트 파일을 적용하기 위해 __init__.py 파일을 수정한다.
from flask import Flask
def create_app():
app = Flask(__name__)
from .views import main_views
app.register_blueprint(main_views.bp)
return app
hello_pybo 함수 대신 블루프린트를 사용하도록 변경. 블루프린트를 사용하려면 main_views.py 파일에서 생성한 블루프린트 객체인 bp를 등록하면 된다.
main_views.py 파일을 열어 hello_pybo 함수의 URL 매핑을 /에서 /hello로 바꾸고, index 함수를 추가해 URL 매핑을 /로 입력
from flask import Blueprint
bp = Blueprint('main', __name__, url_prefix='/')
@bp.route('/hello')
def hello_pybo():
return 'Hello, Pybo!'
@bp.route('/')
def index():
return 'Pybo index'
localhost:5000과 localhost:5000/hello에 접속
주의 ! myproject 디렉토리로 이동한 후 flask run을 실행해야 한다.
localhost:5000 : 라우터 /에 매핑된 index 함수 호출됨
localhost:5000/hello : 라우터 /hello에 매핑된 hello_pybo 함수 호출됨
파이보는 질문 답변 게시판이다. 질문이나 답변을 작성하면 데이터가 생성됨으로 데이터를 저장, 조회, 수정 등의 기능을 구현하는 데이터베이스를 사용해야 한다.
DB를 사용하려면 SQL 쿼리라는 구조화된 질의를 작성하고 실행하는 등 복잡한 과정이 필요하다. 이때 ORM(object relational mapping)을 이용하면 파이썬 문법만으로 쿼리를 직접 작성하지 않고 DB의 데이터를 처리할 수 있다.
question 테이블 구성
쿼리
insert into question (subject, content) values ('안녕하세요', '가입 인사드립니다 ^^');
insert into question (subject, content) values ('질문 있습니다', 'ORM이 궁금합니다');
ORM
question1 = Question(subject=’안녕하세요’, content='가입 인사드립니다 ^^')
db.session.add(question1)
question2 = Question(subject=’질문 있습니다’, content='ORM이 궁금합니다')
db.session.add(question2)
Question은 파이썬 클래스이고, 이처럼 데이터를 관리하는 데 사용하는 ORM 클래스를 모델이라고 한다. 모델을 사용하면 내부에서 SQL 쿼리를 자동으로 생성해준다.
ORM을 이용하면 DB 종류에 상관 없이 일관된 코드를 유지할 수 있어서 프로그램을 유지보수하기 편리하고, 내부에서 자동으로 안전한 sQL 쿼리를 생성해주기 때문에 개발자가 달라도 통일된 쿼리를 작성할 수 있다.
ORM 라이브러리 중 SQLAlchemy를 가장 많이 사용한다. 또한 파이썬 모델을 이용해 테이블을 생성하고 컬럼을 추가하는 등의 작업을 하는 Flask-Migrate 라이브러리도 사용한다.
myproject 가상 환경에서 Flask-Migrate 라이브러리를 설치
(myproject) c:\projects\myproject>pip install Flask-Migrate
config.py라는 설정 파일을 통해 파이보에 ORM 적용
루트 디렉토리에 config.py 파일 생성
import os
BASE_DIR = os.path.dirname(__file__)
SQLALCHEMY_DATABASE_URI = 'sqlite:///{}'.format(os.path.join(BASE_DIR, 'pybo.db'))
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_DATABASE_URI : 데이터베이스 접속 주소SQLALCHEMY_TRACK_MODIFICATIONS : SQLAlchemy의 이벤트를 처리하는 옵션
필요하지 않은 옵션들은 False
pybo.db라는 DB 파일을 프로젝트의 루트 디렉토리에 저장
SQLite DB : 소규모 프로젝트에 사용하는 가벼운 파일을 기반으로 한 DB. SQLite로 개발을 빠르게 진행 후 실제 운영 시스템에 반영할 때 큰 규모 DB 사용
__init__.py 파일 수정해 SQLAlchemy 적용
from flask import Flask
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
import config
db = SQLAlchemy()
migrate = Migrate()
def create_app():
app = Flask(__name__)
# config.py 파일에 작성한 항목을 app.config 환경 변수로 부르기 위해
app.config.from_object(config)
# ORM
db.init_app(app)
migrate.init_app(app, db)
# 블루프린트
from .views import main_views
app.register_blueprint(main_views.bp)
return app
전역 변수로 db, migrate 객체를 만든 다음 create_app 함수 안에서 init_app 메서드를 이용해 초기화
db 객체를 create_app 함수 안에서 생성하면 블루프린트와 같은 다른 모듈에서 불러올 수 없다. 따라서 db, migrate와 같은 객체를 create_app 함수 밖에서 생성하고, 실제 객체 초기화는 create_app 함수에서 수행한다.
flask db init 명령으로 DB 초기화
(myproject) c:\projects\myproject>flask db init
DB를 관리하는 초기 파일들을 migrations라는 디렉토리에 자동 생성. 이때 생성되는 파일들은 Flask-Migrate 라이브러리에서 사용.
초기화 명령은 최초 한 번만 필요.
DB 관리 명령어
flask db migrate : 모델을 새로 생성 or 변경
flask db upgrade : 모델의 변경 내용을 실제 DB에 적용
질문과 답변에 해당하는 모델(데이터를 다룰 목적으로 만든 파이썬 클래스) 필요
질문 모델
id : 질문 데이터의 고유 번호
subject : 질문 제목
content : 질문 내용
create_date : 질문 작성일시
답변 모델
id : 답변 데이터의 고유 번호
question_id : 질문 데이터의 고유 번호
content : 답변 내용
create_date : 답변 작성일시
pybo 디렉터리에 models.py 파일을 생성하고 질문 모델인 Question 클래스를 정의
from pybo import db
class Question(db.Model):
id = db.Column(db.Integer, primary_key=True)
subject = db.Column(db.String(200), nullable=False)
content = db.Column(db.Text(), nullable=False)
create_date = db.Column(db.DateTime(), nullable=False)
Question 클래스는 모든 모델의 기본 클래스인 db.Model 상속받았다. (이 때 db는 init 파일에서 생성한 SQLAlchemy 객체). 각 속성은 db.Column 클래스 사용하여 생성한다.
db.Column 첫 옵션 : 데이터 타입(필수 옵션으로 속성에 저장할 데이터의 종류 결정)
db.Integer : 고유 번호와 같은 숫자값
db.String : 제목처럼 글자 수가 제한된 텍스트
db.Text : 글 내용처럼 글자 수를 제한할 수 없는 텍스트
db.DateTime : 날짜와 시각
데이터 타입 외 여러 옵션 가능. 고유 번호(모델에서 각 데이터를 구분하는 유효한 값으로 중복 X) id에 primary_key 지정한다.
플라스크는 데이터 타입이 db.Integer이고 기본키로 지정한 속성은 값이 자동으로 증가. 데이터를 저장할 때 해당 속성값을 지정하지 않아도 1씩 자동으로 증가하여 저장
nullable 옵션 : 속성에 빈값을 허용할 것인이 결정. 지정하지 않으면 기본으로 빈값 허용. 빈값 허용하지 않으려면 nullable = False.
models.py 파일에 Answer 클래스
# 추가
class Answer(db.Model):
id = db.Column(db.Integer, primary_key=True)
question_id = db.Column(db.Integer, db.ForeignKey('question.id', ondelete='CASCADE'))
# 첫 번째 : 참조할 모델. 두 번째 backref에 지정한 값 : 역참조 설정
question = db.relationship('Question', backref=db.backref('answer_set'))
# 파이썬 코드로 연관 데이터 모두 삭제 바랄 경우
# question = db.relationship('Question', backref=db.backref('answer_set', cascade='all, delete-orphan'))
content = db.Column(db.Text(), nullable=False)
create_date = db.Column(db.DateTime(), nullable=False)
question_id 속성은 질문 모델과 연결하기 위해 추가. 질문 모델의 기본 키가 답변 모델의 외부키이다. 질문 모델과 연결하기 위해 db.ForeignKey를 이용한다.
ondelete='CASCADE' : 데이터베이스에서 쿼리를 이용하여 질문을 삭제하면 해당 질문에 달린 답변도 함께 삭제(삭제 연동 설정)
question 속성은 답변 모델에서 질문 모델을 참조하기 위해 추가.
ex) 제목 참조 : answer.question.subject (db.relationship을 사용한 경우에만 가능)
db.relationship에 지정한 첫 번째 값은 참조할 모델이고 두 번째 backref에 지정한 값은 역참조(질문에서 답변을 참조) 설정이다. 한 질문에 여러 개의 답변이 달릴 수 있기 때문에 역참조가 필요하다.
파이썬 코드를 이용해 질문 데이터 삭제 시 연관 답변 데이터 모두 삭제하는 방법
파이썬 코드 a_question.delete() 로 질문 데이터를 삭제하면 해당 질문과 연관된 답변 데이터는 삭제되지 않고 답변 데이터의 question_id 컬럼만 빈값으로 업데이트된다. 연관된 답변 데이터가 모두 삭제되게 하려면 db.backref 설정에 cascade='all, delete-orphan' 옵션을 추가해야 한다.
플라스크의 Migrate 기능을 이용해 데이터베이스 테이블을 생성
앞선 실습에서 생성한 모델들을 플라스크의 Migrate 기능이 인식할 수 있도록 pybo/__init__.py 파일을 수정
# (... 생략 ...)
# ORM
db.init_app(app)
migrate.init_app(app, db)
from . import models
# (... 생략 ...)
DB가 변경되도록 cmd에서 flask db migrate 명령
(myproject) c:\projects\myproject> flask db migrate
DB 변경을 처리할 리비전 파일이 생성된다.
(myproject) c:\projects\myproject> flask db upgrade
DB에 question과 answer 테이블 생성
pybo.db 파일이 생성된다. (SQLite 데이터베이스의 데이터 파일)
SQLite의 GUI 도구인 DB Browser for SQLite를 사용
https://sqlitebrowser.org/dl 에 접속 후 DB Broswer for SQLite(이하 DB 브라우저) 설치 파일(standard installer) 설치
(myproject) c:\projects\myproject>flask shell
>>> from pybo.models import Question, Answer
>>> from datetime import datetime
>>> q = Question(subject='pybo가 무엇인가요?', content='pybo에 대해서 알고 싶습니다.', create_date=datetime.now())
객체 q를 만들었다고 DB에 저장되는 것은 아니다. 저장하려면 SQLAlchemy의 db 객체를 사용해야 한다.
>>> from pybo import db
>>> db.session.add(q)
>>> db.session.commit()
신규 데이터를 저장할 때는 add 함수를 사용한 다음 commit 함수 실행
db.session : DB를 처리하기 위한 DB와 연결된 세션(접속된 상태). 세선을 통해 데이터를 저장, 수정, 삭제 했으면 반드시 db.sesstion.commit 함수로 커밋해야 한다.
db.session.rollback 함수로 수행한 작업을 취소할 수도 있다. 단, 커밋을 이미 한 경우에는 불가능하다.
id는 기본 키이고 데이터를 설정할 때 속성값이 자동으로 1씩 증가한다. 따라서 하나 더 생성한 후 id를 확인하면 2가 찍힌다.
>>> q = Question(subject='플라스크 모델 질문입니다.', content='id는 자동으로 생성되나요?', create_date=datetime.now())
>>> db.session.add(q)
>>> db.session.commit()
>>> q.id
2
>>> Question.query.all()
[<Question 1>, <Question 2>]
Question.query.all 함수는 Question 객체 리스트를 반환한다. 결과에서 보이는 숫자가 id 속성값이다.
>>> Question.query.filter(Question.id==1).all()
[<Question 1>]
filter 함수는 인자로 전달한 조건에 맞는 데이터를 모두 반환해준다.
id는 유일한 값이므로 filter 함수 대신 get 함수를 이용할 수도 있다. (리스트가 아닌 객체 1개만 반환)
>> Question.query.get(1) <Question 1>
>>> Question.query.filter(Question.subject.like('%플라스크%')).all()
[<Question 2>]
like 함수
플라스크%: ‘플라스크’로 시작하는 문자열
%플라스크: ‘플라스크’로 끝나는 문자열
%플라스크%: ‘플라스크’를 포함하는 문자열
ilike 함수 : 대소문자 구분 x
>>> q = Question.query.get(2)
>>> q
<Question 2>
>>> q.subject = 'Flask Model Question'
>>> db.session.commit()
수정 후 반드시 커밋을 해야 DB에 반영된다.
>>> q = Question.query.get(1)
>>> db.session.delete(q)
>>> db.session.commit()
# 확인 작업
>>> Question.query.all()
[<Question 2>]
>>> from datetime import datetime
>>> from pybo.models import Question, Answer
>>> from pybo import db
>>> q = Question.query.get(2)
>>> a = Answer(question=q, content='네 자동으로 생성됩니다.', create_date=datetime.now())
>>> db.session.add(a)
>>> db.session.commit()
우선 질문 데이터를 구해와서 q에 저장 후 Answer 모델의 question 속성에 대입해 답변 데이터를 생성한다. Answer 모델에는 어떤 질문에 해당하는 답변인지 연결할 목적으로 question_id속성을 만들었었다. Answer 모델의 객체를 생성할 때 question에 q를 대입하면 question_id에 값을 지정하지 않아도 자동으로 입력된다.
Answer도 id 소성이 기본 키이므로 값이 자동으로 생성된다.
>>> a.id
1
>>> a = Answer.query.get(1)
>>> a
<Answer 1>
Answer 모델의 question 속성 이용
>>> a.question
<Question 2>
Answer 모델의 question 속성에 역참조 설정 backref=db.backref(‘answer_set’)을 적용했던 것을 이용한다.
>>> q.answer_set
[<Answer 1>]
플라스크 셸에서 빠져 나오려면 <Ctrl+Z>를 누르고 <Enter>를 입력한다.