백엔드/Laravel

[트랜잭션] 데이터 일관성을 위한 트랜잭션 개념과 실무 적용

알쓸신개 2025. 2. 8. 22:10

 

1. 트랜잭션이란?

트랜잭션은 하나의 논리적 작업 단위를 의미하며, 데이터베이스에서 여러 개의 작업이 모두 성공하거나, 하나라도 실패하면 전체 작업을 되돌리는(rollback) 기법을 제공합니다.

트랜잭션의 4가지 필수 특성 (ACID)

  • Atomicity (원자성): 모든 연산이 하나의 단위로 실행되며, 하나라도 실패하면 전체가 실패합니다.
  • Consistency (일관성): 트랜잭션이 성공적으로 완료되면 데이터베이스가 일관된 상태를 유지해야 합니다.
  • Isolation (고립성): 여러 트랜잭션이 동시에 실행되더라도 서로 간섭받지 않으며, 독립적으로 실행됩니다.
  • Durability (지속성): 트랜잭션이 성공적으로 완료되면 그 변경 사항은 영구적으로 저장됩니다.

2. 트랜잭션을 사용하는 이유

2-1. 데이터 무결성(Data Integrity) 보장

트랜잭션이 없다면, 일부 SQL만 실행된 후 서버 오류가 발생하면 데이터가 불완전한 상태가 될 수 있습니다. 트랜잭션은 데이터베이스의 상태를 일관되게 유지합니다.

2-2. 동시성 제어(Concurrency Control)

여러 사용자가 동시에 같은 데이터를 수정할 경우, 트랜잭션을 통해 경합 상태(Race Condition)를 방지합니다.

2-3. 예외 상황 대비

네트워크 장애나 시스템 오류가 발생해도 데이터를 안전하게 유지할 수 있습니다.

예시:

  • 은행 계좌 이체 처리
    • A 계좌에서 100만 원 차감
    • B 계좌에서 100만 원 추가
    • 만약 A 계좌에서 차감된 후 시스템 오류로 인해 B 계좌에 입금이 되지 않는다면, 트랜잭션을 사용하면 자동으로 롤백되어 문제를 방지할 수 있습니다.

3. Laravel에서 트랜잭션 적용 방법

3-1. 기본 트랜잭션 관리

use Illuminate\Support\Facades\DB;

DB::beginTransaction(); // 트랜잭션 시작

try {
    // 여러 개의 데이터 삽입, 수정, 삭제 실행
    DB::table('users')->insert(['name' => 'John Doe', 'email' => 'john@example.com']);
    DB::table('orders')->insert(['user_id' => 1, 'amount' => 10000]);

    DB::commit(); // 모두 성공하면 커밋
} catch (\Exception $e) {
    DB::rollback(); // 실패하면 롤백
    throw $e; // 예외를 다시 던짐
}

3-2. DB::transaction() 활용

DB::transaction(function () {
    DB::table('users')->insert(['name' => 'Jane Doe', 'email' => 'jane@example.com']);
    DB::table('orders')->insert(['user_id' => 2, 'amount' => 15000]);
});
  • 이렇게 하면 Laravel이 자동으로 commit()과 rollback()을 처리합니다.

3-3. Eloquent 모델에서 트랜잭션 사용

use App\Models\User;
use App\Models\Order;

DB::transaction(function () {
    $user = User::create(['name' => 'Mike', 'email' => 'mike@example.com']);
    Order::create(['user_id' => $user->id, 'amount' => 20000]);
});

4. 트랜잭션과 성능 최적화

4-1. Deadlock 방지 전략

  • 같은 순서로 테이블을 접근하여 데드락 발생 가능성을 줄입니다.
  • 트랜잭션 범위를 최소화하여 데이터베이스 리소스를 절약합니다.

4-2. 트랜잭션 범위 최소화

트랜잭션이 너무 오래 유지되면 DB 성능이 저하될 수 있습니다.

DB::transaction(function () {
    // 트랜잭션 내에서 최소한의 작업만 실행
    DB::table('accounts')->where('id', 1)->update(['balance' => 5000]);
});

4-3. 데이터베이스 잠금과 트랜잭션

  • SELECT ... FOR UPDATE: 특정 행을 잠금
  • LOCK IN SHARE MODE: 읽기 잠금
DB::table('orders')->where('id', 1)->lockForUpdate()->get();

5. 실무 적용 사례

5-1. 주문 처리 시스템

  • 주문 생성
  • 재고 차감
  • 결제 처리
  • 주문 상태 변경

이 모든 과정이 트랜잭션 내에서 실행되어야 합니다.

DB::transaction(function () use ($user, $orderData) {
    $order = Order::create([
        'user_id' => $user->id,
        'status' => 'pending',
    ]);

    Inventory::where('product_id', $orderData['product_id'])
        ->decrement('stock', $orderData['quantity']);

    Payment::create([
        'order_id' => $order->id,
        'amount' => $orderData['total_price'],
    ]);
});
  • 이때, 어느 하나라도 실패하면 자동으로 롤백됩니다.

6. 트랜잭션 관련 자주 발생하는 문제와 해결책

6-1. 트랜잭션 롤백이 작동하지 않는 경우

Laravel에서는 기본적으로 예외가 발생하면 rollback()이 호출됩니다. 하지만 예외를 catch()한 후에는 rollback()이 자동으로 호출되지 않을 수 있습니다.
해결책: 예외를 다시 던져야 합니다.

try {
    DB::transaction(function () {
        DB::table('users')->insert(['name' => 'Test']);
        throw new Exception("강제 오류 발생");
    });
} catch (\Exception $e) {
    // 예외를 catch 했지만 rollback이 자동으로 수행되지 않음
}
  • 해결책: throw를 사용하여 예외를 다시 던집니다.

6-2. Deadlock(교착 상태) 발생

  • 같은 데이터를 여러 트랜잭션이 동시에 수정할 때 발생할 수 있습니다.
    해결책: 트랜잭션 재시도 로직 추가
for ($i = 0; $i < 3; $i++) {
    try {
        DB::transaction(function () {
            DB::table('products')->where('id', 1)->update(['stock' => DB::raw('stock - 1')]);
        });
        break; // 성공하면 루프 종료
    } catch (\Exception $e) {
        sleep(1); // 잠시 대기 후 재시도
    }
}

7. 결론

트랜잭션을 활용하면 데이터 무결성을 유지하면서도 안정적인 시스템을 구축할 수 있습니다. 특히 Laravel에서는 트랜잭션을 효율적으로 처리할 수 있는 다양한 방법을 제공하므로, 이를 잘 활용하여 시스템의 안정성을 높일 수 있습니다.


8. 참고 링크

MY SQL 트랜잭션에 대한 상세 설명

Inpa 블로그 - 트랜잭션이란?