SQLAlchemy 시리즈 1 - Connection 과 Result


Python 애플리케이션에서 데이터베이스 작업을 수행할 때, SQLAlchemy는 강력하고 유연한 ORM(Object Relational Mapper)이자 SQL 툴입니다. 데이터베이스와의 상호작용에서 가장 기본적이면서도 중요한 부분은 연결(Connection)을 관리하고 쿼리 결과를 처리하는 것입니다. 이번 글에서는 SQLAlchemy의 Connection
객체를 활용한 트랜잭션 관리 방법과 쿼리 결과를 담는 Result
및 Row
객체의 효과적인 사용법에 대해 자세히 알아보겠습니다.
SQLAlchemy Engine 설정
모든 SQLAlchemy 애플리케이션은 Engine
객체로부터 시작합니다. Engine
은 데이터베이스와의 연결을 위한 입구 역할을 하며, 실제 DBAPI 연결을 관리하는 Connection
객체를 생성하는 팩토리(Factory)입니다. 가벼운 예제이므로 SQLite 인메모리(in-memory) 데이터베이스를 위한 Engine
을 생성하여 진행해 보겠습니다.
(**echo=True
인자는 실행되는 SQL 쿼리를 로깅하여 디버깅에 유용합니다.)
from sqlalchemy import create_engine
engine = create_engine("sqlite+pysqlite:///:memory:", echo=True)
Connection을 이용한 기본적인 작업과 트랜잭션
데이터베이스와 직접 상호작용하기 위해서는 Engine
으로부터 Connection
객체를 얻어야 합니다. Connection
객체는 데이터베이스 작업을 수행하는 데 필요한 리소스를 제공하며, 작업 범위를 정의하고 해당 범위 내에서 리소스 관리를 돕습니다. 파이썬의 with
문과 함께 사용하면 Connection
객체가 컨텍스트 관리자(contextmanager
)로 동작하여, 블록을 벗어날 때 자동으로 연결 리소스를 정리해 줍니다.
from sqlalchemy import text
with engine.connect() as conn:
result = conn.execute(text("select 'Hello World'"))
print(result.all())
위 코드의 실행 결과를 보면, with
블록이 시작될 때 암시적(implicit)으로 트랜잭션이 시작(BEGIN (implicit)
)되고, 쿼리가 실행된 후 블록이 종료될 때 명시적인 commit()
호출이 없었기 때문에 자동으로 ROLLBACK
이 수행됩니다.
2025-04-25 00:03:30,561 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2025-04-25 00:03:30,562 INFO sqlalchemy.engine.Engine select 'Hello World'
2025-04-25 00:03:30,563 INFO sqlalchemy.engine.Engine [generated in 0.00221s] ()
[('Hello World',)]
2025-04-25 00:03:30,564 INFO sqlalchemy.engine.Engine ROLLBACK
데이터베이스 상태를 변경하는 작업(예: INSERT
, UPDATE
, DELETE
)을 영구적으로 반영하려면, 작업 완료 후 conn.commit()
을 명시적으로 호출해야 합니다.
with engine.connect() as conn:
"""데이터를 생성하고 커밋하는 예제"""
conn.execute(text("CREATE TABLE some_table (x int, y int)"))
conn.execute(text("INSERT INTO some_table (x, y) VALUES (:x, :y)"), [{"x": 1, "y": 2}, {"x": 3, "y": 4}])
conn.commit() # 변경 사항을 커밋합니다.
실행 결과를 보면 COMMIT
이 명시적으로 수행되어 데이터베이스 변경 사항이 저장됩니다.
2025-04-25 00:03:30,571 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2025-04-25 00:03:30,572 INFO sqlalchemy.engine.Engine CREATE TABLE some_table (x int, y int)
2025-04-25 00:03:30,573 INFO sqlalchemy.engine.Engine [generated in 0.00173s] ()
2025-04-25 00:03:30,574 INFO sqlalchemy.engine.Engine INSERT INTO some_table (x, y) VALUES (?, ?)
2025-04-25 00:03:30,574 INFO sqlalchemy.engine.Engine [generated in 0.00048s] [(1, 2), (3, 4)]
2025-04-25 00:03:30,575 INFO sqlalchemy.engine.Engine COMMIT
이처럼 각 작업 단위가 끝날 때마다 commit()
을 호출하는 방식을 SQLAlchemy에서는 "Commit as you go" 패턴이라고 부릅니다. 이 패턴은 개발자가 트랜잭션의 범위를 명확하게 제어할 수 있게 해주지만, 커밋 시점을 직접 관리해야 하는 부담이 있습니다.
begin()
메소드를 활용한 자동 트랜잭션 관리 ("Begin once")
"Commit as you go" 패턴의 번거로움을 줄이고 트랜잭션 관리를 자동화하기 위해 SQLAlchemy는 engine.begin()
메소드를 제공합니다. 이 메소드 역시 with
문과 함께 사용되며, 컨텍스트 블록에 진입할 때 자동으로 Connection
을 얻고 트랜잭션을 시작합니다. 블록 내의 모든 작업이 성공적으로 완료되면 자동으로 COMMIT
을 수행하고, 예외가 발생하면 자동으로 ROLLBACK
을 수행합니다. 이를 "Begin once" 패턴이라고 합니다.
with engine.begin() as conn:
conn.execute(text("INSERT INTO some_table (x, y) VALUES (:x, :y)"), [{"x": 6, "y": 7}, {"x": 8, "y": 9}])
raise Exception("test") # 예외 발생 시뮬레이션
위 코드에서는 INSERT
작업 후 의도적으로 예외를 발생시켰습니다. 실행 결과를 보면 트랜잭션이 시작되고 INSERT
쿼리가 실행되었지만, 예외 발생으로 인해 ROLLBACK
이 수행된 것을 확인할 수 있습니다.
2025-04-25 00:03:30,582 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2025-04-25 00:03:30,583 INFO sqlalchemy.engine.Engine INSERT INTO some_table (x, y) VALUES (?, ?)
2025-04-25 00:03:30,583 INFO sqlalchemy.engine.Engine [cached since 0.009627s ago] [(6, 7), (8, 9)]
2025-04-25 00:03:30,584 INFO sqlalchemy.engine.Engine ROLLBACK
# 예외 없이 정상적으로 완료되는 경우
with engine.begin() as conn:
conn.execute(text("INSERT INTO some_table (x, y) VALUES (:x, :y)"), [{"x": 10, "y": 20}, {"x": 30, "y": 40}])
# conn.commit() # begin() 사용 시 명시적 커밋 불필요
성공적으로 완료된 경우, COMMIT
이 자동으로 수행됩니다.
2025-04-25 00:00:13,047 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2025-04-25 00:00:13,049 INFO sqlalchemy.engine.Engine INSERT INTO some_table (x, y) VALUES (?, ?)
2025-04-25 00:00:13,050 INFO sqlalchemy.engine.Engine [cached since 9.264s ago] [(10, 20), (30, 40)]
2025-04-25 00:00:13,051 INFO sqlalchemy.engine.Engine COMMIT
흥미로운 점은 begin()
블록 시작 시 로그에 BEGIN (implicit)
이라고 표시되는 부분입니다. 이는 실제 데이터베이스에 BEGIN
명령을 보내는 것이 아니라, SQLAlchemy 내부적으로 트랜잭션이 시작되었음을 논리적으로 표시하는 것입니다. 실제 DB 트랜잭션은 첫 번째 SQL 명령이 실행될 때 시작됩니다. engine.begin()
은 개발자의 실수를 줄이고 보다 안정적인 트랜잭션 관리를 가능하게 합니다.
다음 코드로 롤백된 데이터 (6, 7)
, (8, 9)
는 테이블에 존재하지 않고, 커밋된 데이터만 존재하는 것을 확인할 수 있습니다.
"""
롤백된 데이터가 안들어간것 확인 (6,7), (8,9) 데이터가 롤백된것 확인
"""
with engine.begin() as conn:
result = conn.execute(text("SELECT x, y FROM some_table"))
for row in result:
print(f"x: {row.x}, y: {row.y}")
2025-04-25 00:02:22,486 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2025-04-25 00:02:22,488 INFO sqlalchemy.engine.Engine SELECT x, y FROM some_table
2025-04-25 00:02:22,489 INFO sqlalchemy.engine.Engine [cached since 126.4s ago] ()
x: 1, y: 2
x: 3, y: 4
x: 1, y: 2 # 이전에 conn.commit()으로 추가된 데이터
x: 3, y: 4 # 이전에 conn.commit()으로 추가된 데이터
x: 10, y: 20 # engine.begin()으로 추가된 데이터
x: 30, y: 40 # engine.begin()으로 추가된 데이터
2025-04-25 00:02:22,490 INFO sqlalchemy.engine.Engine COMMIT
쿼리 결과 처리: Result와 Row 객체
conn.execute()
메소드는 Result
객체를 반환합니다. 이 객체는 데이터베이스 쿼리 결과를 나타내며, 순회 가능(iterable)한 특징을 가집니다. 따라서 for
루프 등을 사용하여 결과 행(row)들을 하나씩 처리하거나, all()
, first()
, scalar()
등 다양한 메소드를 활용하여 결과를 원하는 형태로 변환(transform)할 수 있습니다.
# Result 객체 타입 확인 및 순회 예시
with engine.begin() as conn:
result = conn.execute(text("SELECT x, y FROM some_table WHERE x > 5")) # 조건 추가
print(type(result))
for row in result:
print(f"x: {row.x}, y: {row.y}")
# (이전 실행 로그 생략)
2025-04-25 00:08:21,655 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2025-04-25 00:08:21,656 INFO sqlalchemy.engine.Engine SELECT x, y FROM some_table WHERE x > 5 # WHERE 조건 추가됨
2025-04-25 00:08:21,657 INFO sqlalchemy.engine.Engine [generated in 0.00043s] ()
<class 'sqlalchemy.engine.cursor.CursorResult'>
x: 10, y: 20
x: 30, y: 40
2025-04-25 00:08:21,658 INFO sqlalchemy.engine.Engine COMMIT
Result
객체를 순회할 때 얻어지는 각 행은 Row
객체입니다. Row
객체는 파이썬의 collections.namedtuple
과 유사하게 동작하는 불변(immutable) 자료구조입니다. 각 컬럼의 값에 접근할 때 속성(attribute) 방식(row.x
, row.y
)이나 인덱스 방식(row[0]
, row[1]
)을 사용할 수 있습니다.
Row
객체의 독특한 특징 중 하나는 언패킹(unpacking)이 가능하다는 점입니다. 이를 활용하면 조회된 데이터를 사용자 정의 객체나 다른 자료구조로 쉽게 변환할 수 있습니다.
1. 위치 인자 언패킹 (*row
): Row
객체를 *
연산자와 함께 사용하면, 각 컬럼 값이 순서대로 함수의 인자로 전달됩니다.
class Data():
def __init__(self, x, y) -> None:
self.x = x
self.y = y
def __repr__(self) -> str:
return f"Data ({self.x}, {self.y})"
with engine.begin() as conn:
result = conn.execute(text("SELECT x, y FROM some_table WHERE x < 5"))
"""
위치 인자 언패킹을 활용한 데이터 변환
"""
data = [Data(*row) for row in result]
print(data)
# (이전 실행 로그 생략)
2025-04-25 00:21:09,976 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2025-04-25 00:21:09,978 INFO sqlalchemy.engine.Engine SELECT x, y FROM some_table WHERE x < 5
2025-04-25 00:21:09,979 INFO sqlalchemy.engine.Engine [cached since 768.3s ago] ()
[Data (1, 2), Data (3, 4), Data (1, 2), Data (3, 4)]
2025-04-25 00:21:09,980 INFO sqlalchemy.engine.Engine COMMIT
2. 키워드 인자 언패킹 (**row._mapping
): Row
객체는 내부적으로 컬럼 이름을 키(key)로, 값을 값(value)으로 가지는 매핑(mapping) 인터페이스를 제공합니다 (_mapping
속성). **
연산자와 _mapping
속성을 함께 사용하면, 컬럼 이름과 값을 키워드 인자로 전달할 수 있습니다. 이는 생성자나 함수가 키워드 인자를 받을 때 유용합니다.
class Data():
def __init__(self, **kwargs) -> None: # 키워드 인자를 받도록 수정
self.x = kwargs["x"]
self.y = kwargs["y"]
def __repr__(self) -> str:
return f"Data ({self.x}, {self.y})"
with engine.begin() as conn:
result = conn.execute(text("SELECT x, y FROM some_table WHERE x < 5"))
"""
키워드 인자 언패킹을 활용한 데이터 변환
"""
data = [Data(**row._mapping) for row in result]
print(data)
# (이전 실행 로그 생략)
2025-04-25 00:21:13,280 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2025-04-25 00:21:13,282 INFO sqlalchemy.engine.Engine SELECT x, y FROM some_table WHERE x < 5
2025-04-25 00:21:13,283 INFO sqlalchemy.engine.Engine [cached since 771.6s ago] ()
[Data (1, 2), Data (3, 4), Data (1, 2), Data (3, 4)]
2025-04-25 00:21:13,284 INFO sqlalchemy.engine.Engine COMMIT
이러한 언패킹 기능은 데이터베이스 조회 결과를 파이썬 객체로 변환하는 과정을 매우 간결하고 직관적으로 만들어 줍니다.
결론
SQLAlchemy를 사용하여 데이터베이스와 상호작용할 때, Connection
객체를 통한 명시적인 트랜잭션 관리("Commit as you go")와 engine.begin()
을 활용한 자동 트랜잭션 관리("Begin once") 방법을 이해하는 것은 중요합니다. 또한, 쿼리 결과를 담는 Result
객체의 순회 가능한 특성과 Row
객체의 불변성 및 언패킹 기능을 활용하면 데이터를 효율적으로 처리하고 원하는 형태로 변환하는 데 큰 도움이 됩니다. 이러한 SQLAlchemy의 핵심 기능들을 잘 이해하고 활용한다면 더욱 견고하고 효율적인 데이터베이스 애플리케이션을 구축할 수 있을 것입니다.
Subscribe to my newsletter
Read articles from roach directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
