메뉴 닫기

Flask SQLAlchemy ORM 기본 모델 세션 쿼리 완벽가이드

Flask SQLAlchemy ORM 기본 모델 세션 쿼리 완벽가이드

📌 모델부터 세션과 쿼리까지, Flask에서 SQLAlchemy ORM을 제대로 쓰는 가장 쉬운 길

파이썬으로 웹 서비스를 만들다 보면 데이터베이스와 말을 맞추는 일이 제일 막막하게 느껴집니다.
SQL을 일일이 작성하기엔 반복이 많고, ORM을 쓰자니 개념이 추상적으로 느껴지죠.
Flask에서 SQLAlchemy ORM을 사용하면 파이썬 클래스만으로 테이블을 정의하고, 세션을 통해 안전하게 트랜잭션을 관리하며, 직관적인 쿼리 API로 데이터를 읽고 쓰는 흐름을 자연스럽게 이어갈 수 있습니다.
이 글은 ORM의 핵심 구조를 간단한 예시와 함께 정리하고, 현업에서 바로 적용할 수 있는 패턴을 중심으로 설명합니다.
처음 접하는 분들도 “왜 이렇게 하는지”를 이해하면서 따라갈 수 있도록 실용적인 맥락과 주의점을 함께 담았습니다.

Flask 애플리케이션에 SQLAlchemy를 붙일 때 가장 중요한 것은 모델의 설계와 세션의 생명주기입니다.
여기에 더해 관계 설정과 CRUD 패턴을 깔끔하게 정리해두면 유지보수 비용이 크게 줄어듭니다.
또한 설정값 관리, 마이그레이션 도구, 테스트 환경 분리 같은 현실적인 이슈를 놓치지 않는 것이 안정적인 서비스 운영의 기반이 됩니다.
이 글에서는 기본 모델·세션·쿼리를 핵심 축으로 삼아 Flask 프로젝트에 자연스럽게 통합하는 방법을 체계적으로 정리했습니다.
개념과 코드를 연결해 사고할 수 있도록, 실수하기 쉬운 포인트도 함께 짚어드립니다.



🔗 SQLAlchemy ORM 기본 개념과 구조

SQLAlchemy는 파이썬에서 가장 널리 쓰이는 ORM(Object Relational Mapping) 라이브러리로, 객체지향적 코드와 관계형 데이터베이스를 자연스럽게 연결해 줍니다.
ORM은 SQL 쿼리를 직접 작성하지 않고, 파이썬 클래스를 정의하여 데이터베이스 테이블과 매핑하는 방식을 사용합니다.
이를 통해 데이터베이스 작업이 훨씬 직관적으로 바뀌고, 유지보수성이 좋아집니다.

ORM의 핵심은 크게 세 가지 축으로 나눌 수 있습니다.
먼저 모델(Model)은 데이터베이스 테이블과 매핑되는 파이썬 클래스입니다.
그다음 세션(Session)은 데이터베이스 연결과 트랜잭션을 관리하는 핵심 객체입니다.
마지막으로 쿼리(Query)는 데이터를 조회하거나 수정하기 위해 사용하는 직관적인 API입니다.

📌 ORM과 전통적인 SQL의 차이

전통적으로는 SQL문을 직접 작성하여 데이터베이스와 통신했습니다.
하지만 ORM을 사용하면 SQL 문법을 몰라도, 파이썬 코드로 데이터를 처리할 수 있습니다.
예를 들어, 사용자를 조회하는 SQL 쿼리 SELECT * FROM users WHERE id=1은 ORM에서는 session.query(User).get(1)과 같은 코드로 표현됩니다.
SQLAlchemy는 이 과정을 내부적으로 SQL로 변환하여 실행합니다.

CODE BLOCK
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)
    name = Column(String)

#  클래스는 데이터베이스의 users 테이블과 매핑됩니다.

📌 SQLAlchemy의 두 가지 핵심 계층

SQLAlchemy는 크게 CoreORM으로 나뉩니다.
Core는 SQL을 추상화한 저수준 계층으로, 쿼리 빌더와 엔진을 다룹니다.
ORM은 Core 위에서 동작하며, 클래스를 테이블에 매핑하여 더 객체지향적으로 사용할 수 있게 해줍니다.
Flask와 함께 사용할 때는 대부분 ORM 계층을 다루게 됩니다.

  • 📌모델: 파이썬 클래스 ↔ 데이터베이스 테이블
  • 📌세션: 연결과 트랜잭션 관리
  • 📌쿼리: 직관적인 API로 데이터 조회 및 조작

