Isar 개념 3 - Transaction

pharmDev·2025년 1월 1일

isar

목록 보기
4/7

Isar의 transaction 개념을 세 가지 방식으로 설명해드리겠습니다.

  1. 예시를 통한 설명
    은행 송금을 생각해보세요. A가 B에게 100만원을 송금할 때:
  2. A의 계좌에서 100만원을 차감
  3. B의 계좌에 100만원을 입금

이 두 작업은 반드시 함께 성공하거나 함께 실패해야 합니다. 만약 A의 계좌에서 돈이 빠져나갔는데 B의 계좌에 입금이 실패하면 큰 문제가 되겠죠. Isar의 transaction도 이와 같이 여러 데이터베이스 작업을 하나의 단위로 묶어서 처리합니다.

  1. 실제 코드 예시
// 트랜잭션 없이 데이터 처리
void riskyOperation() {
  final user = isar.users.get(1); // 사용자 정보 가져오기
  user.points -= 100; // 포인트 차감
  isar.users.put(user); // 사용자 정보 저장
  
  final item = isar.items.get(5); // 아이템 정보 가져오기
  item.quantity += 1; // 수량 증가
  isar.items.put(item); // 아이템 정보 저장
  // 중간에 오류가 발생하면 데이터 정합성이 깨질 수 있습니다
}

// 트랜잭션을 사용한 안전한 데이터 처리
void safeOperation() {
  isar.writeTxn(() async {
    final user = await isar.users.get(1);
    user.points -= 100;
    await isar.users.put(user);
    
    final item = await isar.items.get(5);
    item.quantity += 1;
    await isar.items.put(item);
    // 모든 작업이 성공적으로 완료되거나
    // 하나라도 실패하면 모든 변경사항이 취소됩니다
  });
}
  1. 비유를 통한 설명
    트랜잭션은 택배 포장과 비슷합니다:
  • 일반 작업: 각각의 물건을 개별 포장지로 싸서 보내는 것과 같습니다. 중간에 하나가 분실되거나 파손되면 다른 물건들은 이미 배송된 상태라 되돌리기 어렵습니다.

  • 트랜잭션: 여러 물건을 하나의 큰 박스에 담아 포장하는 것과 같습니다. 배송 중 문제가 발생하면 박스 전체가 반송되어 모든 물건이 원래 상태로 돌아갑니다.

추가 중요 개념:
1. 원자성(Atomicity): 트랜잭션 내의 모든 작업이 성공하거나, 아니면 모두 실패합니다.
2. 격리성(Isolation): 동시에 실행되는 트랜잭션들은 서로 영향을 주지 않습니다.
3. 읽기 트랜잭션:

await isar.txn(() async {
  // 읽기만 하는 경우 사용
  final user = await isar.users.get(1);
  final items = await isar.items.where().findAll();
});
  1. 쓰기 트랜잭션:
await isar.writeTxn(() async {
  // 데이터를 수정하는 경우 사용
  await isar.users.put(newUser);
  await isar.items.delete(123);
});

트랜잭션을 사용하면 앱의 데이터 일관성을 유지하고 안정성을 높일 수 있습니다. 특히 여러 테이블의 데이터를 동시에 수정해야 하는 경우에 매우 유용합니다.

// 1. 트랜잭션 없이 데이터를 처리하는 위험한 방식
void riskyOperation() {
  // 데이터베이스에서 ID가 1인 사용자 정보를 가져옵니다
  final user = isar.users.get(1); 
  
  // 사용자의 포인트를 100 차감합니다
  // 이 시점에서 앱이 종료되거나 오류가 발생하면 포인트만 차감된 상태로 남을 수 있습니다
  user.points -= 100; 
  
  // 변경된 사용자 정보를 데이터베이스에 저장합니다
  isar.users.put(user); 
  
  // 데이터베이스에서 ID가 5인 아이템 정보를 가져옵니다
  final item = isar.items.get(5); 
  
  // 아이템 수량을 1 증가시킵니다
  // 이전 작업이 실패했더라도 이 작업은 진행될 수 있어 데이터 불일치가 발생할 수 있습니다
  item.quantity += 1; 
  
  // 변경된 아이템 정보를 데이터베이스에 저장합니다
  isar.items.put(item); 
}

