app.py 하나가 편할 때와 불편해질 때
처음에는 app.py 하나가 가장 편합니다. 파일을 열면 Flask 앱 생성, 라우트, 폼 처리, 데이터베이스 코드가 한눈에 보입니다. 초보 단계에서는 구조를 예쁘게 나누는 것보다 "서버를 실행해 보고, 화면이 뜨고, 데이터가 저장되는 경험"이 더 중요합니다.
하지만 지난 장까지 만든 게시판 앱을 떠올려 봅시다. 이제 app.py 안에는 다음 코드가 함께 들어 있습니다.
Flask(__name__)로 앱을 만드는 코드/,/posts,/posts/<int:post_id>,/posts/new같은 라우트폼 입력값 검증 코드
SQLite 연결 코드
게시글 조회와 저장 SQL
오류 처리와 리다이렉트 코드
기능이 적을 때는 괜찮습니다. 문제는 기능이 늘어날 때입니다. 회원가입, 로그인, 댓글, REST API까지 추가하면 한 파일이 너무 길어집니다. 코드를 고치려면 매번 긴 파일을 위아래로 훑어야 합니다. 데이터베이스 연결만 수정하고 싶은데 라우트 코드 사이를 헤매게 됩니다.
프로젝트 구조를 정리한다는 것은 코드를 보기 좋게 꾸미는 일이 아닙니다. 역할이 다른 코드를 서로 다른 자리로 보내는 일입니다. 주방에 냄비, 칼, 접시를 따로 두는 것과 같습니다. 물건의 위치가 정해져 있으면 요리 속도가 빨라지고 실수도 줄어듭니다.
AI 생성 이미지
Flask 프로젝트를 패키지로 만들기
Flask 공식 튜토리얼은 작은 앱을 패키지 구조로 확장할 때 애플리케이션 코드가 들어갈 폴더를 만들고, 그 안에 __init__.py를 두는 방식을 사용합니다.1 우리 예제에서는 패키지 이름을 board로 하겠습니다.
현재 프로젝트를 다음처럼 정리해 봅시다.
flask-study/
├─ board/
│ ├─ __init__.py
│ ├─ db.py
│ ├─ routes.py
│ ├─ templates/
│ │ ├─ base.html
│ │ ├─ index.html
│ │ ├─ posts.html
│ │ ├─ post_detail.html
│ │ └─ post_create.html
│ └─ static/
│ └─ style.css
├─ instance/
│ ├─ board.sqlite3
│ └─ config.py
└─ .venv/여기서 board/는 파이썬 패키지입니다. 패키지는 관련된 파이썬 파일을 묶어 놓은 폴더입니다. 단순한 폴더와 다른 점은 __init__.py가 있다는 것입니다.
__init__.py는 두 가지 역할을 합니다.
첫째, 파이썬에게 "이 폴더는 import할 수 있는 패키지입니다"라고 알려 줍니다. 그래서 from board import ... 같은 코드가 가능해집니다.
둘째, Flask 앱을 만드는 시작 지점이 됩니다. 앞으로는 app.py에서 바로 앱을 만들지 않고, board/__init__.py 안의 create_app() 함수에서 앱을 만들겠습니다.
instance/ 폴더도 중요합니다. 이 폴더에는 공개 저장소에 올리면 안 되는 파일을 둡니다. 예를 들어 실제 운영용 비밀 키, 로컬 설정 파일, SQLite 데이터베이스 파일이 여기에 들어갈 수 있습니다. 코드와 개인 설정을 분리하는 자리라고 생각하면 됩니다.
지난 장에서 데이터베이스 파일을 프로젝트 루트의 board.sqlite3로 두었다면, 이번 장부터는 instance/board.sqlite3로 옮겨 관리하겠습니다. 데이터가 중요하지 않은 연습용이라면 새로 초기화해도 괜찮습니다.
애플리케이션 팩토리란 무엇인가
팩토리(factory)는 공장이라는 뜻입니다. 애플리케이션 팩토리는 Flask 앱을 찍어 내는 함수입니다. 보통 이름을 create_app으로 짓습니다.
지금까지는 이렇게 앱을 만들었습니다.
from flask import Flask
app = Flask(__name__)이 방식은 파일이 import되는 순간 앱이 바로 만들어집니다. 작은 앱에서는 단순해서 좋습니다. 그러나 테스트용 앱과 개발용 앱을 따로 만들거나, 설정을 바꿔서 실행해야 할 때는 불편합니다.
애플리케이션 팩토리 패턴은 앱 생성을 함수 안으로 넣습니다. Flask 공식 문서에서도 앱 객체를 전역에서 바로 만들지 않고 함수 안에서 만들면 서로 다른 설정을 가진 앱 인스턴스를 만들 수 있고, 테스트에도 유리하다고 설명합니다.2
board/__init__.py를 만들어 다음처럼 작성합니다.
import os
from flask import Flask
def create_app(test_config=None):
app = Flask(__name__, instance_relative_config=True)
app.config.from_mapping(
SECRET_KEY='dev',
DATABASE=os.path.join(app.instance_path, 'board.sqlite3'),
)
if test_config is None:
app.config.from_pyfile('config.py', silent=True)
else:
app.config.from_mapping(test_config)
os.makedirs(app.instance_path, exist_ok=True)
from . import db
db.init_app(app)
from . import routes
routes.init_app(app)
return app한 줄씩 핵심만 보겠습니다.
app = Flask(__name__, instance_relative_config=True)는 Flask 앱을 만듭니다. instance_relative_config=True는 설정 파일을 instance/ 폴더 기준으로 읽기 쉽게 해 줍니다.
app.config.from_mapping(...)은 기본 설정값을 등록합니다. 여기서는 개발 중 사용할 SECRET_KEY와 SQLite 데이터베이스 경로를 넣었습니다.
SECRET_KEY는 세션과 flash 메시지 같은 기능에서 데이터를 안전하게 서명할 때 사용됩니다. 지금은 학습용으로 'dev'를 넣었지만, 실제 서비스에서는 절대 이렇게 단순한 값을 쓰면 안 됩니다. 운영 환경에서는 길고 예측하기 어려운 비밀 키를 instance/config.py나 환경 변수로 따로 관리해야 합니다.
DATABASE=os.path.join(app.instance_path, 'board.sqlite3')는 데이터베이스 파일을 instance/board.sqlite3에 만들겠다는 뜻입니다. app.instance_path는 Flask가 관리하는 instance 폴더의 실제 경로입니다.
app.config.from_pyfile('config.py', silent=True)는 instance/config.py 파일이 있으면 읽고, 없어도 오류를 내지 않습니다. 그래서 처음 학습할 때는 config.py를 만들지 않아도 앱을 실행할 수 있습니다.
test_config는 나중에 테스트 코드를 작성할 때 유용합니다. 지금 당장 깊게 이해하지 않아도 됩니다. "필요하면 설정을 바꾼 앱을 새로 만들 수 있게 해 주는 자리"라고 생각하면 충분합니다.
데이터베이스 코드를 db.py로 옮기기
지난 장에서는 데이터베이스 연결 함수와 초기화 명령을 app.py에 두었습니다. 이제 그 역할을 board/db.py로 옮기겠습니다.
import sqlite3
import click
from flask import current_app, g
from flask.cli import with_appcontext
def get_db():
if 'db' not in g:
g.db = sqlite3.connect(current_app.config['DATABASE'])
g.db.row_factory = sqlite3.Row
return g.db
def close_db(error=None):
db = g.pop('db', None)
if db is not None:
db.close()
def init_db():
db = get_db()
db.execute('''
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
author_email TEXT NOT NULL,
content TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
)
''')
db.commit()
@click.command('init-db')
@with_appcontext
def init_db_command():
init_db()
click.echo('Initialized the database.')
def init_app(app):
app.teardown_appcontext(close_db)
app.cli.add_command(init_db_command)여기서 새로 등장한 것이 current_app입니다. 애플리케이션 팩토리를 사용하면 db.py 안에 전역 app 객체가 없습니다. 대신 요청이나 명령이 실행되는 동안 Flask가 "현재 사용 중인 앱"을 알고 있는데, 그 앱을 가리키는 이름이 current_app입니다.
그래서 데이터베이스 경로도 이렇게 읽습니다.
current_app.config['DATABASE']init_app(app) 함수도 중요합니다. 이 함수는 db.py가 Flask 앱에 자기 기능을 등록하는 입구입니다.
app.teardown_appcontext(close_db)
app.cli.add_command(init_db_command)첫 줄은 앱 작업이 끝날 때 데이터베이스 연결을 닫도록 등록합니다. 둘째 줄은 터미널에서 init-db 명령을 사용할 수 있게 등록합니다.
라우트 코드를 routes.py로 옮기기
이제 화면과 URL을 담당하는 코드를 board/routes.py로 옮기겠습니다. 아직 Blueprint까지 쓰지는 않겠습니다. Blueprint는 다음에 다룰 기능별 분리 도구이고, 이번 장에서는 먼저 app.py 하나를 __init__.py, db.py, routes.py로 나누는 흐름에 집중합니다.
from flask import abort, flash, redirect, render_template, request, url_for
from .db import get_db
def validate_post(form):
errors = {}
if not form['title']:
errors['title'] = '제목을 입력하세요.'
if not form['author_email']:
errors['author_email'] = '이메일을 입력하세요.'
if not form['content']:
errors['content'] = '내용을 입력하세요.'
return errors
def init_app(app):
@app.route('/')
def index():
return render_template('index.html')
@app.route('/posts')
def post_list():
db = get_db()
posts = db.execute('''
SELECT id, title, author_email, created_at
FROM posts
ORDER BY id DESC
''').fetchall()
return render_template('posts.html', posts=posts)
@app.route('/posts/<int:post_id>')
def post_detail(post_id):
db = get_db()
post = db.execute('''
SELECT id, title, author_email, content, created_at
FROM posts
WHERE id = ?
''', (post_id,)).fetchone()
if post is None:
abort(404)
return render_template('post_detail.html', post=post)
@app.route('/posts/create', methods=['GET', 'POST'])
def post_create():
if request.method == 'POST':
form = {
'title': request.form.get('title', '').strip(),
'author_email': request.form.get('author_email', '').strip(),
'content': request.form.get('content', '').strip(),
}
errors = validate_post(form)
if errors:
flash('입력 내용을 다시 확인해 주세요.', 'error')
return render_template('post_create.html', form=form, errors=errors)
db = get_db()
db.execute('''
INSERT INTO posts (title, author_email, content)
VALUES (?, ?, ?)
''', (form['title'], form['author_email'], form['content']))
db.commit()
flash('게시글이 저장되었습니다.', 'success')
return redirect(url_for('post_list'))
form = {
'title': '',
'author_email': '',
'content': '',
}
return render_template('post_create.html', form=form, errors={})이 코드에서 눈여겨볼 부분은 init_app(app)입니다. routes.py도 db.py처럼 앱에 자기 기능을 등록하는 함수를 가집니다. create_app() 안에서 routes.init_app(app)을 호출하면, 그때 라우트들이 Flask 앱에 연결됩니다.
지난 장에서 글 작성 주소를 /posts/new로 만들었다면, 이번 장에서 /posts/create로 이름을 정리해도 됩니다. 주소 이름은 프로젝트마다 다를 수 있습니다. 중요한 것은 템플릿의 url_for()와 라우트 함수 이름을 함께 맞추는 것입니다. 예를 들어 글쓰기 링크는 이제 url_for('post_create')를 사용해야 합니다.
템플릿과 static 폴더의 위치
Flask는 기본적으로 애플리케이션 패키지 안의 templates/ 폴더와 static/ 폴더를 찾습니다. 이번 구조에서는 다음 위치입니다.
board/templates/
board/static/따라서 기존 템플릿 파일은 board/templates/ 안으로 옮기면 됩니다.
board/templates/
├─ base.html
├─ index.html
├─ posts.html
├─ post_detail.html
└─ post_create.html기존에 글 작성 템플릿 이름을 post_form.html로 만들었다면 그대로 사용해도 됩니다. 다만 이 장의 예제 코드에서는 post_create.html이라는 이름을 사용했습니다. 파일 이름을 바꾸었다면 render_template() 안의 이름도 반드시 함께 바꿔야 합니다.
CSS 파일은 board/static/style.css로 옮깁니다. 템플릿에서 이미 다음처럼 연결하고 있었다면 그대로 동작합니다.
<link rel='stylesheet' href='{{ url_for('static', filename='style.css') }}'>Flask는 static이라는 특별한 엔드포인트를 기본으로 제공하기 때문에, 정적 파일이 패키지 안으로 들어가도 url_for('static', filename='style.css') 형태를 계속 사용할 수 있습니다.
실행 명령도 바뀐다
이제 앱은 app.py가 아니라 board 패키지 안의 create_app()으로 만들어집니다. 그래서 실행할 때도 --app app 대신 --app board를 사용합니다.
먼저 데이터베이스를 초기화합니다.
flask --app board init-db그다음 개발 서버를 실행합니다.
flask --app board run --debug브라우저에서 다음 주소로 접속해 봅니다.
http://127.0.0.1:5000/postsflask --app board라고 쓰면 Flask는 board 패키지를 import하고, 그 안의 create_app() 함수를 찾아 앱을 만듭니다. 만약 사용 중인 환경에서 자동으로 찾지 못한다면 다음처럼 명시해도 됩니다.
flask --app board:create_app run --debug둘 중 하나가 동작하면 됩니다. 중요한 것은 이제 앱 생성 위치가 app.py가 아니라 board/__init__.py의 create_app()이라는 점입니다.
instance/config.py는 언제 쓸까
instance/config.py는 개인 설정을 따로 넣는 파일입니다. 예를 들어 다음처럼 작성할 수 있습니다.
SECRET_KEY = 'replace-this-with-a-random-secret-key'이 파일은 공개 저장소에 올리지 않는 것이 좋습니다. 특히 실제 서비스의 SECRET_KEY, 외부 API 키, 운영 데이터베이스 접속 정보처럼 노출되면 안 되는 값은 코드와 분리해야 합니다.
학습 중에는 SECRET_KEY='dev'만으로도 실습이 가능합니다. 하지만 "설정은 코드와 분리할 수 있다"는 감각은 일찍 익혀 두는 편이 좋습니다. 프로젝트가 커질수록 개발 환경, 테스트 환경, 운영 환경의 설정이 달라지기 때문입니다.
구조를 나누면 무엇이 좋아질까
이번 장에서 한 일은 기능을 새로 추가한 것이 아닙니다. 사용자가 보는 게시판 화면은 거의 그대로입니다. 대신 내부 구조가 달라졌습니다.
board/__init__.py는 Flask 앱을 만들고 전체를 조립합니다.board/db.py는 데이터베이스 연결, 종료, 초기화 명령을 담당합니다.board/routes.py는 URL과 화면 처리 코드를 담당합니다.board/templates/는 HTML 템플릿을 담습니다.board/static/는 CSS 같은 정적 파일을 담습니다.instance/는 로컬 설정과 SQLite 파일처럼 공개하지 않을 데이터를 담습니다.
이렇게 나누면 파일 이름만 보아도 어디를 고쳐야 할지 짐작할 수 있습니다. 데이터베이스 문제가 생기면 db.py를 먼저 보고, 화면 이동 문제가 생기면 routes.py를 먼저 보면 됩니다.
또 하나의 장점은 테스트와 확장입니다. create_app() 함수가 있으면 설정이 다른 앱을 만들기 쉽습니다. 예를 들어 나중에 테스트용 데이터베이스를 따로 지정한 앱을 만들 수 있습니다. 지금은 당장 테스트 코드를 작성하지 않더라도, 이런 구조를 미리 잡아 두면 다음 단계로 넘어가기가 훨씬 수월합니다.
자주 만나는 오류
프로젝트 구조를 바꾸면 처음에는 import와 실행 명령에서 오류를 자주 만납니다. 오류가 나면 아래 항목부터 확인해 보세요.
ModuleNotFoundError: No module named 'board'
터미널 위치가 프로젝트 폴더가 아닐 때 자주 발생합니다. flask-study/ 폴더에서 명령을 실행하고 있는지 확인하세요. board/ 폴더와 instance/ 폴더가 보이는 위치가 기준입니다.
Flask가 앱을 찾지 못하는 경우
flask --app board run --debug가 동작하지 않으면 앱 팩토리를 명시해 보세요.
flask --app board:create_app run --debug또 board/__init__.py 안에 create_app() 함수 이름을 정확히 썼는지도 확인합니다. create_apps, createapp처럼 이름이 달라지면 Flask가 자동으로 찾기 어렵습니다.
no such table: posts
새 위치의 데이터베이스에 아직 posts 테이블이 없다는 뜻입니다. 구조를 바꾸면서 데이터베이스 파일 위치가 instance/board.sqlite3로 달라졌다면 초기화 명령을 다시 실행해야 합니다.
flask --app board init-dbTemplateNotFound
템플릿 파일 위치나 이름이 맞지 않을 때 발생합니다. 예를 들어 코드에서는 post_create.html을 렌더링하는데 실제 파일 이름이 post_form.html이면 오류가 납니다. render_template()에 적은 이름과 board/templates/ 안의 파일 이름을 맞추세요.
이번 장의 정리
이번 장에서는 app.py 하나에 모여 있던 코드를 유지보수하기 쉬운 구조로 나누었습니다.
처음에는 한 파일로 시작하는 것이 좋습니다. 그러나 라우트, 데이터베이스, 설정, 템플릿이 계속 늘어나면 파일을 나누어야 합니다. 이때 Flask 앱을 board/ 패키지로 만들고, create_app() 함수에서 앱을 생성하는 애플리케이션 팩토리 패턴을 사용하면 구조가 훨씬 깔끔해집니다.
기억할 내용은 다음과 같습니다.
board/는 Flask 애플리케이션 코드가 들어가는 파이썬 패키지입니다.__init__.py안의create_app()함수가 앱을 만들고 설정과 기능을 조립합니다.db.py에는 데이터베이스 연결과 초기화 코드를 둡니다.routes.py에는 URL과 화면 처리 코드를 둡니다.instance/에는 공개하지 않을 설정 파일과 SQLite 데이터베이스 파일을 둘 수 있습니다.실행 명령은
flask --app board run --debug처럼 패키지를 기준으로 바뀝니다.
이제 프로젝트는 한 파일짜리 연습 앱에서 조금 더 실제 프로젝트다운 모습으로 바뀌었습니다. 다음에는 routes.py도 기능별로 더 나누는 방법을 배웁니다. 그때 사용할 도구가 Flask의 Blueprint입니다.
참고
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.