소개
이 실습에서는 데이터 무결성을 보장하는 데 중요한 PostgreSQL 트랜잭션 관리를 살펴봅니다. 여러 작업을 하나의 작업 단위로 처리하는 트랜잭션을 시작하고 커밋하는 방법을 배웁니다. 실패한 트랜잭션을 롤백하고, 격리 수준을 설정하고, 동시에 실행되는 업데이트로 잠금을 시뮬레이션하는 방법도 배웁니다.
트랜잭션 시작 및 커밋
이 단계에서는 PostgreSQL에서 트랜잭션을 시작하고 커밋하는 방법을 배웁니다. 트랜잭션은 여러 작업을 하나의 작업 단위로 처리해 데이터 무결성을 보장합니다. 트랜잭션 안에서 작업 하나라도 실패하면 전체 트랜잭션을 롤백합니다. 이렇게 하면 일부 작업만 반영되는 일을 막고 데이터 일관성을 유지할 수 있습니다.
먼저 postgres 사용자로 PostgreSQL 데이터베이스에 연결합니다. 터미널을 열고 다음 명령을 실행합니다.
sudo -u postgres psql
postgres=# 프롬프트가 표시되면 연결된 것입니다.
다음으로 트랜잭션을 실습할 accounts 테이블을 만듭니다.
CREATE TABLE accounts (
id SERIAL PRIMARY KEY,
name VARCHAR(50),
balance DECIMAL(10, 2)
);
accounts 테이블에 초기 데이터를 입력합니다.
INSERT INTO accounts (name, balance) VALUES ('Alice', 100.00);
INSERT INTO accounts (name, balance) VALUES ('Bob', 50.00);
이제 BEGIN 명령으로 트랜잭션을 시작합니다.
BEGIN;
트랜잭션 안에서 Alice의 계좌에서 Bob의 계좌로 $20를 이체합니다.
UPDATE accounts SET balance = balance - 20.00 WHERE name = 'Alice';
UPDATE accounts SET balance = balance + 20.00 WHERE name = 'Bob';
변경 사항을 영구적으로 반영하려면 COMMIT 명령으로 트랜잭션을 커밋합니다.
COMMIT;
accounts 테이블을 조회해 트랜잭션이 성공했는지 확인합니다.
SELECT * FROM accounts;
Alice의 잔액이 $20 감소하고 Bob의 잔액이 $20 증가한 것을 확인할 수 있습니다.

마지막으로 psql 셸을 종료합니다.
\q
실패한 트랜잭션 롤백
이 단계에서는 PostgreSQL에서 실패한 트랜잭션을 롤백하는 방법을 배웁니다. 데이터베이스 작업을 수행하던 중 오류가 발생하면 트랜잭션을 롤백해야 데이터베이스의 일관성을 유지할 수 있습니다.
이전 단계에서 연결한 PostgreSQL 데이터베이스 세션이 아직 열려 있어야 합니다. 연결이 끊겼다면 다음 명령으로 다시 연결합니다.
sudo -u postgres psql
새 트랜잭션을 시작합니다.
BEGIN;
이 트랜잭션 안에서 일부러 실패하는 작업을 실행합니다. 기본 키 값을 중복으로 입력해 보겠습니다. 먼저 다음에 사용할 수 있는 id 값을 확인합니다.
SELECT MAX(id) FROM accounts;
결과가 2라고 가정합니다. 이제 이미 존재하는 id = 1을 사용해 새 계정을 입력합니다.
INSERT INTO accounts (id, name, balance) VALUES (1, 'Eve', 25.00);
이 명령을 실행하면 ERROR: duplicate key value violates unique constraint "accounts_pkey" 오류가 발생합니다.
트랜잭션 안에서 오류가 발생했으므로 ROLLBACK 명령을 사용해 트랜잭션을 롤백하고 변경 사항을 모두 취소합니다.
ROLLBACK;
accounts 테이블을 조회해 ROLLBACK이 성공했는지 확인합니다.
SELECT * FROM accounts;
테이블에 Alice와 Bob의 계좌만 남아 있고, 잔액은 1단계가 끝났을 때와 같은 것을 확인할 수 있습니다. 실패한 INSERT 작업이 롤백된 것입니다.

