트랜잭션
트랜잭션 (Transactions)
**트랜잭션(transaction)**은 모든 데이터베이스 시스템의 근본 개념이에요. 여러 단계를 "전부 성공하거나 전부 실패하거나" 하나의 덩어리로 묶는 게 핵심이에요. 은행 계좌 이체 예제를 통해 왜 이게 중요한지, PostgreSQL에서 어떻게 쓰는지 배워볼게요.
트랜잭션이란 (What is a transaction)
트랜잭션은 모든 데이터베이스 시스템의 근본 개념이에요. 트랜잭션의 핵심은 여러 단계를 하나의 "전부 아니면 전무(all-or-nothing)" 연산으로 묶는 것이에요. 단계 사이의 중간 상태는 다른 동시 실행 트랜잭션에게 보이지 않아요. 그리고 트랜잭션을 완료하지 못하게 만드는 어떤 실패가 발생하면, 그 어떤 단계도 데이터베이스에 영향을 주지 않아요.
예를 들어, 여러 고객 계좌의 잔액과 각 지점(branch)의 총 예금 잔액을 담은 은행 데이터베이스를 생각해볼게요. Alice의 계좌에서 Bob의 계좌로 $100.00를 송금한다고 해봐요. 아주 많이 단순화하면, SQL 명령은 이렇게 생길 거예요:
UPDATE accounts SET balance = balance - 100.00
WHERE name = 'Alice';
UPDATE branches SET balance = balance - 100.00
WHERE name = (SELECT branch_name FROM accounts WHERE name = 'Alice');
UPDATE accounts SET balance = balance + 100.00
WHERE name = 'Bob';
UPDATE branches SET balance = balance + 100.00
WHERE name = (SELECT branch_name FROM accounts WHERE name = 'Bob');
여기서 명령의 세부 내용은 중요하지 않아요. 중요한 건 이렇게 간단한 작업을 완수하는 데 여러 번의 별도 갱신이 관여한다는 점이에요. 우리 은행 직원들은 이 모든 갱신이 전부 일어나거나, 전부 일어나지 않거나 둘 중 하나라는 걸 보장받고 싶을 거예요. 시스템 실패로 인해 Bob이 Alice에게서 출금되지 않은 $100.00를 받게 되는 일은 절대 있어선 안 되죠. 마찬가지로 Bob에게 입금도 안 됐는데 Alice만 출금당한다면 Alice는 당장 은행을 바꿀 거예요.
우리가 필요한 건, 작업 도중 어딘가에서 문제가 생기면, 지금까지 실행된 어떤 단계도 효력을 갖지 않는다는 보장이에요. 갱신들을 하나의 트랜잭션으로 묶으면 이 보장을 얻을 수 있어요. 트랜잭션은 **원자적(atomic)**이라고 말해요. 다른 트랜잭션의 관점에서 보면, 트랜잭션은 완전히 일어나거나 아예 일어나지 않거나 둘 중 하나니까요.
영구 저장 보장 (Durability)
우리는 또한 이런 보장도 원해요: 트랜잭션이 완료되고 데이터베이스 시스템이 이를 인정한 순간, 그 변경은 영구히 기록되며 곧바로 크래시가 나도 사라지지 않는다. 예를 들어 Bob의 현금 출금을 기록하고 있다면, 그가 은행 문을 나서자마자 크래시가 나서 그의 계좌 출금이 사라질 가능성은 절대 없어야 해요. 트랜잭션 데이터베이스는, 트랜잭션의 모든 갱신이 완료로 보고되기 전에 영구 저장소(즉 디스크)에 로그로 기록되는 것을 보장해요.
동시성과 가시성 (Visibility & concurrency)
트랜잭션 데이터베이스의 또 하나 중요한 속성은 원자적 갱신 개념과 밀접한 관련이 있어요: 여러 트랜잭션이 동시에 실행 중일 때, 각각은 다른 트랜잭션의 불완전한 변경을 볼 수 없어야 한다. 예를 들어 어떤 트랜잭션이 모든 지점 잔액을 합산하느라 바쁘다면, Alice의 지점 출금은 포함하면서 Bob의 지점 입금은 빠뜨리는(또는 그 반대) 일이 있어선 안 돼요.
그래서 트랜잭션은 데이터베이스에 대한 영구적 효과 측면에서 뿐 아니라, 일어나는 과정의 가시성 측면에서도 전부 아니면 전무여야 해요. 아직 열려 있는 트랜잭션이 지금까지 만든 갱신은 트랜잭션이 완료될 때까지 다른 트랜잭션에 보이지 않아요. 완료되는 순간 모든 갱신이 동시에 보이게 되죠.
BEGIN과 COMMIT (BEGIN and COMMIT)
PostgreSQL에서 트랜잭션은, 트랜잭션에 포함할 SQL 명령들을 **BEGIN**과 COMMIT 명령으로 감싸서 설정해요. 그래서 우리의 은행 트랜잭션은 실제로 이렇게 생겼어요:
BEGIN;
UPDATE accounts SET balance = balance - 100.00
WHERE name = 'Alice';
-- etc etc
COMMIT;
만약 트랜잭션 도중 (아마 Alice의 잔액이 음수가 된 걸 막 알아챘다거나 해서) 커밋하고 싶지 않다면, COMMIT 대신 ROLLBACK 명령을 내리면 돼요. 그러면 지금까지의 모든 갱신이 취소돼요.
사실 PostgreSQL은 모든 SQL 문장을 트랜잭션 안에서 실행되는 것처럼 취급해요. BEGIN을 내리지 않으면, 각각의 개별 문장에 암시적인 BEGIN과 (성공할 경우) COMMIT이 감싸져 있다고 생각하면 돼요. BEGIN과 COMMIT으로 감싼 문장 묶음을 가끔 **트랜잭션 블록(transaction block)**이라고 불러요.
참고: 일부 클라이언트 라이브러리는
BEGIN과COMMIT명령을 자동으로 내리기도 해서, 당신이 요청하지 않았는데도 트랜잭션 블록의 효과를 보게 될 수 있어요. 사용 중인 인터페이스의 문서를 확인해보세요.
세이브포인트 (Savepoints)
세이브포인트(savepoint)를 사용하면 트랜잭션 안의 문장들을 더 세밀하게 제어할 수 있어요. 세이브포인트를 사용하면 트랜잭션의 일부를 골라서 버리고, 나머지는 커밋할 수 있어요. SAVEPOINT로 세이브포인트를 정의한 뒤, 필요하면 ROLLBACK TO로 그 세이브포인트까지 되돌릴 수 있어요. 세이브포인트를 정의한 시점과 그로 되돌리는 시점 사이의 트랜잭션 데이터베이스 변경은 모두 버려지지만, 세이브포인트보다 이른 변경은 유지돼요.
세이브포인트로 되돌린 뒤에도 그 세이브포인트는 계속 정의되어 있어서, 여러 번 되돌릴 수 있어요. 반대로 특정 세이브포인트로 다시는 되돌릴 일이 없다고 확신되면, 그것을 해제(release)해서 시스템이 일부 자원을 비울 수 있게 할 수 있어요. 명심할 점은, 세이브포인트를 해제하거나 그로 되돌리면, 그 이후에 정의된 모든 세이브포인트도 자동으로 해제된다는 거예요.
이 모든 일은 트랜잭션 블록 안에서 일어나므로, 다른 데이터베이스 세션에는 아무것도 보이지 않아요. 트랜잭션 블록을 커밋하면 커밋된 작업이 하나의 단위로 다른 세션에 보이게 되고, 되돌린(rollback) 작업은 절대 보이지 않게 돼요.
은행 예제로 보는 세이브포인트 (Savepoints in action)
은행 데이터베이스를 다시 떠올려볼게요. Alice 계좌에서 $100.00를 출금하고 Bob 계좌에 입금했다가, 나중에 알고 보니 Wally 계좌에 입금했어야 했다고 해봐요. 세이브포인트를 써서 이렇게 할 수 있어요:
BEGIN;
UPDATE accounts SET balance = balance - 100.00
WHERE name = 'Alice';
SAVEPOINT my_savepoint;
UPDATE accounts SET balance = balance + 100.00
WHERE name = 'Bob';
-- oops ... forget that and use Wally's account
ROLLBACK TO my_savepoint;
UPDATE accounts SET balance = balance + 100.00
WHERE name = 'Wally';
COMMIT;
물론 이 예제는 지나치게 단순화된 거지만, 세이브포인트를 쓰면 트랜잭션 블록 안에서 아주 많은 제어가 가능하다는 걸 보여줘요. 게다가 ROLLBACK TO는, 오류로 인해 시스템이 aborted(중단) 상태로 만든 트랜잭션 블록의 제어를 되찾을 수 있는 유일한 방법이에요. 완전히 롤백하고 처음부터 다시 시작하는 방법 말고는요.
더 알아보기 (Learn more)
- 외래 키 (Foreign Keys) — 이전 단계
- 윈도우 함수 (Window Functions) — 다음 단계로 넘어가요
- 동시성 제어 (Concurrency Control) — 트랜잭션의 격리 동작을 더 깊게