복잡한 결제 처리나 회원 가입 과정에서 여러 개의 데이터베이스 쿼리를 연속으로 실행하다가 중간에 에러가 발생하여 데이터가 꼬인 경험이 있을 것이다.
다만 트랜잭션의 정확한 작동 원리나 예외 처리 방식을 모른 채 개별 쿼리만 연속으로 던지는 개발자가 많다.
이번에는 PHP PDO 환경에서 데이터 정합성을 완벽하게 보장하는 트랜잭션(Transaction)의 기본 개념부터 commit, rollback 사용법 및 실무 주의사항까지 완벽하게 정리해서 소개하겠다.

 

1. PDO 트랜잭션(Transaction)이란 무엇인가?

트랜잭션은 데이터베이스의 상태를 변화시키는 여러 작업들을 하나로 묶은 작업 단위다. 계좌 이체를 예로 들면, 'A의 계좌에서 돈을 차감'하는 쿼리와 'B의 계좌에 돈을 입금'하는 쿼리는 반드시 둘 다 성공하거나, 둘 다 취소되어야 한다. 중간에 서버가 다운되거나 에러가 발생해서 한쪽만 실행되면 치명적인 데이터 불일치가 발생한다.

트랜잭션이 안전하게 수행되기 위해서는 ACID 특성을 만족해야 한다.

특성설명실무 적용 의미
원자성 (Atomicity)트랜잭션 내 작업은 모두 반영되거나 모두 취소되어야 함중간 실패 시 전체 작업 롤백
일관성 (Consistency)작업 전후에 데이터베이스 일관성이 유지되어야 함제약 조건(FK, Unique) 위반 방지
격리성 (Isolation)동시 실행 중인 트랜잭션이 서로 영향을 주지 않음동시성 제어 및 데이터 락 관리
영속성 (Durability)성공적으로 완료된 결과는 영구적으로 저장됨커밋 완료 후 데이터 안전성 보장

 

2. PDO에서 트랜잭션을 제어하는 주요 메서드

PHP PDO 라이브러리는 트랜잭션을 손쉽게 제어할 수 있는 세 가지 핵심 메서드를 제공한다.

1. $pdo->beginTransaction(): 자동 커밋(Autocommit) 모드를 비활성화하고 새로운 트랜잭션을 시작한다.
2. $pdo->commit(): 트랜잭션 내에서 수행된 모든 쿼리 결과를 데이터베이스에 영구적으로 반영한다.
3. $pdo->rollBack(): 트랜잭션 내에서 수행된 모든 쿼리 작업을 취소하고 트랜잭션 시작 이전 상태로 되돌린다.

이때 가장 중요한 전제 조건은 **PDO의 에러 모드가 Exception(예외 발생)을 던지도록 설정되어 있어야 한다**는 점이다.

 

3. 실전 예제: 포인트 차감 및 결제 내역 저장

사용자의 포인트를 차감하고 결제 내역 테이블에 기록을 남기는 작업을 예제로 비교해 보자.

 

✗ 잘못된 예제: 트랜잭션 미적용 또는 예외 처리 누락

아래 코드는 트랜잭션을 적용하지 않거나 에러 모드를 설정하지 않은 전형적인 나쁜 예시다.

<?php
// 에러 모드가 설정되지 않아 쿼리 실패 시에도 Exception이 발생하지 않음
$pdo = new PDO("mysql:host=localhost;dbname=test_db;charset=utf8mb4", "dbuser", "dbpassword");

$userId = 10;
$amount = 5000;

// 1. 포인트 차감 쿼리
$stmt1 = $pdo->prepare("UPDATE users SET point = point - :amount WHERE id = :id");
$stmt1->execute(['amount' => $amount, 'id' => $userId]);

// 만약 여기서 서버 장애가 발생하거나 아래 쿼리에 컬럼명 오타 등 에러가 발생한다면?
// 포인트는 이미 차감되었지만 내역 테이블 기록은 남지 않는 심각한 결제가 발생한다!
$stmt2 = $pdo->prepare("INSERT INTO payment_history_wrong (user_id, amount) VALUES (:id, :amount)");
$stmt2->execute(['id' => $userId, 'amount' => $amount]);
?>