// 2. 트랜잭션을 사용한 안전한 데이터 처리 방식
void safeOperation() {
  // writeTxn으로 쓰기 트랜잭션 블록을 시작합니다
  // 이 블록 내의 모든 데이터베이스 작업은 하나의 단위로 처리됩니다
  isar.writeTxn(() async {
    // 1) 사용자 정보 업데이트
    // await를 사용하여 비동기 작업의 완료를 기다립니다
    final user = await isar.users.get(1);
    user.points -= 100;
    await isar.users.put(user);
    
    // 2) 아이템 정보 업데이트
    final item = await isar.items.get(5);
    item.quantity += 1;
    await isar.items.put(item);
    
    // 트랜잭션 블록이 끝나면 모든 변경사항이 함께 저장됩니다
    // 블록 내에서 오류가 발생하면 모든 변경사항이 취소됩니다
  });
}

// 3. 읽기 전용 트랜잭션 예시
void readOperation() async {
  // txn으로 읽기 전용 트랜잭션 블록을 시작합니다
  await isar.txn(() async {
    // 여러 개의 읽기 작업을 하나의 트랜잭션으로 묶어서 처리합니다
    // 이렇게 하면 데이터를 읽는 동안 다른 작업이 데이터를 변경할 수 없습니다
    final user = await isar.users.get(1);
    
    // where()를 사용하여 조건에 맞는 모든 아이템을 조회합니다
    final items = await isar.items.where()
        .quantityGreaterThan(0)  // 수량이 0보다 큰 아이템만 선택
        .findAll();  // 조건에 맞는 모든 아이템을 가져옵니다
    
    // 읽기 전용 트랜잭션에서는 데이터 수정이 불가능합니다
    // user.points += 100;  // 이런 수정 작업을 시도하면 오류가 발생합니다
  });
}

// 4. 복잡한 트랜잭션 예시 - 포인트 선물하기 기능
Future<bool> giftPoints(int fromUserId, int toUserId, int points) async {
  bool success = false;
  
  // 쓰기 트랜잭션 시작
  await isar.writeTxn(() async {
    // 1) 포인트를 주는 사용자 정보 확인
    final fromUser = await isar.users.get(fromUserId);
    if (fromUser == null || fromUser.points < points) {
      // 사용자가 없거나 포인트가 부족하면 트랜잭션 취소
      return;
    }
    
    // 2) 포인트를 받는 사용자 정보 확인
    final toUser = await isar.users.get(toUserId);
    if (toUser == null) {
      // 받는 사용자가 없으면 트랜잭션 취소
      return;
    }
    
    // 3) 포인트 이동 처리
    fromUser.points -= points;
    toUser.points += points;
    
    // 4) 변경사항 저장
    await isar.users.put(fromUser);
    await isar.users.put(toUser);
    
    // 5) 포인트 선물 기록 저장
    final pointGift = PointGift()
      ..fromUserId = fromUserId
      ..toUserId = toUserId
      ..points = points
      ..timestamp = DateTime.now();
    await isar.pointGifts.put(pointGift);
    
    success = true;
  });
  
  return success;
}

Isar 데이터베이스 트랜잭션 이해하기

Dart를 초급 수준으로 학습 중인 개발자를 위해 Isar 데이터베이스에서 트랜잭션 사용법을 쉽고 자세하게 설명하겠습니다.


트랜잭션이란?

트랜잭션은 여러 데이터베이스 작업을 하나의 작업 단위로 묶어 실행하는 것을 의미합니다. 예를 들어, 이메일 주소를 변경하면서 동시에 사용자 이름도 업데이트해야 한다고 가정해봅시다. 이 작업 중 하나라도 실패하면 데이터 불일치 문제가 생길 수 있습니다. 트랜잭션은 이 두 작업을 하나의 단위로 묶어, 둘 다 성공하거나 둘 다 실패하도록 보장합니다. Isar에서는 대부분의 읽기/쓰기 작업이 내부적으로 트랜잭션을 사용하며, 트랜잭션은 ACID(원자성, 일관성, 고립성, 지속성)를 준수합니다. 만약 작업 중 오류가 발생하면 트랜잭션은 자동으로 롤백되어 데이터 일관성을 보장합니다.

Isar는 “명시적 트랜잭션(explicit transactions)” 기능을 제공합니다. 이를 통해 개발자는 트랜잭션을 직접 제어할 수 있습니다.


명시적 트랜잭션

