DB/몽고DB 완벽 가이드 3판

[8장] 트랜잭션

꾸준함. 2025. 4. 16. 00:59

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로 전달되는 명시적 논리 세션 필요

 

 

2.1 코어 API 예제

  • 트랜잭션의 두 가지 작업은 아래 프로그램 목록의 Step 1에서 강조됨


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
view raw .py hosted with ❤ by GitHub

 

2.2 콜백 API 예제

  • 트랜잭션의 두 가지 작업은 아래 프로그램 목록의 Step 1에서 강조됨


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)
view raw .py hosted with ❤ by GitHub

 

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