이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
기초개념 › DB
트랜잭션
트랜잭션은 무엇을 묶고, COMMIT과 ROLLBACK은 무엇을 하는가
계좌 1에서 300을 빼고 계좌 2에 300을 더하는 일은 UPDATE 두 문장입니다. 하나만 반영되면 돈이 사라지거나 생깁니다. 트랜잭션은 이런 여러 문장을 "모두 반영되거나 모두 없던 일이 되는" 한 묶음으로 만드는 것입니다. 코드에서는 BEGIN이라는 문장으로 묶음을 시작하고, COMMIT이나 ROLLBACK으로 끝냅니다. COMMIT은 묶음에서 한 일을 전부 확정해 다른 연결에서도 보이게 하고, ROLLBACK은 묶음에서 한 일을 전부 취소합니다. 확정하기 전의 변경은 다른 연결에서 보이지 않습니다. (MySQL에서는 BEGIN 대신 START TRANSACTION이라고 쓰는 것이 표준이며 같은 뜻입니다. 이 실습을 대조한 H2는 START TRANSACTION을 받지 않아서 BEGIN으로 적었습니다.) 이 실습은 BEGIN을 지우거나 COMMIT을 ROLLBACK으로 바꾸거나 끝내는 문장을 지워 가며 같은 UPDATE 두 개를 실행하고, 다른 연결에서 읽은 잔액이 어떻게 달라지는지를 봅니다.
실행으로 대조 · H2 2.4.240에서 여섯 가지 조합을 실행해 대조, 규칙은 MySQL 문서와 대조
이 실습에 나오는 개념
- 트랜잭션
- 여러 데이터 변경 작업을 하나의 단위로 묶어, 전부 반영되거나(커밋) 전부 취소되게(롤백) 하는 것이다. 계좌 이체에서 출금과 입금 중 하나만 반영되는 중간 상태를 막는다. 데이터베이스가 정합성을 지키는 가장 기본적인 수단이다.
- 커밋과 롤백
- 커밋(commit)은 트랜잭션 안에서 한 데이터 변경을 모두 DB에 확정해 반영하는 것이고, 롤백(rollback)은 그 변경을 모두 취소해 트랜잭션을 시작하기 전 상태로 되돌리는 것이다. 트랜잭션은 끝날 때 둘 중 하나로 끝나며, 이미 커밋한 변경은 롤백으로 되돌릴 수 없다. 스프링에서 @Transactional이 붙은 메서드는 정상으로 끝나면 스프링이 커밋하고, 예외로 끝나면 롤백 규칙을 보고 롤백한다. 기본 규칙에서는 RuntimeException과 Error가 메서드 밖으로 던져지면 롤백하고 체크 예외는 롤백하지 않는다. 트랜잭션이 없으면 JDBC 연결의 기본 설정인 자동 커밋에 따라 저장 문장마다 곧바로 커밋되므로, 뒤에서 예외가 나도 앞의 저장은 롤백할 수 없다.
- 자동 커밋(autocommit)
- 자동 커밋은 트랜잭션을 따로 시작하지 않았을 때 DB가 SQL 문장 하나하나를 실행하자마자 커밋하는 방식이다. MySQL은 새 연결을 자동 커밋을 켠 채로 시작하므로, 문장 하나가 곧 트랜잭션 하나이고 그 결과가 바로 확정되어 다른 연결에서도 보인다. 이렇게 확정된 문장은 ROLLBACK으로 되돌릴 수 없다. 트랜잭션을 시작하면 COMMIT이나 ROLLBACK으로 끝낼 때까지 자동 커밋이 꺼지고, 끝나면 이전 상태로 돌아온다. 자동 커밋을 끈 세션이 마지막 트랜잭션을 확정하지 않고 끝나면 MySQL(InnoDB)은 그 트랜잭션을 롤백한다.
- BEGIN, COMMIT, ROLLBACK 문
- 트랜잭션을 시작하고 끝내는 SQL 문장이다. BEGIN은 트랜잭션을 시작하는 문장이고, MySQL에서는 START TRANSACTION과 같은 뜻이다(START TRANSACTION이 표준 SQL 문법이라 권장된다). COMMIT은 지금까지 트랜잭션 안에서 한 변경을 모두 영구히 확정하고, ROLLBACK은 그 변경을 모두 취소한다. 기본 설정에서는 자동 커밋이 켜져 있어, 시작하는 문장이 없으면 COMMIT과 ROLLBACK이 확정하거나 취소할 트랜잭션이 없다. MySQL의 저장 프로그램(프로시저, 트리거 등) 안에서는 BEGIN이 BEGIN ... END 블록의 시작으로 해석되므로 START TRANSACTION을 써야 하고, 시작하는 문장의 문법은 DB마다 조금씩 다르다.
이렇게 바꿔 보세요
- 처음 설정으로 — BEGIN으로 시작해 COMMIT으로 끝나는 묶음. 두 변경이 COMMIT에서 한꺼번에 확정되고, 그 전에는 다른 연결에서 보이지 않습니다.
- ROLLBACK으로 끝내면 — 묶음에서 한 일이 전부 취소되어 두 계좌가 처음 값으로 돌아갑니다.
- BEGIN을 지우면 — 묶음이 없어서 UPDATE가 실행되자마자 확정됩니다. 첫 UPDATE 직후 다른 연결에서 이미 바뀐 값이 보입니다.
- BEGIN을 지우고 ROLLBACK하면 — 이미 확정된 UPDATE는 ROLLBACK으로 되돌릴 수 없습니다.
- COMMIT도 ROLLBACK도 없이 연결이 끊기면 — 끝내지 않은 묶음은 확정되지 않습니다. H2에서는 연결이 끊길 때 취소되었습니다.
처음 설정으로 실행하면
- CREATE TABLE과 INSERT: DB가 account 테이블을 만들고 계좌 1(잔액 1000)과 계좌 2(잔액 0)를 넣습니다. 화면의 SQL은 DB에 접속한 연결 하나(이 연결)에서 실행합니다. 코드에는 없지만 확인을 위해 같은 DB에 연결을 하나 더(다른 연결) 열어 두고, 이 연결이 문장을 실행할 때마다 다른 연결로 계좌 잔액을 읽어 봅니다. 아직 트랜잭션을 시작하지 않았으므로 이 두 문장은 실행되자마자 확정되어, 다른 연결에서 읽어도 계좌 1은 1000, 계좌 2는 0입니다.
- BEGIN: DB가 이 연결에서 보내는 문장들을 한 묶음으로 다루기 시작합니다. 문장마다 바로 확정하던 방식(자동 커밋)을 이 묶음이 끝날 때까지 멈추므로, 지금부터 하는 일은 COMMIT이나 ROLLBACK을 만날 때까지 확정되지 않습니다.
- UPDATE account SET balance = balance - 300 WHERE id = 1: DB가 계좌 1의 잔액을 1000에서 700으로 바꿉니다. 묶음 안의 일이라 아직 확정되지 않았습니다. 이 연결에서 읽으면 700이지만, 다른 연결에서 읽으면 여전히 1000입니다. 확정된 값만 보면 두 계좌의 합은 1000 그대로입니다.
- UPDATE account SET balance = balance + 300 WHERE id = 2: DB가 계좌 2의 잔액을 0에서 300으로 바꿉니다. 이것도 묶음 안이라 확정되지 않았습니다. 이 연결에서는 700과 300이지만 다른 연결에서는 1000과 0 그대로입니다. 두 변경이 한 묶음에 쌓여 있습니다.
- COMMIT: DB가 묶음에 쌓인 두 변경을 한꺼번에 확정합니다. 이제 다른 연결에서 읽어도 계좌 1은 700, 계좌 2는 300입니다. 확정은 묶음 전체에 대해 한 번 일어나므로, 이 실습에서 다른 연결이 읽은 값은 1000과 0, 또는 700과 300뿐이었고 한쪽만 바뀐 값은 없었습니다. 묶음이 끝났으므로 이 연결은 문장마다 바로 확정하는 방식으로 돌아옵니다.
- SELECT * FROM account ORDER BY id: 이 연결에서 계좌를 읽습니다. 결과는 (1, 700), (2, 300)입니다. 확정된 값과 같습니다.
- 모든 것이 끝난 뒤, 새 연결에서 계좌를 읽어 봅니다. 계좌 1은 700, 계좌 2는 300이고 합은 1000입니다. 묶음 전체가 반영되었습니다.
실행 결과
- 트랜잭션은 BEGIN으로 시작해 COMMIT으로 끝나는 문장들의 묶음입니다. 두 UPDATE는 COMMIT에서 한꺼번에 확정되었고, 그 전에는 다른 연결에서 계좌 1이 1000으로 보였습니다.
- 묶음을 시작하고 확정하는 일은 우리가 쓴 BEGIN과 COMMIT이 합니다. 두 변경이 함께 보이거나 함께 보이지 않으므로, 돈이 사라지거나 생긴 순간이 다른 연결에는 보이지 않았습니다.
코드
CREATE TABLE account (id INT PRIMARY KEY, balance INT);
INSERT INTO account VALUES (1, 1000), (2, 0);
-- 시작: 여기서부터의 문장을 한 묶음으로 다룬다
BEGIN;
UPDATE account SET balance = balance - 300 WHERE id = 1;
UPDATE account SET balance = balance + 300 WHERE id = 2;
-- 끝: 묶음 전체를 확정한다
COMMIT;
SELECT * FROM account ORDER BY id;
이 모형의 전제
H2 2.4.240(메모리 DB, JDBC)에서 실행해 대조했고 MySQL에서의 실행 대조는 하지 않았습니다. 여섯 가지 조합을 모두, 화면의 SQL 전체(주석 줄 포함)를 글자 그대로 연결 하나(이 연결)에서 한 문장씩 실행했습니다. 두 연결이 같은 DB를 보도록 이름을 준 메모리 DB(DB_CLOSE_DELAY=-1)를 조합마다 새로 만들었고, 격리 수준은 H2 기본값인 READ COMMITTED 그대로 두었습니다. 대조한 것은 결과 표의 세 항목, 곧 첫 UPDATE 직후 따로 열어 둔 다른 연결이 SELECT balance FROM account WHERE id = 1로 읽은 값, 그리고 모든 것이 끝난 뒤(끝내지 않은 조합은 이 연결을 닫은 뒤) 새 연결로 읽은 두 계좌의 값입니다. 이 확인용 조회는 화면의 코드에 없는 것이고, 다른 연결의 조회가 기다리거나 실패한 경우는 없었습니다(시간 제한 3초를 두고 실행). 단계 문장이 기대는 사실로 문장마다 이 연결과 다른 연결이 읽은 두 계좌의 값, BEGIN 뒤 자동 커밋이 꺼지고 COMMIT이나 ROLLBACK 뒤 다시 켜지는 것(JDBC의 getAutoCommit), 마지막 SELECT가 이 연결에 보여 준 값도 실행으로 확인했습니다. 끝내지 않고 연결을 닫으면 확정되지 않은 변경이 취소된다는 것은 H2에서 JDBC 연결을 닫았을 때 관찰한 동작이고, 연결이 끊기는 다른 방식(네트워크 단절, 서버 종료)은 실행하지 않았습니다. 격리 수준에 따라 달라지는 세부(다른 연결이 어떤 값을 볼 수 있는지, 기다리는지)는 다루지 않았으며 H2 기본값에서 본 결과일 뿐입니다. 트랜잭션을 시작하는 문장은 H2 2.4.240이 START TRANSACTION을 받지 않아(구문 오류) BEGIN으로 적었습니다. BEGIN은 H2 2.4.240에서 실행되었으나 H2 문서의 Commands 페이지에서는 찾지 못했습니다. COMMIT, ROLLBACK, SET AUTOCOMMIT은 그 페이지에 있습니다. MySQL 8.4 문서의 15.3.1절 START TRANSACTION, COMMIT, and ROLLBACK Statements에는 BEGIN과 BEGIN WORK가 START TRANSACTION의 다른 이름이라는 것(START TRANSACTION이 표준 SQL이며 권장된다는 것), 저장 프로그램 안에서는 BEGIN이 BEGIN ... END 블록의 시작으로 해석되므로 START TRANSACTION을 써야 한다는 것, START TRANSACTION 동안 자동 커밋이 꺼지고 COMMIT이나 ROLLBACK 뒤 이전 상태로 돌아온다는 것, 기본으로 자동 커밋이 켜져 있어 문장마다 확정되고 ROLLBACK으로 되돌릴 수 없다는 것, COMMIT은 변경을 영구하게 하고 ROLLBACK은 변경을 취소한다는 것이 적혀 있습니다. 17.7.2.2절 autocommit, Commit, and Rollback(InnoDB)에는 새 연결이 자동 커밋을 켠 채 시작한다는 것, 자동 커밋을 끈 세션이 마지막 트랜잭션을 확정하지 않고 끝나면 MySQL이 그 트랜잭션을 롤백한다는 것이 적혀 있습니다. 위 두 절은 직접 열어 확인했지만 MySQL이 같은 SQL에서 같은 값을 내는지는 실행으로 확인하지 않았습니다. 화면의 코드는 CREATE TABLE과 INSERT를 포함한 전체이고, 다른 연결의 조회와 연결을 닫는 일은 보이지 않습니다. 이 실습은 한 묶음의 시작과 끝만 다루며, 격리 수준, 락, 중첩 트랜잭션, 스프링의 @Transactional은 다루지 않습니다.
앞 실습: 인덱스 · 다음 실습: 행 잠금