데이터를 안전하게 — 트랜잭션과 동시성 제어
데이터는 단순히 저장만 하면 되는 게 아닙니다.
정확하게 저장되고, 동시에 여러 요청이 와도 꼬이지 않게 유지되는 것 —
그게 진짜 백엔드 시스템의 기본기입니다. 이번 글에서는 트랜잭션과 동시성 제어의 본질을
Node.js + Sequelize 예제와 함께 배워봅니다.
트랜잭션이란 무엇인가?
트랜잭션(Transaction)은 데이터베이스의 여러 연산을 하나의 논리적 단위로 묶는 것입니다.
예를 들어 ‘계좌 이체’는 “출금 + 입금”이 둘 다 성공해야만 유효하죠.
이 두 연산을 하나로 묶는 것이 바로 트랜잭션입니다.
트랜잭션의 4가지 특성 (ACID)
| 특성 | 설명 | 
|---|---|
| Atomicity (원자성) | 모든 작업이 완전히 수행되거나 전혀 수행되지 않아야 한다. | 
| Consistency (일관성) | 트랜잭션 전후에 데이터 무결성이 유지되어야 한다. | 
| Isolation (고립성) | 동시에 실행되는 트랜잭션이 서로에게 영향을 미치지 않는다. | 
| Durability (지속성) | 커밋된 데이터는 시스템 장애 후에도 유지된다. | 
왜 트랜잭션이 중요한가?
트랜잭션은 단순히 데이터 오류를 막는 기술이 아닙니다.
서비스의 신뢰성과 금전적 안정성을 보장합니다.
트랜잭션이 없는 시스템에서는 ‘이체 실패 후 잔액 감소’ 같은 치명적인 오류가 발생할 수 있습니다.
트랜잭션이 없는 시스템에서 생기는 문제
- 주문 중 결제만 성공하고 주문 데이터 저장 실패
 - 두 사용자가 동시에 같은 재고를 차감 → 재고 마이너스 발생
 - 중간에 서버 오류 발생 시 데이터 일부만 반영
 
Sequelize에서 트랜잭션 다루기
기본 트랜잭션 구조
const { Sequelize } = require('sequelize');
const sequelize = new Sequelize('shop', 'root', 'pw', { dialect: 'mysql' });
async function transferMoney(fromUserId, toUserId, amount) {
  const t = await sequelize.transaction();
  try {
    const from = await User.findByPk(fromUserId, { transaction: t });
    const to = await User.findByPk(toUserId, { transaction: t });
    if (from.balance < amount) throw new Error('잔액 부족');
    from.balance -= amount;
    to.balance += amount;
    await from.save({ transaction: t });
    await to.save({ transaction: t });
    await t.commit();
    console.log('이체 성공');
  } catch (err) {
    await t.rollback();
    console.error('트랜잭션 실패:', err.message);
  }
}
여러 쿼리를 하나로 묶는 방법
트랜잭션 객체(transaction)를 넘겨서 모든 쿼리를 같은 트랜잭션 범위에서 실행하면 됩니다.
동시성 제어란 무엇인가?
동시성 제어(Concurrency Control)는 여러 트랜잭션이 동시에 실행될 때
서로의 데이터에 영향을 주지 않도록 관리하는 기술입니다.
즉, “한 사용자가 데이터를 수정할 때 다른 사용자가 같은 데이터를 건드리지 못하게 하는 것”이죠.
동시성 문제의 대표 사례
| 문제 유형 | 설명 | 
|---|---|
| Dirty Read | 아직 커밋되지 않은 데이터를 다른 트랜잭션이 읽음 | 
| Lost Update | 두 트랜잭션이 같은 데이터를 수정하고 마지막 결과만 반영됨 | 
| Phantom Read | 같은 쿼리라도 시점에 따라 결과가 달라지는 문제 | 
트랜잭션 격리 수준(Isolation Level)
| 격리 수준 | Dirty Read | Non-repeatable Read | Phantom Read | 
|---|---|---|---|
| READ UNCOMMITTED | 발생 | 발생 | 발생 | 
| READ COMMITTED | 방지 | 발생 | 발생 | 
| REPEATABLE READ | 방지 | 방지 | 발생 | 
| SERIALIZABLE | 방지 | 방지 | 방지 | 
Sequelize로 동시성 제어 실습
트랜잭션 격리 수준 지정
const t = await sequelize.transaction({
  isolationLevel: Sequelize.Transaction.ISOLATION_LEVELS.REPEATABLE_READ
});
Optimistic Lock (낙관적 락)
버전 번호(version 컬럼)를 두고, 수정 시점이 다르면 업데이트를 거부합니다.
const user = await User.findByPk(1);
user.balance += 100;
await user.save(); // version +1 자동 증가
실무에서 트랜잭션 설계할 때의 3가지 원칙
- 트랜잭션은 짧게 유지한다 — 오랜 시간 점유하면 Deadlock 위험 증가
 - 트랜잭션 범위를 명확히 정의한다 — 비즈니스 단위별로 묶기
 - 에러 시 롤백 경로를 명시적으로 구현한다 — 부분 실패를 허용하지 않기
 
FAQ: 트랜잭션과 동시성 제어 관련 질문
Q1. 모든 DB 작업에 트랜잭션을 써야 하나요?
CRUD 중 데이터 무결성이 중요한 연산(이체, 결제 등)에만 사용하세요.
Q2. 트랜잭션은 자동으로 롤백되나요?
에러 발생 시 수동으로 rollback() 호출해야 합니다. try-catch를 항상 사용하세요.
Q3. Deadlock이 자주 발생합니다. 어떻게 해결하나요?
트랜잭션 순서를 통일하거나, 짧은 트랜잭션 단위로 분리하면 해결됩니다.
Q4. Sequelize에서 Nested Transaction도 가능한가요?
가능합니다. Savepoint를 활용해 하위 트랜잭션을 관리할 수 있습니다.
Q5. Isolation Level은 무엇을 기준으로 선택하나요?
성능이 중요하면 READ COMMITTED, 정확성이 중요하면 SERIALIZABLE.
Q6. 분산 트랜잭션(Distributed Transaction)도 Sequelize로 가능한가요?
단일 DB 기준에서는 가능하지만, 여러 DB 간의 분산 트랜잭션은 별도 시스템(예: SAGA 패턴)이 필요합니다.
마무리 — 데이터 안정성은 시스템 신뢰의 핵심
트랜잭션과 동시성 제어는 데이터베이스의 안전벨트입니다.
조금 복잡하지만, 이것이 서비스 신뢰의 근간을 만듭니다.
다음 편에서는 “대규모 트래픽을 견디는 데이터 구조 — 인덱스와 캐싱 전략”을 다루며
고성능 데이터베이스 설계로 넘어가겠습니다.
참고자료:
Sequelize Transactions Docs ·
PostgreSQL Isolation Levels