✗ **문제점**: UPDATE 쿼리는 즉시 DB에 적용되어 포인트가 차감되지만, 뒤따르는 INSERT 쿼리가 실패할 경우 전체 작업이 취소되지 않고 DB 데이터가 완전히 꼬이게 된다.

 

✓ 올바른 예제: try-catch와 PDO 트랜잭션 적용

PDO에 에러 모드를 Exception으로 지정하고, try-catch 구조 내부에서 beginTransaction, commit, rollBack을 완벽하게 다루는 코드다.

<?php
try {
    $pdo = new PDO(
        "mysql:host=localhost;dbname=test_db;charset=utf8mb4",
        "dbuser",
        "dbpassword",
        [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 에러 발생 시 PDOException을 던지도록 필수 설정
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
        ]
    );

    // 1. 트랜잭션 시작 (Autocommit 중단)
    $pdo->beginTransaction();

    $userId = 10;
    $amount = 5000;

    // 2. 사용자 포인트 차감
    $stmt1 = $pdo->prepare("UPDATE users SET point = point - :amount WHERE id = :id AND point >= :amount");
    $stmt1->execute(['amount' => $amount, 'id' => $userId]);

    // 영향받은 행이 없다면 잔액 부족으로 판단하고 예외 발생
    if ($stmt1->rowCount() === 0) {
        throw new Exception("잔액이 부족하거나 존재하지 않는 사용자입니다.");
    }

    // 3. 결제 내역 기록
    $stmt2 = $pdo->prepare("INSERT INTO payment_history (user_id, amount, created_at) VALUES (:id, :amount, NOW())");
    $stmt2->execute(['id' => $userId, 'amount' => $amount]);

    // 4. 모든 작업이 정상 완료되면 커밋하여 DB에 영구 반영
    $pdo->commit();
    echo "결제가 성공적으로 완료되었습니다.";

} catch (Exception $e) {
    // 5. 트랜잭션이 활성화되어 있다면 안전하게 롤백 수행
    if (isset($pdo) && $pdo->inTransaction()) {
        $pdo->rollBack();
    }
    
    // 에러 로그 기록 및 예외 처리
    error_log("Transaction Failed: " . $e->getMessage());
    echo "처리 중 오류가 발생하여 전체 작업이 취소되었습니다: " . $e->getMessage();
}
?>

✓ **출력 결과 (성공 시)**:
결제가 성공적으로 완료되었습니다.

✓ **출력 결과 (실패 시)**:
처리 중 오류가 발생하여 전체 작업이 취소되었습니다: 잔액이 부족하거나 존재하지 않는 사용자입니다.

 

4. 실무에서 흔히 하는 실수 및 주의사항

트랜잭션을 도입할 때 개발자들이 자주 놓치는 대표적인 주의사항 3가지를 정리했다.

  • InnoDB 엔진 확인: MySQL 기준 MyISAM 테이블 엔진은 트랜잭션을 지원하지 않는다. 반드시 InnoDB 엔진으로 테이블이 생성되어 있는지 확인하자.
  • DDL 문장 사용 금지: 트랜잭션 도중에 CREATE TABLE, ALTER TABLE, DROP TABLE 등의 DDL(Data Definition Language) 문을 실행하면 MySQL 내에서 **암묵적 커밋(Implicit Commit)**이 발생하여 이전의 트랜잭션 블록이 강제로 확정된다.
  • inTransaction() 검증: catch 블록에서 롤백을 호출할 때는 $pdo->inTransaction() 메서드를 활용해 현재 트랜잭션이 활성화 상태인지 반드시 체크하자. 이미 롤백되었거나 시작되지 않은 상태에서 rollBack()을 호출하면 별도의 PDOException이 발생한다.

 

PHP PDO Transaction 데이터 꼬임 방지는 백엔드 시스템의 데이터 정합성과 직결되는 가장 기본적인 요소다. 예외 처리와 트랜잭션이라는 작은 모범 사례가 모여서 서비스의 높은 신뢰성을 만든다는 점을 잊지 말자. 이 글의 catch 블록과 inTransaction() 검증 예제를 참고해 기존 데이터베이스 로직에 적용하면, 어떠한 오류 상황에서도 꼬임 없는 안전한 백엔드 시스템을 얻을 수 있을 것이다.