1P by GN⁺ 4시간전 | ★ favorite | 댓글 1개
  • 트랜잭션은 데이터베이스에서 여러 작업을 하나의 원자적 단위로 실행하기 위한 구조로, 읽기·쓰기·갱신·삭제를 포함함
  • MySQL과 Postgres는 begin;commit;으로 트랜잭션을 제어하며, 실패나 오류 시 rollback;으로 변경을 취소함
  • 두 데이터베이스 모두 일관된 읽기(consistent read) 를 보장하지만, Postgres는 다중 버전 행 저장(MVCC) 을, MySQL은 undo log를 사용함
  • 격리 수준(isolation level) 은 트랜잭션 간 데이터 간섭을 제어하며, Serializable부터 Read Uncommitted까지 네 단계로 구분됨
  • Postgres와 MySQL은 동시 쓰기 충돌을 서로 다른 방식으로 처리하며, Postgres는 낙관적 검증을, MySQL은 행 단위 잠금(row-level locking) 을 사용함

트랜잭션의 기본 개념

  • 트랜잭션은 데이터베이스에서 여러 SQL 작업을 하나의 원자적 실행 단위로 묶는 구조
    • begin;으로 시작해 commit;으로 종료하며, 중간에 여러 쿼리를 실행 가능
    • commit; 시점에 모든 변경이 한 번에 적용됨
  • 예기치 못한 장애(전원 차단, 디스크 오류 등)나 의도적 취소 시 rollback;으로 변경을 되돌림
    • Postgres는 WAL(Write-Ahead Log) 로 복구를 지원함
  • 트랜잭션 중 변경된 데이터는 격리되어 다른 세션에서 보이지 않음
    • rollback; 시 모든 변경이 취소되어 데이터베이스는 원래 상태로 복원됨

일관된 읽기(Consistent Reads)

  • 트랜잭션은 실행 중 외부 변경의 영향을 받지 않는 일관된 데이터 뷰를 유지해야 함
  • MySQL과 Postgres는 REPEATABLE READ 모드 이상에서 이를 지원하지만, 구현 방식이 다름
    • Postgres: 다중 버전 행 저장(MVCC) 으로 각 행의 버전을 관리
    • MySQL: undo log를 사용해 과거 버전을 재구성

Postgres의 다중 버전 행 저장

  • 행이 갱신될 때마다 새로운 버전이 생성되고, 이전 버전은 xmax, 새 버전은 xmin으로 트랜잭션 ID를 기록
  • 트랜잭션이 커밋되기 전에는 다른 세션이 변경 내용을 볼 수 없음
  • 커밋 후에는 새 버전이 전체 데이터베이스에 반영됨
  • rollback; 시 변경이 폐기되어 원래 데이터 유지
  • 오래된 행 버전은 VACUUM FULL 명령으로 정리되어 저장 공간을 회수함

MySQL의 Undo Log

  • MySQL은 행을 직접 덮어쓰지만, undo log에 이전 값을 기록해 필요 시 복원 가능
  • 각 행은 xid(최근 수정 트랜잭션 ID)와 ptr(undo log 포인터)을 메타데이터로 가짐
  • 동시에 여러 트랜잭션이 실행될 때, undo log를 통해 각 트랜잭션이 필요한 버전을 선택적으로 조회함
  • 동일 행에 여러 undo log 기록이 존재할 수 있으며, 트랜잭션 ID를 기준으로 적절한 버전을 선택함

격리 수준(Isolation Levels)

  • 트랜잭션 간 데이터 간섭을 제어하는 설정으로, Serializable → Repeatable Read → Read Committed → Read Uncommitted 순으로 완화됨
  • Serializable: 모든 트랜잭션이 순차적으로 실행된 것처럼 동작
  • Repeatable Read: 동일 쿼리 재실행 시 결과가 동일하지만, phantom read 가능
  • Read Committed: 이미 커밋된 다른 트랜잭션의 변경을 읽을 수 있음
  • Read Uncommitted: dirty read 허용, 가장 낮은 보호 수준이지만 성능은 높음

동시 쓰기(Concurrent Writes)

  • 두 트랜잭션이 동일 행을 동시에 수정할 때의 처리 방식은 데이터베이스별로 다름