마지막으로 psql 셸을 종료합니다.
\q
격리 수준 설정 및 테스트
이 단계에서는 PostgreSQL의 트랜잭션 격리 수준을 살펴보고, 격리 수준을 설정하고 테스트하는 방법을 배웁니다. 격리 수준은 동시에 실행되는 트랜잭션이 서로 얼마나 격리되는지를 제어합니다. 격리 수준이 높을수록 데이터 손상을 더 잘 방지할 수 있지만, 동시성이 낮아질 수 있습니다.
이전 단계에서 연결한 PostgreSQL 데이터베이스 세션이 아직 열려 있어야 합니다. 연결이 끊겼다면 다음 명령으로 다시 연결합니다.
sudo -u postgres psql
터미널 창 두 개를 엽니다. 각 터미널에서 postgres 사용자로 PostgreSQL 데이터베이스에 연결합니다. 두 터미널 모두에 postgres=# 프롬프트가 표시되어야 합니다.
터미널 1:
sudo -u postgres psql
터미널 2:
sudo -u postgres psql
터미널 1에서 트랜잭션을 시작한 다음 격리 수준을 기본값인 READ COMMITTED로 설정합니다. SET TRANSACTION은 트랜잭션 안에서 첫 번째 쿼리를 실행하기 전에 실행해야 합니다.
BEGIN;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
터미널 1에서 Alice의 잔액을 조회합니다.
SELECT balance FROM accounts WHERE name = 'Alice';
잔액을 기록해 둡니다. 이제 터미널 2에서 트랜잭션을 시작하고 Alice의 잔액을 업데이트합니다.
BEGIN;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
UPDATE accounts SET balance = 90.00 WHERE name = 'Alice';
COMMIT;
터미널 1에서 Alice의 잔액을 다시 조회합니다.
SELECT balance FROM accounts WHERE name = 'Alice';
격리 수준이 READ COMMITTED이므로 터미널 2에서 커밋한 새 잔액 90.00이 표시됩니다.