🛠️ 모델 정의와 스키마 매핑

ORM에서 모델은 테이블과 1:1로 연결되는 파이썬 클래스입니다.
클래스의 속성은 컬럼으로, 클래스 간 관계는 외래 키와 관계 매핑으로 표현됩니다.
핵심 포인트는 일관된 네이밍, 명시적인 기본 키, 정확한 자료형 지정, 그리고 nullable·unique·index·default 같은 제약 설정입니다.
스키마 매핑이 명확해야 마이그레이션이 단순해지고, 쿼리 최적화 및 데이터 무결성이 자연스럽게 확보됩니다.
Flask에서는 flask_sqlalchemy 확장 또는 순수 SQLAlchemy를 선택할 수 있으며, 확장을 쓰면 앱 컨텍스트와 세션 관리가 편리해지는 장점이 있습니다.

🏗️ Flask-SQLAlchemy로 모델 클래스 선언

아래 예시는 SQLite를 사용하는 최소 구성입니다.
db.Model을 상속받아 테이블 이름, 컬럼, 제약조건을 선언하고, __repr__로 디버깅 가독성을 높입니다.
SQLAlchemy 2.x 스타일에 맞춰 타입과 서버 기본값을 명시하고, 인덱스가 필요한 컬럼에는 index=True를 지정합니다.

CODE BLOCK
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import func

db = SQLAlchemy()

def create_app():
    app = Flask(__name__)
    app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///app.db"
    app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
    db.init_app(app)
    with app.app_context():
        db.create_all()  # 데모용: 실제 서비스는 마이그레이션 권장
    return app

class User(db.Model):
    __tablename__ = "users"
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(255), unique=True, nullable=False, index=True)
    name = db.Column(db.String(80), nullable=False)
    is_active = db.Column(db.Boolean, nullable=False, server_default="1")
    created_at = db.Column(db.DateTime(timezone=True), nullable=False, server_default=func.now())

    def __repr__(self):
        return f"<User id={self.id} email={self.email}>"

class Post(db.Model):
    __tablename__ = "posts"
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200), nullable=False, index=True)
    body = db.Column(db.Text, nullable=False)
    user_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
    created_at = db.Column(db.DateTime(timezone=True), nullable=False, server_default=func.now())

🧩 컬럼 정의 패턴과 제약조건 체크리스트

모델을 설계할 때는 컬럼 타입과 제약조건을 데이터의 의미에 맞게 고르는 것이 가장 중요합니다.
문자열 길이는 충분히 잡되 과도하게 크지 않게 정하고, 불리언은 기본값을 명시해 의도를 분명히 합니다.
시간 컬럼은 타임존 인식 타입을 권장합니다.

항목 권장 설정
기본 키 자동 증가 정수 또는 UUID, primary_key=True
고유값 unique=True로 이메일·아이디 등 중복 방지
인덱스 검색·정렬 잦은 컬럼에 index=True
NULL 처리 nullable=False로 필수 값 보장
기본값 DB 레벨 기본값은 server_default 사용

💡 TIP: 이메일·닉네임처럼 자주 조회되는 컬럼은 unique + index 조합으로 충돌 방지와 성능을 동시에 확보하세요.

🔗 순수 SQLAlchemy 선언형과의 비교

확장을 쓰지 않고 순수 SQLAlchemy로도 동일하게 모델을 만들 수 있습니다.
이 방식은 프레임워크에 독립적이며, 테스트에서 가볍습니다.
아래처럼 DeclarativeBase 또는 declarative_base() 기반 클래스를 만들고, 세션은 Session으로 직접 관리합니다.

CODE BLOCK
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import String, Integer, Boolean, DateTime, func

class Base(DeclarativeBase):
    pass

class User(Base):
    __tablename__ = "users"
    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
    name: Mapped[str] = mapped_column(String(80), nullable=False)
    is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default="1")
    created_at: Mapped["datetime"] = mapped_column(DateTime(timezone=True), server_default=func.now())

⚠️ 주의: 개발 중에는 db.create_all()로 빠르게 테이블을 만들 수 있지만, 운영 환경에서는 스키마 변경 기록과 롤백을 위해 Alembic/Flask-Migrate 같은 마이그레이션 도구를 필수로 사용하세요.

🧪 스키마 검증과 데이터 무결성 패턴