MySQL: 행 단위 잠금(Row-level Locking)

  • 공유 잠금(S lock) 은 여러 트랜잭션이 동시에 읽기 가능
  • 배타 잠금(X lock) 은 한 트랜잭션만 행을 수정 가능
  • SERIALIZABLE 모드에서는 모든 갱신 시 X lock을 획득해야 하며, 충돌 시 교착 상태(deadlock) 발생 가능
  • MySQL은 교착 상태를 감지해 한쪽 트랜잭션을 종료시킴

Postgres: Serializable Snapshot Isolation

  • Postgres는 predicate lock을 사용해 행 집합 단위로 접근을 추적
    • 예: WHERE id BETWEEN 10 AND 20 조건에 대한 잠금
  • 실제 접근을 차단하지 않고, 충돌을 감지해 위반 시 트랜잭션을 종료함
  • 낙관적 충돌 해결(optimistic conflict resolution) 로 교착 상태를 피함
  • MySQL과 마찬가지로, 충돌 시 한 트랜잭션이 종료되며 애플리케이션은 재시도 로직을 구현해야 함

결론

  • 트랜잭션은 데이터베이스의 핵심 구성 요소로, 원자성·일관성·격리성·지속성(ACID) 을 보장함
  • Postgres와 MySQL은 동일한 목표를 서로 다른 내부 구조로 달성함
  • 네 가지 격리 수준과 트랜잭션 동작 원리를 이해하면 데이터베이스를 보다 안정적으로 운용 가능함