명시적 트랜잭션을 사용하면 데이터베이스 상태를 일관되게 유지할 수 있습니다. 트랜잭션 내에서는 네트워크 호출이나 오래 걸리는 작업을 수행해서는 안 됩니다. 또한 여러 작업을 하나의 트랜잭션으로 묶어 효율성을 높이는 것이 중요합니다.

트랜잭션 종류

  1. 동기 트랜잭션(Synchronous Transactions)

    • 동기적으로 트랜잭션을 처리합니다.
    • 읽기 전용: isar.txn()
    • 읽기/쓰기: isar.writeTxn()
  2. 비동기 트랜잭션(Asynchronous Transactions)

    • 비동기로 트랜잭션을 처리합니다.
    • 읽기 전용: isar.txnAsync()
    • 읽기/쓰기: isar.writeTxnAsync()
트랜잭션 종류읽기(Read)읽기/쓰기(Read & Write)
동기 트랜잭션txn()writeTxn()
비동기 트랜잭션txnAsync()writeTxnAsync()

읽기 트랜잭션 (Read Transactions)

명시적 읽기 트랜잭션은 선택 사항이지만, 특정 시점의 데이터베이스 상태를 일관되게 읽어야 할 때 유용합니다. 내부적으로 Isar는 대부분의 읽기 작업에서 암묵적으로 트랜잭션을 처리합니다.

코드 예시:

// 동기 읽기 트랜잭션 사용 예시
final result = isar.txnSync(() {
  return isar.contacts.where().findAllSync(); // 모든 연락처 조회
});

// 비동기 읽기 트랜잭션 사용 예시
final result = await isar.txnAsync(() async {
  return await isar.contacts.where().findAll(); // 모든 연락처 조회
});

위 코드에서는 isar.txnSyncisar.txnAsync를 사용해 데이터를 읽습니다.


쓰기 트랜잭션 (Write Transactions)

쓰기 작업은 반드시 명시적 트랜잭션 내에서 수행해야 합니다. 트랜잭션이 성공적으로 완료되면 모든 변경 사항이 자동으로 디스크에 저장됩니다. 오류가 발생하면 트랜잭션은 롤백되며 데이터 일관성을 유지합니다.

쓰기 트랜잭션 사용 시 주의사항

  • 트랜잭션이 실패하면 트랜잭션 내부에서 수행된 모든 작업이 취소됩니다.
  • 실패한 트랜잭션은 다시 사용할 수 없으며 새로 생성해야 합니다.

코드 예시:

import 'package:isar/isar.dart';


class Contact {
  Id id = Isar.autoIncrement; // 자동 증가 ID
  String name = '';
}

// 비효율적인 코드 예시
Future<void> badWriteExample(Isar isar) async {
  for (var contact in getContacts()) {
    await isar.writeTxn(() async {
      await isar.contacts.put(contact); // 개별적으로 데이터 저장
    });
  }
}

// 올바른 코드 예시
Future<void> goodWriteExample(Isar isar) async {
  await isar.writeTxn(() async {
    for (var contact in getContacts()) {
      await isar.contacts.put(contact); // 트랜잭션 내에서 모두 저장
    }
  });
}

// 데이터를 가져오는 함수 예시
List<Contact> getContacts() {
  return [
    Contact()..name = 'Alice',
    Contact()..name = 'Bob',
  ];
}

코드 설명

  1. 잘못된 예시(badWriteExample):

    • 각 데이터를 저장할 때마다 새로운 트랜잭션을 시작합니다. 이는 비효율적이며 성능 문제를 일으킬 수 있습니다.
  2. 올바른 예시(goodWriteExample):

    • 트랜잭션 하나로 여러 데이터를 처리하여 성능을 최적화합니다.

요약

  • Isar의 트랜잭션은 데이터베이스 작업의 일관성과 효율성을 유지하는 데 필수적입니다.
  • 읽기 작업은 필요에 따라 명시적 트랜잭션으로 묶을 수 있지만, 대부분의 경우 암묵적 트랜잭션으로 충분합니다.
  • 쓰기 작업은 반드시 명시적 트랜잭션으로 묶어야 하며, 여러 작업을 하나의 트랜잭션에 포함시키는 것이 효율적입니다.
  • 트랜잭션 실패 시 데이터를 롤백하여 안전성을 보장합니다.

위 내용을 통해 Isar 트랜잭션을 효과적으로 사용하는 방법을 익힐 수 있기를 바랍니다!

profile
코딩을 배우는 초보

0개의 댓글