HooneyLog
© 2026 Seunghoon Shin. All rights reserved.
모든 게시글
Backend
2026. 3. 28.•
2

데이터 무결성을 지키는 최후의 보루: 트랜잭션과 격리 수준(Isolation Level) 완벽 이해하기

Seunghoon Shin
작성자 Seunghoon Shin풀스택 개발자

데이터 무결성을 지키는 최후의 보루: 트랜잭션과 격리 수준(Isolation Level) 완벽 이해하기

1. 문제의 배경

사용자가 상품을 주문하거나 송금을 하는 찰나의 순간, 데이터베이스 내부에서는 수많은 일이 일어납니다. 만약 재고 차감과 결제 기록 중 하나만 성공하고 나머지가 실패한다면 어떻게 될까요? 혹은 두 명의 사용자가 동시에 남은 마지막 재고 1개를 주문한다면요?

이런 상황에서 데이터의 일관성을 보장하기 위해 우리는 트랜잭션(Transaction)을 사용합니다. 트랜잭션은 "전부 성공하거나, 아니면 아예 일어나지 않은 상태로 되돌리거나(All or Nothing)"를 보장하는 작업의 단위입니다.

2. 핵심 개념: ACID

트랜잭션을 논할 때 빠질 수 없는 4가지 원칙입니다.

  • Atomicity (원자성): 트랜잭션 내의 모든 작업은 하나의 단위로 취급됩니다. 하나라도 실패하면 전체가 롤백됩니다.
  • Consistency (일관성): 트랜잭션 완료 후 데이터베이스는 항상 미리 정의된 규칙(제약 조건 등)을 만족해야 합니다.
  • Isolation (격리성): 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리되어야 합니다.
  • Durability (지속성): 성공적으로 완료된 트랜잭션의 결과는 시스템 장애가 발생하더라도 영구적으로 기록됩니다.

3. 여러 트랜잭션이 동시에 실행된다면?

여러 트랜잭션이 같은 데이터에 동시에 접근하면 이상 현상(Anomalies)이 발생할 수 있습니다. 이를 방지하기 위해 SQL 표준은 4가지 격리 수준을 정의합니다.

트랜잭션 동시성 흐름 시각화

4가지 격리 수준 (Isolation Levels)

  1. Read Uncommitted: 커밋되지 않은 데이터도 읽을 수 있습니다. 가장 빠르지만 가장 위험합니다.
  2. Read Committed: 커밋된 데이터만 읽습니다. PostgreSQL의 기본값이며 실무에서 가장 많이 쓰입니다.
  3. Repeatable Read: 트랜잭션이 시작된 시점의 데이터를 일관되게 읽습니다. MySQL의 기본값입니다.
  4. Serializable: 모든 트랜잭션을 순차적으로 실행하는 것처럼 처리합니다. 완벽하지만 매우 느립니다.

4. 구현 및 트러블슈팅 (TypeScript & Prisma)

현대적인 NestJS 환경에서 Prisma ORM을 사용하여 트랜잭션을 안전하게 처리하는 예시입니다.

Interactive Transaction 예시

단순히 쿼리를 나열하는 것을 넘어, 비즈니스 로직 중간에 검증이 필요한 경우 사용합니다.

async transferMoney(fromId: number, toId: number, amount: number) { // $transaction을 통해 모든 작업이 원자적으로 실행됨을 보장합니다. return await this.prisma.$transaction(async (tx) => { // 1. 송금자 잔액 확인 const sender = await tx.account.findUnique({ where: { id: fromId } }); if (sender.balance < amount) { throw new Error("잔액이 부족합니다."); // 에러 발생 시 자동 롤백 } // 2. 잔액 차감 await tx.account.update({ where: { id: fromId }, data: { balance: { decrement: amount } } }); // 3. 잔액 증액 await tx.account.update({ where: { id: toId }, data: { balance: { increment: amount } } }); }); }

5. 결과 및 Trade-off

격리 수준을 높이면 데이터는 안전해지지만 성능은 떨어집니다. 반대로 수준을 낮추면 성능은 올라가지만 Dirty Read 같은 정합성 문제가 생길 수 있습니다.

시니어 개발자는 단순히 "안전하니까 높은 수준"을 택하는 것이 아니라, 서비스의 특성에 따라 적절한 수준을 선택하고, 필요하다면 애플리케이션 레벨에서 Redis 등을 이용한 분산 락(Distributed Lock)을 고려해야 합니다.

6. 마치며

트랜잭션은 데이터베이스가 제공하는 강력한 도구이지만, 그 내부 원리를 모른 채 사용하면 성능 저하나 데드락(Deadlock)의 함정에 빠지기 쉽습니다. 특히 Javascript/TypeScript 환경에서는 비동기 루프 내에서 트랜잭션을 잘못 사용하지 않도록 주의해야 합니다.

오늘 정리한 격리 수준의 차이를 이해하고, 여러분의 프로젝트에 최적인 전략을 세워보세요!

← 이전 글DB 성능 최적화: N+1 문제부터 데드락(Deadlock)까지 완벽 정복하기
다음 글 →Supabase Client vs 쿼리 빌더: 비슷해 보이지만 완전히 다른 두 세계