티스토리 뷰

개발/python

SQLAlchemy add, flush, commit

Jaeyeon Baek 2021. 4. 27. 22:03

SQLAlchemy는 애플리케이션 개발자에게 SQL의 모든 기능과 유연성을 제공하는 Python SQL 툴킷 및 객체 관계형 매퍼입니다 (공식 홈페이지 소개 문구). 모든 데이터베이스 객체를 다룰 때 신경 써야 하는 부분이 트랜잭션(Transaction)인데 SQLAlchemy도 예외는 아닙니다. 공식 홈페이지의 예제를 통해 기본적인 트랜잭션 관리를 살펴봅시다.

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

# an Engine, which the Session will use for connection
# resources
engine = create_engine('postgresql://scott:tiger@localhost/')

Session = sessionmaker(engine)

with Session() as session:
    session.add(some_object)
    session.add(some_other_object)
    session.commit()  # 세션이 닫힐 때 커밋되기 때문에 필요없는 구문이지만 명시적으로 사용

 

아주 간단한 예제입니다. sessionmaker의 정의를 보면 다음과 같습니다.

class sqlalchemy.orm.sessionmaker(bind=None, class_=<class 'sqlalchemy.orm.session.Session'>, 
autoflush=True, autocommit=False, expire_on_commit=True, info=None, **kw)

중요한 건 autoflush, autocommit입니다. 각각 True, False로 설정되어 있죠. 즉, 다시 첫 번째 예제를 살펴보면 session.flush()를 굳이 할 필요가 없습니다. 그래서 예제 상에서 호출되는 부분이 없는 겁니다. 잠깐, 먼저 add, flush, commit 정의를 알아볼까요?

add : session에 인스턴스를 배치하는 데 사용합니다. 그리고 다음 flush 시에 INSERT가 발생합니다.
flush : 트랜잭션을 데이터베이스로 전송합니다. 아직 커밋되지 않은 상태입니다. 
commit : 트랜잭션을 커밋합니다. 내부적으로 항상 flush()를 실행해서 트랜잭션을 flush 합니다. sessionmaker에서 설정된 autoflush 여부와 상관없습니다. 

말은 비슷비슷한데 autoflush를 False로 사용하게 되면 주의해야 할 것이 생깁니다. 바로 FK가 걸려있는 여러 테이블 객체를 다룰 때입니다. 예를 들어 some_object를 먼저 생성하고 id 컬럼을 some_other_object에서 FK로 사용한다고 생각해보세요. 위에 로직 상으로는 문제가 없어 보이지만 서로 다른 클래스 객체에 대해서 add 된 내용이 commit 될 때 순서가 보장되지 않습니다. 고로, 아래와 같은 에러를 만나게 될 거예요.

sqlalchemy.exc.IntegrityError: (mysql.connector.errors.IntegrityError) 1452 (23000): 
Cannot add or update a child row: a foreign key constraint fails 
(`some`.`SOME_OTHER_OBJECT`, CONSTRAINT `SOME_OTHER_OBJECT_SOME_OTHER_ID_FK` 
FOREIGN KEY (`SOME_OTHER_ID`) REFERENCES `SOME_OTHER` (`ID`) ON UPDATE CASCADE)

이런 문제를 막기 위해 중간에 flush를 통해 트랜잭션 순서를 보장해줘야 합니다. 위의 예제는 다행스럽게 이미 authflush가 활성화되어 있기 때문에 이런 문제가 발생하지 않겠네요. 하지만 모든 쿼리에 대해서 flush를 하는 것이 좋은지는 서비스 종류에 따라 고민해봐야 합니다. 모든 비즈니스 로직을 매번 데이터베이스 레벨에서 다루는 것은 한정된 리소스를 사용하는 DB에 좋은 모델은 아니기 때문이죠. 상대적으로 ORM을 다루는 서버는 scaling 하게 구성되기 때문에 부하 걱정이 없습니다. 

flush는 트랜잭션을 데이터베이스에 전송만 할 뿐이라 잘못하면 아래와 같은 실수를 할 수 있습니다.

>>> address = user.addresses[1]
>>> session.delete(address)
>>> session.flush()
>>> address in user.addresses
True

삭제하고 flush 했지만 아직 address 객체는 살아있는 상태입니다. 아래와 같이 commit을 해줘야 비로소 삭제가 됩니다.

>>> session.commit()
>>> address in user.addresses
False

그렇다면 commit을 자주 해주는 게 좋을까요? commit은 신중해야 합니다. 한번 커밋되면 rollback 시킬 수 없기 때문이죠. 그렇기 때문에 autocommit은 절대적으로 False로 사용하는 게 좋습니다. 트랜잭션은 비즈니스에서 상당히 중요한 부분을 차지하는 만큼 트랜잭션 범위를 어떻게 잡을지는 서비스 종류에 따라 세밀하고 치밀하게 설계가 되어야겠습니다. 그만큼 많은 고민이 필요한 부분이기도 하고요. 서비스가 궤도에 오른 후에 신경 쓰는 부분이 절대 아닙니다.

 

댓글
  • 프로필사진 BlogIcon GiTo SQLAlchemy 는 내부적으로 정말 많은 기능을 제공하고 있네요.
    하나의 session 객체에서 transaction 이 동작하는 방식에 대해 한번 더 배우고 갑니다.
    좋은 도구를 사용하는 것은 좋지만 제대로 알고 쓰는 것이 더 나은 시스템을 만들어 주는 듯 하네요 :)
    2021.05.17 23:15 신고
댓글쓰기 폼