1. 트랜잭션 소개
- 트랜잭션은 읽기나 쓰기 작업이 가능한 데이터베이스 작업을 하나 이상 포함하는 데이터베이스의 논리적 단위
- 트랜잭션의 중요한 특징은 작업이 성공하든 실패하든 부분적으로는 완료되지 않는다는 점
- 몽고DB에서 트랜잭션을 사용하려면 버전이 4.2 이상이어야 하며 몽고DB 드라이버를 몽고DB 4.2 이상에 맞게 갱신해야 함
1.1 ACID의 정의
- 트랜잭션이 `진정한` 트랜잭션이 되려면 ACID라는 속성을 충족해야 함
- ACID는 원자성 (Atomicity), 일관성 (Consistency), 고립성 (Isolation), 그리고 영속성 (Durability)의 약어
- ACID 트랜잭션은 오류가 발생할 때도 데이터와 데이터베이스의 상태의 유효성을 보장
- 원자성 (Atomicity)은 트랜잭션 내 모든 작업이 적용되거나 아무 작업도 적용되지 않도록 보장하며 트랜잭션은 부분적으로 적용될 수 없음
- 즉, 커밋되거나 중단됨
- 일관성 (Consistency)은 트랜잭션이 성공하면 데이터베이스가 하나의 일관성 있는 상태에서 다음 일관성 있는 상태로 이동하도록 보장
- 고립성 (Isolation)은 여러 트랜잭션이 데이터베이스에서 동시에 실행되도록 허용하는 속성
- 트랜잭션이 다른 트랜잭션의 부분 결과를 보지 않도록 보장
- 여러 병령 트랜잭션이 각 트랜잭션을 순차적으로 실행할 때와 동일한 결과를 얻게 됨
- 영속성 (Durability)은 트랜잭션이 커밋될 때 시스템 오류가 발생하더라도 모든 데이터가 유지되도록 보장
- 데이터베이스는 앞서 나열한 속성을 모두 충족하고 성공적인 트랜잭션만 처리될 때 ACID를 준수한다고 함
- 트랜잭션이 완료되기 전에 오류가 발생할 경우 ACID 준수는 데이터가 변경되지 않게 보장
- 몽고DB는 복제 셋과 샤드 전체에 ACID 호환 트랜잭션이 있는 분산 데이터베이스
2. 트랜잭션 사용법
- 몽고DB는 트랜잭션을 사용하기 위한 두 가지 API를 제공
- 첫 번째는 코어 API라는 관계형 데이터베이스와 유사한 구문 i.g. start_transaction, commit_transaction
- 두 번째는 트랜잭션 사용에 권장되는 접근 방식인 콜백 AP.I
- 코어 API는 대부분의 오류에 재시도 로직을 제공하지 않으며 개발자가 작업에 대한 로직, 트랜잭션 커밋 함수, 그리고 필요한 재시도 및 오류 로직을 모두 작성해야 함
- 콜백 API는 지정된 논리 세션과 관련된 트랜잭션 시작, 콜백 함수로 제공된 함수 실행, 트랜잭션 커밋 또는 오류 시 중단을 포함해 코어 API에 비해 많은 기능을 Wrapping 하는 단일 함수를 제공
- 해당 함수는 커밋 오류를 처리하는 재시도 로직도 포함
- 콜백 API는 몽고DB 4.2에 추가돼 트랜잭션을 통해 애플리케이션 개발을 단순화하고 트랜잭션 오류를 처리하는 애플리케이션 재시도 로직을 쉽게 추가함
- 두 API에서 개발자는 트랜잭션에서 사용할 논리 세션을 시작해야 하며, 트랜잭션의 작업이 특정 논리 세션과 연결돼야 함
- 몽고DB 논리 세션은 전체 몽고DB 배포 컨텍스트에서 작업의 시간과 순서를 추적
- 논리 세션 또는 서버 세션은 몽고DB에서 재시도 가능한 쓰기와 인과적 일관성을 지원하기 위해 클라이언트 세션에서 사용하는 기본 프레임워크의 일부
- 순서에 따라 인과관계가 반영된 읽기 및 쓰기 작업의 시퀀스는 몽고DB에서 인과관계가 있는 클라이언트 세션으로 정의됨
- 클라이언트 세션은 애플리케이션에 의해 시작되며 서버 세션과 상호작용하는 데 사용됨
- 애플리케이션이 복잡하고 추가 코드 작성이 필요할 때 코어 API보다 콜백 API를 권장
코어 API | 콜백 API |
트랜잭션을 시작하고 커밋하려면 명시적인 호출 필요 | 트랜잭션을 시작하고 지정된 작업을 실행한 뒤 커밋 (오류 시 중단) |
TransientTransactionError 및 UnknownTransactionCommitResult에 대한 오류 처리 로직을 통합하지 않은 대신 사용자 지정 오류 처리를 통합하는 유연성 제공 | TransientTransactionError 및 UnknownTransactionCommitResult에 대한 오류 처리 로직을 자동으로 통합 |
특정 트랜잭션을 위해 API로 전달되는 명시적 논리 세션 필요 |
- 자세한 내용은 [몽고DB의 클러스터 전체 논리 클록과 인과적 일관성 구현] 논문 참고
2.1 코어 API 예제
- 트랜잭션의 두 가지 작업은 아래 프로그램 목록의 Step 1에서 강조됨
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from pymongo import MongoClient, WriteConcern, ReadConcern, ReadPreference | |
from pymongo.errors import ConnectionFailure, OperationFailure | |
# MongoDB Core API 예제 코드 | |
# 이 코드는 트랜잭션을 사용하여 'webshop' 데이터베이스의 주문(orders)과 인벤토리(inventory) 컬렉션을 업데이트합니다. | |
# MongoDB 클러스터에 연결할 URI를 지정합니다. | |
uri = 'mongodb+srv://server.example.com/' | |
client = MongoClient(uri) # MongoDB 클러스터에 연결합니다. | |
# majority 쓰기 우선순위와 타임아웃(1000ms)을 가진 WriteConcern 객체를 생성합니다. | |
my_wc_majority = WriteConcern('majority', wtimeout=1000) | |
# 전제 조건: | |
# Step 0: 컬렉션이 없으면 미리 생성합니다. | |
# 트랜잭션 내의 CRUD 작업은 반드시 기존에 생성된 컬렉션에서 수행되어야 합니다. | |
client.get_database("webshop", write_concern=my_wc_majority).orders.insert_one({"sku": "abc123", "qty": 0}) | |
client.get_database("webshop", write_concern=my_wc_majority).inventory.insert_one({"sku": "abc123", "qty": 1000}) | |
# Step 1: 트랜잭션 내에서 수행할 작업들과 그 순서를 정의합니다. | |
def update_orders_and_inventory(my_session): | |
# 세션을 통해 'webshop' 데이터베이스의 주문(orders)와 인벤토리(inventory) 컬렉션에 접근합니다. | |
orders = my_session.client.webshop.orders | |
inventory = my_session.client.webshop.inventory | |
# 트랜잭션을 시작합니다. | |
with my_session.start_transaction( | |
read_concern=ReadConcern("snapshot"), | |
write_concern=WriteConcern(w="majority"), | |
read_preference=ReadPreference.PRIMARY): | |
# 주문 컬렉션에 주문을 추가합니다. (예: sku "abc123"의 수량 100 주문) | |
orders.insert_one({"sku": "abc123", "qty": 100}, session=my_session) | |
# 인벤토리 컬렉션에서 재고가 100 이상인 항목에 대해, 재고를 100 감소시킵니다. | |
inventory.update_one({"sku": "abc123", "qty": {"$gte": 100}}, | |
{"$inc": {"qty": -100}}, | |
session=my_session) | |
# 트랜잭션 커밋 작업을 재시도 로직을 통해 수행합니다. | |
commit_with_retry(my_session) | |
# Step 2: 트랜잭션 커밋을 시도하고, 일시적 오류 발생 시 재시도하는 로직입니다. | |
def commit_with_retry(session): | |
while True: | |
try: | |
# 트랜잭션 시작 시 설정된 쓰기 우선순위를 반영하여 커밋합니다. | |
session.commit_transaction() | |
print("트랜잭션이 커밋되었습니다.") | |
break # 커밋 성공 시 루프에서 탈출합니다. | |
except (ConnectionFailure, OperationFailure) as exc: | |
# 커밋 중 오류가 발생한 경우, 재시도 가능한 오류인지 확인합니다. | |
if exc.has_error_label("UnknownTransactionCommitResult"): | |
print("UnknownTransactionCommitResult 발생, 커밋 작업 재시도 중...") | |
continue # 재시도합니다. | |
else: | |
print("커밋 중 오류 발생...") | |
raise # 재시도 불가능한 오류이면 예외를 발생시킵니다. | |
# Step 3: 트랜잭션 함수를 실행하며, 일시적 오류가 발생한 경우 전체 트랜잭션을 재시도하는 로직입니다. | |
def run_transaction_with_retry(txn_func, session): | |
while True: | |
try: | |
# 전달받은 트랜잭션 함수를 실행합니다. | |
txn_func(session) | |
break # 함수 실행이 성공하면 루프에서 탈출합니다. | |
except (ConnectionFailure, OperationFailure) as exc: | |
# 일시적인 트랜잭션 오류 발생 시 전체 트랜잭션을 재시도합니다. | |
if exc.has_error_label("TransientTransactionError"): | |
print("TransientTransactionError 발생, 트랜잭션 재시도 중...") | |
continue # 재시도합니다. | |
else: | |
raise # 일시적 오류가 아니면 예외를 발생시킵니다. | |
# Step 4: 세션을 시작합니다. | |
with client.start_session() as my_session: | |
# Step 5: 'update_orders_and_inventory' 함수를 실행하는 트랜잭션을 | |
# 'run_transaction_with_retry' 함수를 통해 실행하여, my_session과 연계합니다. | |
try: | |
run_transaction_with_retry(update_orders_and_inventory, my_session) | |
except Exception as exc: | |
# 오류 발생 시 추가적인 에러 처리 로직을 구현할 수 있습니다. | |
# Core API에서는 에러 처리 코드가 기본적으로 제공되지 않습니다. | |
raise |
2.2 콜백 API 예제
- 트랜잭션의 두 가지 작업은 아래 프로그램 목록의 Step 1에서 강조됨
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from pymongo import MongoClient, WriteConcern, ReadConcern, ReadPreference | |
# MongoDB Callback API 예제 코드 | |
# 이 코드는 'webshop' 데이터베이스 내의 'orders'와 'inventory' 컬렉션에 대해 | |
# 트랜잭션을 이용한 작업을 콜백 함수로 실행하는 방법을 보여줍니다. | |
# MongoDB 클러스터에 연결할 URI를 설정합니다. | |
uriString = 'mongodb+srv://server.example.com/' | |
client = MongoClient(uriString) | |
# majority 쓰기 우선순위와 1000ms 타임아웃을 갖는 WriteConcern 객체를 생성합니다. | |
my_wc_majority = WriteConcern('majority', wtimeout=1000) | |
# 전제조건: | |
# Step 0: 컬렉션이 아직 존재하지 않으면 미리 생성합니다. | |
# 트랜잭션 내의 CRUD 작업은 이미 존재하는 컬렉션에서만 수행할 수 있습니다. | |
client.get_database("webshop", write_concern=my_wc_majority).orders.insert_one({"sku": "abc123", "qty": 0}) | |
client.get_database("webshop", write_concern=my_wc_majority).inventory.insert_one({"sku": "abc123", "qty": 1000}) | |
# Step 1: 트랜잭션 내에서 실행할 작업 순서를 정의하는 콜백(callback) 함수를 만듭니다. | |
def callback(my_session): | |
# 세션을 통해 'webshop' 데이터베이스의 'orders' 및 'inventory' 컬렉션에 접근합니다. | |
orders = my_session.client.webshop.orders | |
inventory = my_session.client.webshop.inventory | |
# 중요: 작업을 실행할 때 반드시 세션 변수 'my_session'을 인자로 전달해야 합니다. | |
orders.insert_one({"sksu": "abc123", "qty": 100}, session=my_session) | |
inventory.update_one({"sku": "abc123", "qty": {"$gte": 100}}, | |
{"$inc": {"qty": -100}}, | |
session=my_session) | |
# Step 2: 클라이언트 세션을 시작합니다. | |
with client.start_session() as session: | |
# Step 3: with_transaction()을 사용하여 트랜잭션을 시작하고 콜백 함수를 실행한 후, | |
# 에러 발생시 트랜잭션을 커밋하지 않고 중단 또는 재시도합니다. | |
session.with_transaction(callback, | |
read_concern=ReadConcern('local'), | |
write_concern=my_wc_majority, | |
read_preference=ReadPreference.PRIMARY) |
3. 애플리케이션을 위한 트랜잭션 제한 조정
- 트랜잭션을 사용할 때 숙지해야 할 몇 가지 매개변수가 있음
- 애플리케이션이 트랜잭션을 최적으로 사용하도록 매개변수를 조정해야 함
3.1 타이밍과 Oplog 크기 제한
- 몽고DB 트랜잭션에는 두 가지 주요 제한 범주가 있음
- 첫 번째는 트랜잭션의 시간제한, 즉 특정 트랜잭션이 실행될 수 있는 시간, 트랜잭션이 락을 획득하려고 대기하는 시간, 그리고 모든 트랜잭션이 실행될 최대 길이를 제어하는 것과 관련 있음
- 두 번째 범주는 특히 몽고DB oplog 항목과 개별 항목에 대한 크기 제한과 관련 있음
가. 시간 제한
- 트랜잭션의 최대 실행 시간은 기본적으로 1분 이하
- mongod 인스턴스 레벨에서 transactionLifetimeLimitSeconds에 의해 제어되는 제한을 수정해 증가시킬 수 있음
- 샤드 클러스터의 경우 모든 샤드 복제 셋 멤버에 매개변수를 설정해야 함
- 이 시간이 경과하면 트랜잭션이 만료됐다고 간주하며 주기적으로 실행되는 정리 프로세스에 의해 중단됨
- 정리 프로세스는 min(60초, transactionLifetimeLimitSeconds / 2) 값을 주기로 실행됨
- 트랜잭션에 시간 제한을 명시적으로 설정하려면 commitTransaction에 maxTimeMS를 지정하는 것이 좋음
- maxTimeMS를 설정하지 않으면 transactionLifetimeLimitSeconds가 사용됨
- maxTimeMS를 설정했지만 transactionLifetimeLimitSeconds를 초과하는 경우 transactionLifetimeLimitSeconds가 대신 사용됨
- 트랜잭션의 작업에 필요한 락을 획득하기 위해 트랜잭션이 대기하는 최대 시간은 기본적으로 5ms
- maxTransactionLockRequestTimeoutMillis에 의해 제어되는 제한을 수정해 늘릴 수 있음
- 이 시간 내 락을 획득할 수 없으면 트랜잭션은 중단됨
- maxTransactionLockRequestTimeoutMillis는 0, -1 또는 0보다 큰 숫자로 설정 가능
- 0으로 설정한 경우 필요한 모든 락을 즉시 획득할 수 없으면 트랜잭션이 중단됨
- -1로 설정하면 작업별 제한 시간이 maxTimeMS에 지정된 대로 사용됨
- 0보다 큰 숫자는 트랜잭션이 필요한 락을 획득하려고 시도하는 기간으로 해당 시간까지의 대기 시간을 구성함
나. Oplog 크기 제한
- 몽고DB는 트랜잭션의 쓰기 작업에 필요한 만큼 oplog 항목을 생성하지만 각 oplog 항목은 BSON 도큐먼트 크기 제한인 16MB 이하여야 함
정리
- 트랜잭션은 일관성을 보장하기 위해 몽고DB에서 유용한 기능을 제공하지만 풍부한 도큐먼트 모델과 함께 사용돼야 함
- 유연성 있는 모델과 스키마 설계 패턴과 같은 모범 사례를 사용하면 대부분의 상황에서 트랜잭션을 사용하지 않아도 됨
- 따라서 트랜잭션은 애플리케이션에서 드물게 사용하는 것이 좋은 강력한 기능
참고
몽고DB 완벽 가이드 3판 - 한빛미디어
반응형
'DB > 몽고DB 완벽 가이드 3판' 카테고리의 다른 글
[10장] 복제 셋 설정 (0) | 2025.04.22 |
---|---|
[9장] 애플리케이션 설계 (0) | 2025.04.21 |
[7장] 집계 프레임워크 (0) | 2025.04.12 |
[6장] 특수 인덱스와 컬렉션 유형 (0) | 2025.04.10 |
[5장] 인덱싱 (0) | 2025.04.04 |