본문 바로가기

[Python] db.flush()는 언제, 왜 사용하는가?

@Jeeqong 2025. 4. 12. 12:42
반응형

서론

FastAPI와 SQLAlchemy를 사용한 백엔드 개발을 하다 보면, 가장 기본적인 데이터 저장 흐름으로 db.add() → db.commit()을 익히게 됩니다. 하지만 어느 순간, db.flush()라는 생소한 메서드를 만나게 되는 경우가 있죠.

이 함수는 왜 필요한 걸까요? 그냥 commit()만 하면 되는 거 아닌가요?

이번 포스팅에서는 flush()와 commit()의 차이, flush()가 어떤 상황에서 필요한지, 그리고 실제 예제 코드에서는 어떻게 사용하는지를 초보자도 이해할 수 있게 정리해보겠습니다.


본론

flush()란 무엇인가요?

db.flush()

이 코드는 SQLAlchemy에서 세션에 등록된 작업들을 실제 DB에 반영하지만, 트랜잭션은 종료하지 않고 그대로 유지하는 역할을 합니다.

좀 더 쉽게 말하면, 우리가 .add()를 사용해서 SQLAlchemy 세션에 객체를 등록했을 때, 실제로는 DB에 쿼리가 바로 날아가는 게 아닙니다. 일종의 "대기열"에 들어가 있는 상태죠.

그런데 어떤 경우에는 데이터베이스에 바로 INSERT 쿼리를 날려야 할 때가 있습니다. 예를 들어, id처럼 데이터베이스에서 자동으로 생성되는 값을 바로 활용해야 할 때가 그렇습니다.

이럴 때 flush()를 호출하면, DB에 쿼리를 날려서 그 결과(id 등)를 세션 객체에 반영해줍니다.

단, commit()과 달리 트랜잭션을 끝내지는 않기 때문에, 실수로 잘못 저장했을 경우 rollback도 가능합니다.


flush() vs commit() 차이점

항목  flush()  commit()
실행 시점 명시적 또는 자동 호출 (ORM 내부) 개발자가 명시적으로 호출
트랜잭션 종료 ❌ 유지됨 ✅ 종료됨
INSERT 실행 ✅ 실행됨 ✅ 실행됨
객체에 ID 반영 ✅ 가능 ✅ 가능
롤백 가능 ✅ 가능 ❌ 확정되어 rollback 불가

💡 flush()는 내부적으로 SQL 쿼리를 실행하지만, 트랜잭션은 유지되는 중간 상태입니다.

commit()은 말 그대로 변경 사항을 완전히 확정짓는 단계입니다.


실전 예제

아래는 유저를 생성하면서 바로 연결된 프로필도 함께 만드는 상황입니다.

def create_user(db: Session, user_data: UserCreate):
    user = User(**user_data.dict())
    db.add(user)
    db.flush()  # 이 시점에 user.id 생성됨

    # user.id가 생성되었기 때문에 연결된 프로필 생성 가능
    profile = UserProfile(user_id=user.id, nickname="newbie")
    db.add(profile)

    db.commit()  # 트랜잭션 최종 확정
    return user

설명

  • flush()를 하지 않고 commit()을 하게 되면, user.id를 바로 사용할 수 없습니다.
  • 하지만 flush()를 해주면, 데이터베이스에 INSERT 쿼리가 실행되고, user 객체에 id가 반영되므로 user.id를 바로 활용할 수 있습니다.
  • 이후 commit()을 호출하면, user, profile 모두가 데이터베이스에 안전하게 저장됩니다.

이런 상황에서 유용해요

  • 외래키(FK) 연결을 사용하는 경우 (ex: 프로필, 주문 상세 등)
  • PK(id) 값이 있어야 다음 로직을 실행할 수 있는 경우
  • 여러 테이블에 한 번에 저장하면서 순서가 중요한 경우
  • 트랜잭션 안에서 INSERT 결과를 먼저 확인하고 싶은 경우

flush() 없이 처리할 때 발생 가능한 문제

user = User(username="abc")
db.add(user)

profile = UserProfile(user_id=user.id, nickname="abc-user")
db.add(profile)

