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 같은 통합 라이브러리를 쓰면 이런 실수를 방지할 수 있다
Flask에서 SQLAlchemy 세션 관리 실수와 해결