모델 단에서 서버 기본값NOT NULL을 명시하고, 애플리케이션 계층에서는 입력 검증을 추가해 2중 방어를 구축하는 것이 안전합니다.
또한 비즈니스 규칙(예: 제목 최소 길이, 허용 문자 등)은 모델 훅이나 서비스 레이어에서 검사하여 DB 제약 위에서 한 번 더 걸러내면 데이터 일관성이 높아집니다.
인덱스는 읽기 패턴에 맞춰 선택적으로 추가하고, 과도한 인덱싱은 쓰기 성능을 저하시킬 수 있으므로 주기적으로 실행 계획을 점검하는 습관이 필요합니다.



⚙️ 세션 생성과 트랜잭션 관리

SQLAlchemy에서 세션(Session)은 데이터베이스와의 모든 상호작용을 책임지는 핵심 객체입니다.
세션은 단순한 커넥션 풀링 이상으로, 트랜잭션 범위를 정의하고, 변경 내용을 추적하며, 커밋이나 롤백을 통해 데이터 일관성을 보장합니다.
Flask 애플리케이션에서는 요청 단위로 세션을 열고 닫는 패턴이 권장되며, 이를 통해 메모리 누수나 커넥션 고갈 문제를 방지할 수 있습니다.

🔑 세션 기본 구조와 사용 패턴

SQLAlchemy Core 또는 순수 ORM을 사용할 경우, sessionmaker를 이용해 세션 팩토리를 구성합니다.
Flask-SQLAlchemy 확장을 쓰면 db.session을 바로 사용할 수 있어 훨씬 간단합니다.
세션은 변경된 객체를 추적하고, commit()을 호출할 때만 실제 SQL이 실행됩니다.
rollback()을 하면 직전 트랜잭션 내의 변경 사항이 모두 취소됩니다.

CODE BLOCK
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

engine = create_engine("sqlite:///app.db")
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)

# 세션 열기
session = SessionLocal()

try:
    user = User(name="홍길동", email="hong@test.com")
    session.add(user)
    session.commit()  # INSERT 실행
except:
    session.rollback()
    raise
finally:
    session.close()

🌐 Flask 요청 단위 세션 관리

Flask-SQLAlchemy를 사용하면 요청이 시작될 때 세션이 열리고, 요청이 끝날 때 자동으로 커밋 또는 롤백 후 닫히도록 설정할 수 있습니다.
이는 application contextteardown hooks를 통해 동작합니다.
직접 세션을 관리할 때는 블루프린트 또는 미들웨어 레벨에서 세션을 열고 닫는 코드를 배치하는 것이 안전합니다.

💬 “요청 단위 세션(one request, one session)” 패턴은 Flask + SQLAlchemy에서 가장 안정적인 관리 방식입니다.

🧩 세션 상태와 객체 라이프사이클

세션에 추가된 객체는 transient → pending → persistent의 단계를 거칩니다.
add() 호출 시 pending 상태가 되고, commit() 후 실제 테이블에 반영되면 persistent 상태가 됩니다.
세션에서 제거되거나 close() 이후에는 detached 상태로 바뀌며, 이 상태에서 접근하는 속성은 lazy load 에러를 유발할 수 있습니다.

  • ⚙️세션은 커넥션 풀을 관리하며, commit/rollback 단위로 트랜잭션 제어
  • 🔑요청 단위 세션 관리로 안정성과 자원 효율성 확보
  • 🧩객체는 transient → persistent → detached 등 상태 전환을 거침

⚠️ 주의: 세션을 장시간 열어두거나 전역 변수처럼 사용하는 것은 메모리 누수, 잠금 경합, 불필요한 트랜잭션 장기화 문제를 일으킬 수 있습니다.

🔌 CRUD 쿼리와 관계 설정

SQLAlchemy ORM에서 쿼리는 CRUD(Create, Read, Update, Delete) 작업을 중심으로 구성됩니다.
ORM 객체를 세션에 추가하고 커밋하면 INSERT가 실행되고, 쿼리 객체로 조회하면 SELECT가 실행됩니다.
UPDATE와 DELETE 역시 객체를 수정하거나 삭제 후 커밋하는 방식으로 처리됩니다.
이 모든 과정은 ORM이 자동으로 SQL로 변환하여 실행하기 때문에, 개발자는 SQL 구문 대신 직관적인 파이썬 문법으로 데이터베이스를 다룰 수 있습니다.

📥 Create와 Read