이제 REPEATABLE READ 격리 수준을 테스트합니다. 터미널 1에서 현재 트랜잭션을 롤백하고 새 트랜잭션을 시작한 다음, 데이터를 조회하기 전에 격리 수준을 REPEATABLE READ로 설정합니다.
ROLLBACK;
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
터미널 1에서 Alice의 잔액을 다시 조회합니다.
SELECT balance FROM accounts WHERE name = 'Alice';
잔액을 기록해 둡니다. 이제 터미널 2에서 트랜잭션을 시작하고 Alice의 잔액을 다시 업데이트합니다.
BEGIN;
UPDATE accounts SET balance = 100.00 WHERE name = 'Alice';
COMMIT;
터미널 1에서 Alice의 잔액을 다시 조회합니다.
SELECT balance FROM accounts WHERE name = 'Alice';
격리 수준이 REPEATABLE READ이므로 터미널 2에서 100.00을 커밋했더라도 90.00이 표시됩니다. 이 값은 현재 트랜잭션의 첫 번째 쿼리가 설정한 스냅샷에 포함된 잔액입니다.
마지막으로 터미널 1에서 트랜잭션을 커밋합니다.
COMMIT;
이제 터미널 1에서 Alice의 잔액을 다시 조회합니다. 트랜잭션이 끝난 상태이므로 가장 최근에 커밋된 값인 100.00이 표시됩니다.
SELECT balance FROM accounts WHERE name = 'Alice';
두 psql 셸을 모두 종료합니다.
\q
동시 업데이트로 잠금 시뮬레이션
이 단계에서는 PostgreSQL에서 동시에 실행되는 업데이트를 이용해 잠금을 시뮬레이션합니다. 잠금은 동시에 실행되는 트랜잭션이 서로 간섭하지 않도록 막아 데이터 무결성을 유지하는 메커니즘입니다.
이전 단계에서 연결한 PostgreSQL 데이터베이스 세션이 아직 열려 있어야 합니다. 연결이 끊겼다면 다음 명령으로 다시 연결합니다.
sudo -u postgres psql
터미널 창 두 개를 엽니다. 각 터미널에서 postgres 사용자로 PostgreSQL 데이터베이스에 연결합니다. 두 터미널 모두에 postgres=# 프롬프트가 표시되어야 합니다.
터미널 1:
sudo -u postgres psql
터미널 2:
sudo -u postgres psql
터미널 1에서 트랜잭션을 시작하고 Alice의 잔액을 조회합니다. 이때 SELECT ... FOR UPDATE를 사용해 해당 행을 잠급니다.
BEGIN;
SELECT balance FROM accounts WHERE name = 'Alice' FOR UPDATE;
이 명령은 Alice의 잔액을 조회하고 해당 행에 잠금을 설정합니다. 따라서 이 트랜잭션을 커밋하거나 롤백할 때까지 다른 트랜잭션은 이 행을 수정할 수 없습니다.
터미널 2에서 트랜잭션을 시작하고 Alice의 잔액 업데이트를 시도합니다.
BEGIN;
UPDATE accounts SET balance = balance + 10 WHERE name = 'Alice';
터미널 2에서 이 명령이 멈춘 것처럼 보입니다. 터미널 1이 보유한 잠금이 해제되기를 기다리는 상태입니다.
이제 터미널 1에서 트랜잭션을 커밋합니다.
COMMIT;
터미널 1에서 트랜잭션을 커밋하면 터미널 2의 UPDATE 명령이 계속 진행됩니다.
터미널 2에서 트랜잭션을 커밋합니다.
COMMIT;
이제 어느 터미널에서든 accounts 테이블을 조회해 변경 사항을 확인합니다.
SELECT * FROM accounts;
터미널 1에서 잠금을 해제한 뒤 터미널 2의 트랜잭션이 Alice의 잔액을 업데이트한 것을 확인할 수 있습니다.

마지막으로 두 psql 셸을 모두 종료합니다.
\q
이 예시에서는 SELECT ... FOR UPDATE를 사용해 잠금을 설정하고, 동시 업데이트가 서로 간섭하지 않도록 막는 방법을 확인했습니다. 잠금이 없다면 두 트랜잭션이 같은 초기 잔액을 읽고 그 값을 기준으로 각각 업데이트할 수 있습니다. 이 경우 한쪽 업데이트가 다른 쪽 업데이트를 덮어써 변경 사항이 사라질 수 있습니다.
요약
이 실습에서는 PostgreSQL에서 트랜잭션을 관리하는 방법을 배웠습니다. 먼저 psql을 사용해 PostgreSQL 데이터베이스에 연결하고, 초기 데이터가 포함된 예제 accounts 테이블을 만들었습니다.
그런 다음 트랜잭션 사용법을 실습했습니다. BEGIN 명령으로 트랜잭션을 시작하고, Alice와 Bob의 잔액을 업데이트하는 등 여러 데이터베이스 작업을 수행한 뒤, COMMIT 명령으로 트랜잭션을 커밋해 변경 사항을 영구적으로 반영했습니다. 이를 통해 여러 작업을 하나의 원자적 단위로 처리하는 트랜잭션의 기본 원칙을 확인했습니다. 또한 트랜잭션을 롤백하고 격리 수준을 설정하는 방법도 배웠습니다. 마지막으로 동시 업데이트를 이용해 잠금을 시뮬레이션하고 데이터 손상을 방지하는 방법을 살펴봤습니다.



