서론
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를 바로 사용하는 경우, 다음과 같은 문제가 발생할 수 있습니다.
❌ 예상 가능한 문제들
- user.id가 None일 수 있음
- user 객체는 아직 DB에 INSERT되지 않았기 때문에, id는 생성되지 않은 상태입니다.
- 이 상태에서 user_id=user.id로 연결 데이터를 만들면 None이 들어가게 되죠.
- 결과적으로 외래키 제약 조건(FK constraint) 위반으로 DB 에러가 발생할 수 있습니다.
- 관계가 꼬이거나 데이터 무결성 문제 발생
- 여러 관계형 테이블을 동시에 처리할 때, 순서와 의존성이 중요한데 flush()가 없으면 의도치 않은 순서로 처리될 수 있습니다.
- 예를 들어 A 테이블의 id 값을 참조해서 B, C 테이블을 구성하는 경우, A의 id가 없으면 이후 로직이 모두 깨질 수 있어요.
- 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 계층 분리 구조에서의 실전 예제
'Dev > Python' 카테고리의 다른 글
[Python] setup.py와 requirements.txt는 뭐가 다를까? (0) | 2025.04.15 |
---|---|
[Python] print만 쓰다가 logging으로 바꾼 이유 (파이썬 로깅 입문기) (2) | 2025.04.14 |
[Python] db.flush() Service/CRUD 계층 분리 구조에서의 실전 예제 (0) | 2025.04.11 |
튜플 (Tuple) - 여러 값을 묶는 간단하고 효율적인 방법 (2) | 2025.04.10 |
[FastAPI x PostgreSQL] 프로세스 정리 (with SQLAlchemy, Pydantic) (0) | 2025.04.08 |