새로운 객체를 생성하려면 모델 인스턴스를 만들고 세션에 추가한 뒤 커밋합니다.
데이터를 조회할 때는 query() 또는 select() 구문을 사용할 수 있습니다.
SQLAlchemy 2.x에서는 select() 문법이 권장됩니다.

CODE BLOCK
# Create
new_user = User(name="Alice", email="alice@test.com")
session.add(new_user)
session.commit()

# Read
from sqlalchemy import select
stmt = select(User).where(User.email == "alice@test.com")
result = session.execute(stmt).scalar_one()

✏️ Update와 Delete

객체의 속성을 수정한 뒤 커밋하면 UPDATE가 실행됩니다.
삭제는 delete() 메서드가 아니라 세션의 delete() 또는 session.delete()로 수행합니다.

CODE BLOCK
# Update
user = session.get(User, 1)
user.name = "Alice Updated"
session.commit()

# Delete
session.delete(user)
session.commit()

🔗 관계 설정과 조인

ORM의 강점은 테이블 간 관계를 클래스 간 속성으로 표현할 수 있다는 점입니다.
예를 들어, User와 Post 모델이 1:N 관계라면 relationship()을 사용해 양방향 참조를 설정할 수 있습니다.

CODE BLOCK
from sqlalchemy.orm import relationship

class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)
    posts = relationship("Post", back_populates="author", cascade="all, delete")

class Post(Base):
    __tablename__ = "posts"
    id = Column(Integer, primary_key=True)
    user_id = Column(Integer, ForeignKey("users.id"))
    author = relationship("User", back_populates="posts")

이제 user.posts로 해당 사용자의 글 목록을 가져오거나, post.author로 글 작성자를 조회할 수 있습니다.
쿼리에서 조인을 명시하고 싶다면 join()을 활용합니다.

  • 📥Create: 인스턴스 추가 → commit()
  • 🔍Read: select() 구문으로 조회
  • ✏️Update: 속성 수정 후 commit()
  • 🗑️Delete: session.delete() 후 commit()
  • 🔗관계: relationship()으로 양방향 참조 설정

⚠️ 주의: 대량 삭제나 업데이트 작업 시 ORM 루프 대신 session.execute()로 직접 SQL을 실행하는 것이 성능상 유리할 수 있습니다.



💡 Flask 앱에 통합하는 베스트 프랙티스

SQLAlchemy를 Flask 애플리케이션에 통합할 때는 단순히 모델과 세션만 정의하는 것으로 끝나지 않습니다.
앱 구조화, 환경별 설정 관리, 마이그레이션 도구 활용, 그리고 테스트 전략까지 고려해야 안정적이고 유지보수하기 좋은 프로젝트를 만들 수 있습니다.
여기서는 실무에서 자주 쓰이는 베스트 프랙티스를 정리합니다.

📂 애플리케이션 팩토리 패턴

Flask는 앱 인스턴스를 함수로 생성하는 팩토리 패턴을 권장합니다.
이 방식은 확장 초기화, 블루프린트 등록, 환경별 설정 분리를 유연하게 할 수 있습니다.

CODE BLOCK
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

def create_app(config_name="default"):
    app = Flask(__name__)
    app.config.from_object(f"config.{config_name.capitalize()}Config")
    db.init_app(app)
    return app

🗂️ 설정 관리와 환경 분리

개발, 테스트, 운영 환경마다 데이터베이스 주소와 옵션은 달라집니다.
이를 위해 설정 클래스를 별도 모듈로 분리하고, 환경 변수로 현재 환경을 주입하는 방식을 씁니다.
이렇게 하면 테스트 DB를 운영에 잘못 연결하는 실수를 예방할 수 있습니다.

💡 TIP: 운영 환경에서는 반드시 DB 연결 문자열을 환경 변수로 관리하고, 코드에 직접 노출하지 마세요.

⚡ 마이그레이션과 스키마 관리

실제 서비스에서는 테이블 스키마가 자주 바뀌므로 Flask-Migrate(Alembic 기반)를 활용하는 것이 필수입니다.
flask db migrate, flask db upgrade 명령으로 버전별 스키마를 관리하면 롤백과 배포 자동화가 쉬워집니다.

🧪 테스트 전략과 트랜잭션 롤백

단위 테스트에서는 테스트 전용 데이터베이스를 생성하고, 각 테스트가 끝날 때마다 트랜잭션을 롤백해 초기 상태를 유지하는 것이 가장 깔끔합니다.
Pytest를 사용할 경우 fixture로 세션을 관리하고, 테스트마다 새로운 세션을 열어 독립성을 보장합니다.