Hacker News 의견들
  • 이 글이 좀 부족하게 느껴졌음
    SQL 표준에서 정의한 현상(phenomena) 중심으로 격리 수준을 설명하는 대신, 직렬 가능성(serializability) 개념에서 출발하는 게 더 직관적이라 생각함
    직렬 가능성은 스레드 안전성의 일반화로 볼 수 있고, 이를 잃으면 실행 순서에 따라 결과가 달라지는 버그가 생김
    데이터베이스의 다양한 격리 수준은 이 보장을 완화한 형태일 뿐이며, 사용자가 다른 방식으로 보장을 확보해야 함
    현상들은 비직렬적 상황을 시각화하는 도구일 뿐, 직렬 가능성과 직접적으로 연결된 것은 아님
    예를 들어 Kubernetes 클러스터도 잘 설계된 컨트롤러를 사용하면 직렬 가능하게 동작할 수 있음

    • 글쓴이임. 좋은 피드백 고마움
      트랜잭션, 격리 수준, MVCC를 여러 DB 간 비교까지 포함해 한 번에 다루는 건 방대한 작업임
      기술적 깊이와 접근성, 그리고 글의 길이 사이에서 균형을 잡으려 했음
    • Jepsen: MariaDB Galera Cluster 분석 링크를 공유함
      더 많은 표기와 인용이 있으면 좋겠다는 의견임
    • 대부분의 RDBMS는 필요하면 직렬 가능 격리를 제공함
      하지만 불필요하게 사용하면 트랜잭션 간 조정 비용이 커져 동시성과 처리량이 줄어듦
    • 그렇다면 더 나은 설명을 제안해보라는 반응임
  • 트랜잭션을 copy-on-write 파일시스템(btrfs, zfs) 의 스냅샷처럼 생각할 수도 있지만, Git 브랜치로 비유하는 게 더 직관적이라 봄
    BEGIN은 브랜치 생성, UPDATE는 커밋, ROLLBACK은 브랜치 삭제, COMMIT은 git merge와 같음
    충돌이 나면 DB가 행 단위로 병합을 시도하고, 실패 시 설정에 따라 롤백하거나 강제 병합함
    READ UNCOMMITTED는 빠른 병합을, SERIALIZABLE은 정확성을 우선함
    이런 비유가 누군가에게 트랜잭션의 개념을 ‘아하!’ 하고 이해하게 도와줄 수 있음

    • (짧은 댓글) 동시성(concurrency)을 암시하는 반응임
  • 많은 사람들이 놀라는 부분은 Postgres와 MySQL이 기본적으로 직렬 가능 모드가 아니라 read-committed라는 점임
    성능 차이는 “약간”이 아니라 실제로는 훨씬 큼
    read-committed를 쓰면 락(lock) 관리에 신경 써야 하고, UNIQUE 제약도 경쟁 조건을 막는 데 필요함
    그래도 직렬 가능 모드의 성능 손실과 재시도 문제를 감수하느니 이 방식을 선호함
    참고: PostgreSQL 공식 문서

    • 최신 MySQL과 MariaDB(InnoDB)는 기본이 repeatable-read
      MySQL 문서, MariaDB 문서 참고
      MyISAM은 이제 거의 안 씀
    • SERIALIZABLE의 문제는 성능뿐 아니라 충돌·데드락·타임아웃으로 트랜잭션이 실패할 수 있다는 점임
      애플리케이션이 이를 감지하고 재시도 전략을 가져야 함
    • Oracle과 SQL Server도 기본은 read committed임
      직렬 가능 모드는 교과서에서는 멋져 보이지만 실제로는 거의 안 씀
  • 요즘 많은 데이터베이스 툴이 ACID보다 실시간 업데이트 공유를 우선함
    예를 들어 Airtable은 필드 수정 시 동료 화면에 바로 반영되지만, 트랜잭션이 없어 데이터 불일치 위험이 있음
    관련 내용은 VisualDB 블로그 글 참고

    • 경쟁사 비판을 가장한 제품 홍보처럼 보인다는 반응임
  • PlanetScale 블로그를 읽는 게 정말 즐거움
    시각화에 어떤 도구를 썼는지 궁금함

    • 글쓴이임. 고마움!
      시각화는 js + gsap(https://gsap.com)으로 제작했음
  • 이 주제에 관심 있다면 『Designing Data-Intensive Applications』 을 강력히 추천함
    다양한 격리 수준뿐 아니라 ACID 정의의 모호성까지 다룸
    2판이 곧 나온다고 들음

  • Postgres 같은 MVCC 시스템의 트랜잭션은 copy-on-write 파일시스템의 스냅샷과 비슷함
    BEGIN 시점에 데이터 스냅샷을 만들고, UPDATE는 개인 복사본에만 반영됨
    ROLLBACK 시 복사본은 폐기되고, COMMIT 시 새 스냅샷이 공식 버전이 됨
    이 비유가 누군가에게 트랜잭션 개념을 명확히 이해시키는 계기가 될 수 있음
    P.S. Git 브랜치 비유도 가능함

    • 완전히 정확하진 않음. DB는 브랜칭과 락킹을 함께 사용
      SELECT 후 UPDATE 같은 경우 한 스레드가 블록될 수 있음
      오늘 MySQL에서 이를 단일 쿼리로 바꿀 수 있을지 실험해볼 예정임
  • 예전엔 백엔드 면접에서 트랜잭션을 자주 물었음
    모두가 써봤지만 이해 수준은 경력에 따라 다름
    격리 수준을 전부 외우진 않아도, 서로 다르게 동작한다는 걸 아는 것만으로도 호기심과 시스템 이해도를 볼 수 있음

    • 같은 이름의 격리 수준이라도 DB마다 동작이 다르므로, 케이스별로 세부 동작을 확인해야 함
  • “phantom read” 설명이 오해를 줄 수 있음
    repeatable read에서는 기존 행의 값은 변하지 않지만 새로운 행이 추가될 수 있음
    기존 행이 바뀌거나 삭제되는 일은 없으므로, 그 점을 명확히 해야 함

  • “xmin/xmax와는 관련 없다”는 문장이 불완전하게 느껴짐
    커밋 시 시각화가 테이블 헤더를 가리키는 것도 이상함
    실제로는 xmax/xmin이 커밋 여부를 판단하는 핵심 메커니즘 아닌가?
    서브트랜잭션까지 고려하면 더 복잡해짐
    그래도 시각화와 설명은 전반적으로 즐겁게 읽었음

    • 나도 xmax/xmin 개념이 빠진 게 아쉽다고 느낌
      격리 수준을 이해하는 데 핵심인데, 마치 섹션이 누락된 것처럼 느껴졌음