백엔드/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에서는 트랜잭션을 효율적으로 처리할 수 있는 다양한 방법을 제공하므로, 이를 잘 활용하여 시스템의 안정성을 높일 수 있습니다.