Flask에서 SQLAlchemy 세션 관리 실수와 해결
문제 상황
사내 관리자 페이지를 Flask로 개발하던 중 간헐적으로 다른 사용자의 데이터가 조회되는 버그가 발생했다. 로컬에서는 재현이 안 되다가 스테이징에서 동시 접속 테스트 중 발견됐다.
원인
전역으로 선언한 SQLAlchemy 세션을 여러 요청에서 공유하고 있었다.
# 잘못된 코드
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
engine = create_engine(DATABASE_URL)
Session = sessionmaker(bind=engine)
session = Session() # 전역 세션
@app.route('/users/<user_id>')
def get_user(user_id):
user = session.query(User).filter_by(id=user_id).first()
return jsonify(user.to_dict())
요청 간 세션이 공유되면서 트랜잭션이 꼬이는 상황이었다.
해결
Flask-SQLAlchemy를 사용하거나, scoped_session을 활용해 요청별 세션을 분리했다.
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = DATABASE_URL
db = SQLAlchemy(app)
@app.route('/users/<user_id>')
def get_user(user_id):
user = User.query.filter_by(id=user_id).first()
return jsonify(user.to_dict())
Flask-SQLAlchemy는 내부적으로 scoped_session을 사용해 애플리케이션 컨텍스트별로 세션을 관리한다. 요청이 끝나면 자동으로 세션을 정리해준다.
직접 SQLAlchemy를 쓴다면 다음과 같이 처리할 수 있다.
from sqlalchemy.orm import scoped_session, sessionmaker
engine = create_engine(DATABASE_URL)
session_factory = sessionmaker(bind=engine)
Session = scoped_session(session_factory)
@app.teardown_appcontext
def shutdown_session(exception=None):
Session.remove()
교훈
- 웹 프레임워크에서 DB 세션은 요청 단위로 관리해야 한다
- 동시성 이슈는 로컬에서 재현하기 어렵다
- Flask-SQLAlchemy 같은 통합 라이브러리를 쓰면 이런 실수를 방지할 수 있다