db.commit()

이 코드처럼 flush() 없이 user.id를 바로 사용하는 경우, 다음과 같은 문제가 발생할 수 있습니다.


❌ 예상 가능한 문제들

  1. user.id가 None일 수 있음
    • user 객체는 아직 DB에 INSERT되지 않았기 때문에, id는 생성되지 않은 상태입니다.
    • 이 상태에서 user_id=user.id로 연결 데이터를 만들면 None이 들어가게 되죠.
    • 결과적으로 외래키 제약 조건(FK constraint) 위반으로 DB 에러가 발생할 수 있습니다.
  2. 관계가 꼬이거나 데이터 무결성 문제 발생
    • 여러 관계형 테이블을 동시에 처리할 때, 순서와 의존성이 중요한데 flush()가 없으면 의도치 않은 순서로 처리될 수 있습니다.
    • 예를 들어 A 테이블의 id 값을 참조해서 B, C 테이블을 구성하는 경우, A의 id가 없으면 이후 로직이 모두 깨질 수 있어요.
  3. ORM 내부에서 자동 flush 되기도 하지만 시점이 애매
    • SQLAlchemy는 특정 시점에 자동으로 flush()를 호출하기도 합니다.
    • 하지만 이 시점은 예측이 어렵고, 특히 복잡한 로직일수록 개발자가 의도한 타이밍과 다를 수 있습니다.
    • 예기치 않은 오류가 발생했을 때 원인을 추적하기 어려워져 디버깅이 복잡해집니다.

요약

  • 외래키 연결 시 id가 아직 없으면 무결성 오류
  • flush() 없이 객체 사용 시 ORM 내부 동작에 의존하게 되어 예측 불가
  • 특히 중간 객체의 id가 중요한 로직에서는 flush()가 필수

이런 문제들을 예방하려면, 객체 간 참조가 필요한 순간에는 flush()를 명시적으로 호출해주는 습관이 중요합니다.

특히 관계형 테이블 설계가 많은 FastAPI 프로젝트에서는 거의 필수로 보셔도 돼요.


결론

db.flush()는 FastAPI나 SQLAlchemy를 사용할 때 중간단계에서 매우 유용한 도구입니다.

단순히 데이터를 저장하는 것이 아니라, 트랜잭션을 유지한 채 쿼리를 먼저 실행하고 결과(id 등)를 받아와야 하는 상황이 점점 많아지기 때문이죠.

flush()의 존재를 잘 이해하고 있으면, 복잡한 비즈니스 로직을 좀 더 유연하고 안전하게 구성할 수 있습니다.


정리하면:

  • flush()는 DB에 반영은 하지만 트랜잭션을 열어둔 상태를 유지
  • 자동 생성 필드(id 등)를 사용해야 할 때 꼭 필요
  • commit() 전에 호출되며, rollback도 가능

 


🔗 Reference

1. SQLAlchemy flush() 공식 설명

https://docs.sqlalchemy.org/en/20/orm/session_api.html#sqlalchemy.orm.Session.flush

flush()가 하는 일, 언제 호출되는지, 내부 동작 방식에 대한 가장 정확한 설명이 포함되어 있어요.

 

2. SQLAlchemy 트랜잭션과 세션 작동 원리

https://docs.sqlalchemy.org/en/20/orm/session_basics.html

세션과 flush/commit 관계에 대한 전체적인 이해를 돕는 구조적 설명.

 

3. FastAPI 공식 문서 - 데이터베이스 예제

https://fastapi.tiangolo.com/tutorial/sql-databases/

FastAPI에서 SQLAlchemy를 사용할 때의 기본 흐름을 이해할 수 있는 구조 제공.

 

 

2025.04.07 - [🚀 Dev/Python] - [Python] db.flush() Service/CRUD 계층 분리 구조에서의 실전 예제

반응형
Jeeqong
@Jeeqong :: JQVAULT

Jeeqong's vault : 정보/기록을 쌓아두는 공간 웹개발 포스팅 일상 리뷰를 기록하는 공간입니다.

공감하셨다면 ❤️ 구독도 환영합니다! 🤗

목차