📚 CS/Server

[Database] 트랜잭션과 ACID, 데이터베이스가 신뢰를 지키는 방법

dev.daisy 2026. 6. 8. 00:45
데이터베이스 트랜잭션을 깊이 있게 들여다보면서 가장 인상 깊었던 점은, 자칫 복잡하게 얽히기 쉬운 동시성과 장애 복구 로직을 Undo LogWAL Log라는 로그 중심의 아키텍처로 깔끔하게 풀어냈다는 사실이었습니다.

이전에는 비즈니스 애플리케이션 레벨에서 인프라 다운이나 동시 요청 충돌 같은 예외 상황을 어떻게 방어해야 할지 막막할 때가 많았는데요. 데이터베이스의 하위 레이어에서 ACID 속성을 받쳐주고 있다는 걸 이해하고 나니, 전체 시스템을 설계할 때 더 예측 가능하게 구현할 수 있다는 생각이 들었습니다.

물론 모든 정합성 문제를 무조건 최고 등급의 격리 수준으로 해결하는 게 정답은 아닐 것이라고 생각합니다. 데이터의 중요도와 서비스 트래픽 상황을 균형 있게 고려하면서, 성능과 정합성 사이의 트레이드오프를 어떻게 조율할 것인지 고민해보는 좋은 계기가 되었습니다. 앞으로 데이터 모델링을 하거나 비즈니스 로직을 짜야 할 때, 트랜잭션의 바운더리부터 고민해보게 될 것 같습니다.

 

복잡한 백엔드 시스템이나 데이터 비즈니스 로직을 개발하다 보면 여러 테이블의 상태를 동시에 변경해야 하는 상황을 자주 마주합니다. 이때 특정 상황에서만 예외가 발생하거나, 여러 연산 중 일부만 성공하고 일부는 실패하는 구조는 시간이 지날수록 데이터 정합성을 깨뜨리고 시스템의 유지보수를 불가능하게 만듭니다.

이 문제의 중심에는 데이터 신뢰성이라는 개념이 있습니다. 시스템 장애나 동시성 충돌 속에서도 데이터가 항상 정확하고 안전하게 유지될 수 있도록 보장하는 데이터베이스의 핵심적인 동작 방식트랜잭션(Transaction), 그리고 ACID 속성입니다.

은행 앱에서 송금 버튼을 눌렀을 때, 내 계좌에서 돈이 빠져나간 뒤 상대방 계좌에 입금되지 않는 상황은 일어나지 않습니다. 트랜잭션이 내부적으로 어떻게 의존성을 관리하고 데이터의 안전을 보장하는지 그 메커니즘을 정리해 보겠습니다.


왜 트랜잭션과 ACID를 고민해야 할까?

트랜잭션은 데이터베이스의 상태를 변화시키는 하나의 논리적 작업 단위입니다. 계좌 이체처럼 여러 DB 연산이 반드시 함께 성공하거나 함께 실패해야 하는 경우, 이 연산들을 하나로 묶은 트랜잭션이 안전하게 처리되기 위해 정의해야 하는 규칙이 필요합니다.

 

그 원칙이 바로 ACID인데요, 각각의 정의와 뜻은 아래와 같습니다.

 

- 원자성 (Atomicity): 트랜잭션 내 모든 연산은 전부 성공하거나 전부 실패합니다. (All or Nothing)

- 일관성 (Consistency): 데이터베이스는 언제나 정의된 규칙(제약 조건, 데이터 타입 등)을 만족해야 합니다.

- 격리성 (Isolation): 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치지 않도록 격리되어야 합니다.

- 지속성 (Durability): 성공적으로 완료(Commit)된 결과는 시스템 장애가 발생하더라도 영구적으로 유지됩니다.


1. 원자성(Atomicity) - Undo Log를 통한 무결성 보장하기

원자성은 전부 실행되거나 전부 실패해야 합니다. 따라서 절반만 실행된 트랜잭션을 허용하지 않습니다. 만약 데이터 변경 도중 예외가 발생하거나 시스템이 다운된다면, 데이터베이스는 문제가 발생하기 전으로 완벽하게 상태를 되돌려야 합니다.

데이터베이스가 이를 내부적으로 처리하는 메커니즘이 바로 Undo Log입니다.

-- A 계좌에서 출금 후 B 계좌 입금 도중 장애가 발생하는 시나리오
START TRANSACTION;

UPDATE accounts SET balance = balance - 100000 WHERE id = 'A';
-- [Internal] 이 시점에 변경 전 데이터(A의 원래 잔액)가 Undo Log에 기록됩니다.

UPDATE accounts SET balance = balance + 100000 WHERE id = 'B';
-- 예기치 못한 에러 발생 -> 자동으로 ROLLBACK 수행


데이터베이스는 데이터를 수정하기 직전, 변경 전의 원래 값을 Undo Log에 먼저 기록해 둡니다. 만약 에러가 발생하여 `ROLLBACK`이 수행되면, 엔진은 이 Undo Log를 역순으로 추적하면서 데이터를 원래 상태로 복원합니다. 개발자는 중간 단계의 실패를 걱정할 필요 없이, 전체 로직의 성공과 실패라는 결과에만 집중할 수 있습니다. 

 

Q. 만약 Undo Log를 채 쓰기도 전에 서버가 꺼지면 어떻게 될까요?

 

A. 애초에 Undo Log를 디스크에 기록(Write)하기 전까지는 실제 데이터 파일의 값을 절대 바꾸지 않습니다.

 