⚠️ 주의: 테스트 환경에서 운영 데이터베이스를 절대 사용하지 마세요.
특히 삭제 쿼리 실행 시 치명적 사고로 이어질 수 있습니다.

  • 📂팩토리 패턴으로 앱 구조화
  • 🗂️환경별 설정 클래스와 환경 변수 관리
  • Flask-Migrate로 스키마 버전 관리
  • 🧪테스트 환경은 별도 DB와 롤백 패턴 사용

자주 묻는 질문 (FAQ)

Flask에서 SQLAlchemy ORM을 꼭 써야 하나요?
꼭 필수는 아니지만, ORM을 쓰면 모델링과 쿼리 작성이 단순해지고 유지보수가 훨씬 편리해집니다. SQL문을 직접 쓰는 것보다 안전성과 가독성이 높아 초보자와 숙련자 모두에게 권장됩니다.
SQLAlchemy Core와 ORM의 차이는 무엇인가요?
Core는 SQL을 파이썬으로 추상화한 저수준 계층이고, ORM은 Core 위에서 클래스와 객체로 데이터베이스를 다룰 수 있게 하는 고수준 계층입니다. Flask 앱에서는 보통 ORM을 주로 사용합니다.
세션을 전역으로 두면 안 되는 이유가 있나요?
세션을 전역으로 두면 트랜잭션이 장시간 유지되면서 메모리 누수, 잠금 경합, 커넥션 고갈 문제가 발생할 수 있습니다. 요청 단위로 열고 닫는 패턴이 안전합니다.
관계 설정 시 cascade 옵션은 어떻게 쓰나요?
cascade 옵션을 설정하면 부모 객체 삭제 시 자식 객체도 함께 삭제되도록 지정할 수 있습니다. 예를 들어 User 삭제 시 Post도 같이 지우려면 cascade="all, delete"를 사용합니다.
Flask-Migrate 없이도 테이블 변경이 가능한가요?
가능은 하지만 권장되지 않습니다. 개발 단계에서는 create_all로 만들 수 있지만, 운영 환경에서는 마이그레이션 도구가 없으면 변경 이력 관리와 롤백이 불가능합니다.
SQLAlchemy에서 N+1 문제를 해결하려면 어떻게 하나요?
관계 데이터를 미리 로드하기 위해 joinedload 또는 selectinload 같은 eager loading 옵션을 사용하면 쿼리 수를 줄일 수 있습니다.
테스트 환경에서는 어떤 데이터베이스를 쓰는 게 좋을까요?
보통 SQLite 인메모리 DB를 많이 씁니다. 하지만 실제 운영 환경과 차이가 크면 PostgreSQL 같은 동일한 엔진으로 테스트 DB를 별도 두는 것이 더 안전합니다.
SQLAlchemy 1.x와 2.x 문법 차이가 큰가요?
2.x에서는 select(), Session.get() 같은 새로운 스타일을 권장하며, 일부 구식 API는 deprecated 처리되었습니다. 새로운 프로젝트라면 2.x 문법을 사용하는 것이 좋습니다.

📝 Flask SQLAlchemy ORM 핵심 정리

Flask와 SQLAlchemy ORM을 함께 사용하면 데이터베이스 작업을 객체 지향적으로 단순화하면서도 강력한 제어력을 유지할 수 있습니다.
모델 정의로 스키마를 명확히 하고, 세션으로 안전하게 트랜잭션을 관리하며, 직관적인 쿼리 API로 CRUD를 수행할 수 있습니다.
또한 관계 설정과 eager loading을 적절히 활용하면 성능과 가독성을 동시에 잡을 수 있습니다.
프로젝트 구조를 애플리케이션 팩토리 패턴으로 정리하고, 환경별 설정 분리와 마이그레이션 도구를 병행하면 확장성과 안정성이 크게 향상됩니다.
테스트 환경에서는 별도의 DB와 롤백 전략으로 데이터 일관성을 유지하는 것이 중요합니다.
즉, SQLAlchemy ORM은 단순한 도구를 넘어 Flask 애플리케이션의 데이터 관리 핵심 축으로 자리 잡으며, 올바른 설계와 활용법을 익히면 장기적인 유지보수와 성능 관리에서 큰 이점을 얻을 수 있습니다.


🏷️ 관련 태그 : Flask, SQLAlchemy, ORM, 파이썬웹개발, 데이터베이스, 트랜잭션관리, 모델링, CRUD, FlaskMigrate, 웹프로그래밍