"Undo Log에 기록하는 연산"과 "실제 데이터를 바꾸는 연산" 사이에 서버가 꺼지면 데이터가 꼬이지 않을까 하는 의문이 들었는데요, 이 문제를 방지하기 위해 데이터베이스는 WAL(Write-Ahead Logging) 규칙을 엄격하게 따릅니다.

 

실제 데이터(Data File)를 수정하기 전에, Undo 변경 이력을 로그에 먼저 쓰고 디스크에 안전하게 반영(Flush)하는 것이 원칙입니다. 따라서 로그를 쓰기 전에 서버가 꺼졌다면 실제 데이터는 애초에 변경된 적이 없으므로 안전한 상태이고, 로그를 쓴 후에 꺼졌다면 앞서 말한 시나리오 1처럼 로그를 보고 되돌릴 수 있습니다.


3. 격리성(Isolation)

수천 명의 사용자가 동시에 같은 데이터에 접근할 때, 트랜잭션들이 서로의 내부 상태를 자유롭게 읽을 수 있다면 Dirty Read(커밋되지 않은 데이터 읽기), Non-Repeatable Read(동일 조회 시 값 변경), Phantom Read(동일 조회 시 행 추가/삭제) 같은 정합성 오류가 발생합니다.

SQL 표준은 동시성 처리 성능과 데이터 정확성 사이의 균형을 위해 4가지 Isolation Level을 정의합니다.

격리 수준 (Isolation Level) Dirty Read Non-Repeatable Read Phantom Read
READ UNCOMMITTED 발생 발생 발생
READ COMMITTED 방지 발생 발생
REPEATABLE READ 방지 방지 발생 (엔진에 따라 방지)
SERIALIZABLE 방지 방지 방지


격리 수준이 높아질수록 데이터 정합성은 강해지지만, 트랜잭션들이 기다려야 하므로 동시에 처리하는 성능은 저하됩니다.

MVCC (Multi-Version Concurrency Control)
데이터베이스 엔진은 이 성능 저하 문제를 해결하기 위해 MVCC 방식을 적극적으로 활용합니다. 읽기 연산과 쓰기 연산이 서로를 블로킹하지 않도록, 데이터의 여러 버전을 언두 영역에 유지하는 기술입니다. 예를 들어 MySQL InnoDB는 REPEATABLE READ를 기본값으로 사용하며 MVCC를 통해 Phantom Read까지 효과적으로 방지하고, PostgreSQL은 READ COMMITTED를 기본값으로 삼아 커밋된 최신 데이터를 보장하는 방식을 취합니다.

4. 지속성(Durability) - WAL 기반의 장애 복구

트랜잭션이 성공적으로 COMMIT되었다면, 그 직후 서버 전원이 꺼지더라도 데이터는 절대 사라지지 않아야 합니다. 하지만 성능 향상을 위해 메모리 버퍼를 거쳐 디스크에 비동기로 쓰는 DB 구조상, 커밋 시점에 실제 데이터 파일(Data File)에 곧바로 기록하는 것은 디스크 I/O 병목을 유발합니다.

이 방식의 문제점을 해결하고 지속성을 보장하는 기술이 바로 WAL(Write-Ahead Logging)입니다.


WAL 동작 원리:

1. 데이터가 변경되면, 메모리 버퍼 내용을 수정함과 동시에 디스크의 WAL Log(Redo Log)에 변경 내용을 먼저 기록합니다.
2. 사용자가 `COMMIT`을 선언하면, 무거운 실제 데이터 파일 대신 WAL 로그만을 디스크에 fsync()하여 빠르게 영구적으로 저장합니다. (로그는 순차 쓰기 방식이기 때문에 디스크 성능 소모가 매우 적음)
3. 디스크의 실제 데이터 파일은 백그라운드 프로세스에 의해 나중에 비동기적으로 업데이트됩니다.

이 원리를 통해 메모리 버퍼의 데이터가 일시적으로 소실되더라도, 디스크에 안전하게 남아있는 WAL Log를 기반으로 시스템 재시작 시 Redo(재실행) 과정을 거쳐 커밋되었던 상태를 복구합니다.


마무리하며

회사에서 풀스택 개발자로 일하게 되면서 부족한 서버 지식을 채우기 위해 시작한 공부였지만, 기대 이상으로 많은 것을 배울 수 있었습니다.

 

서버 배포 시 DB의 정합성을 맞추기 위해 트랜잭션을 이용해 데이터를 삽입하거나 업데이트하곤 하는데요, 이번 기회에 내부 구조를 들여다보면서, 자칫 복잡하게 얽히기 쉬운 동시성과 장애 복구 로직을 Undo Log와 WAL Log라는 로그 중심의 아키텍처로 깔끔하게 격리해 냈다는 점이 가장 흥미로웠습니다.

 

그동안은 동시 요청 충돌 같은 예외 상황을 마주하면 이를 애플리케이션 레벨에서 어떻게 방어해야 할지 막막할 때가 많았습니다. 하지만 데이터베이스가 하부 레이어에서 ACID 속성을 단단하게 받쳐주고 있다는 점을 이해하고 나니, 전체 시스템을 설계할 때 훨씬 더 높은 예측 가능성을 가지고 코드를 작성할 수 있겠다는 자신감이 생겼습니다.

 

물론 모든 정합성 문제를 무조건 최고 등급의 격리 수준으로 해결하는 것이 정답은 아닐 것입니다. 데이터의 중요도와 서비스 트래픽 상황을 균형 있게 고려하면서, 성능과 정합성 사이의 트레이드오프를 어떻게 조율할 것인지 고민해보는 좋은 계기가 되었습니다. 앞으로 데이터 모델링을 하거나 비즈니스 로직을 짜야 할 때, 트랜잭션의 바운더리부터 고민해보게 될 것 같습니다.