여러 SQL문을 더 쪼갤 수 없는 하나의 작업으로 묶은 단위. 논리적으로 한 덩어리인 일을 DB에도 한 덩어리로 알려준다.
계좌 이체
계좌 이체가 대표적이다. A의 잔고를 줄이는 UPDATE와 B의 잔고를 늘리는 UPDATE는 둘 다 성공해야 이체이지, 하나만 반영되면 돈이 사라지거나 복제된다. 이런 “일부만 성공한 상태”를 아예 만들지 않으려고 묶는다.
읽기만 하는 작업에도 필요하다. 보고서를 만드느라 여러 테이블을 읽는 동안 다른 트랜잭션이 데이터를 바꾸면 보고서의 앞부분과 뒷부분이 서로 다른 시점의 그림이 된다. 읽기 전용 트랜잭션은 시작 시점의 스냅샷을 잡아주므로 읽는 내내 같은 그림을 본다.
시작과 끝
BEGIN으로 시작해 COMMIT이나 ROLLBACK으로 끝난다. 커밋은 지금까지 한 작업을 영구히 반영하고, 롤백은 전부 취소해 시작 이전 상태로 되돌린다. 끝나는 방법이 하나 더 있는데 크래시나 정전 같은 예기치 못한 종료다. 이것도 롤백으로 처리된다. DB가 다시 뜰 때 커밋되지 않은 트랜잭션이 있었음을 감지해서 되돌린다.
MySQL에서는 START TRANSACTION으로 열고 COMMIT 또는 ROLLBACK으로 닫는다.
내가 명시하지 않아도 트랜잭션은 항상 있다. 대부분의 DBMS가 SQL문 하나하나를 자동으로 트랜잭션 처리해서 성공하면 커밋하고 실패하면 롤백한다. MySQL은 이 AUTOCOMMIT이 기본으로 켜져 있고 SELECT @@autocommit으로 확인할 수 있다. START TRANSACTION을 실행하면 그 순간 autocommit이 꺼지고 트랜잭션이 끝나면 원래 설정으로 돌아온다.
JDBC의 connection.setAutoCommit(false)가 하는 일이 정확히 이것이다. autocommit을 끄는 동시에 지금부터 트랜잭션이라는 선언이다. 커넥션은 반납되어 재사용되므로 다 쓰고 나면 true로 되돌려놔야 한다.
커밋한 것을 언제 디스크에 쓰는지는 DBMS마다 다르고, 그 선택이 성능 특성을 가른다. 쿼리를 실행할 때마다 변경을 디스크에 기록해두는 구현은 커밋할 때 “이 트랜잭션이 커밋되었다”만 적으면 되니 커밋이 빠른 대신 쿼리 실행 중 I/O가 많고 롤백이 느리다. 반대로 메모리에 모았다가 커밋 시점에 한 번에 flush하는 구현은 쿼리가 빠르고 롤백도 메모리만 비우면 되지만 커밋이 느리다. Postgres가 앞쪽에 가까워서, 커밋이 짧은 만큼 커밋 도중에 시스템이 죽을 확률도 낮아진다. 어느 쪽이 옳은 것이 아니라 무엇에 최적화할지의 문제다.
ACID
트랜잭션이 트랜잭션이려면 네 가지를 만족해야 한다.
원자성(Atomicity)은 전부 성공하거나 전부 실패하라고 요구한다. 99개가 성공했어도 하나가 실패하면 전부 롤백이다. 넷 중 가장 근본인데, 애초에 여러 작업을 하나로 묶는 것이 목적이라 원자성이 깨지면 나머지 셋은 의미가 없어진다.
일관성(Consistency)에서는 트랜잭션 전후로 DB가 유효한 상태여야 한다. 제약조건이나 트리거로 정의한 규칙을 어기면 커밋 전에 DBMS가 알려주고 롤백된다. 다만 DBMS가 챙기는 범위는 DB에 정의된 규칙까지다. 규칙으로 선언하지 않은 것은 애플리케이션 몫으로 남는다. 제약조건
격리성(Isolation)은 여러 트랜잭션이 동시에 돌아도 혼자 도는 것처럼 보이게 한다. 엄격하게 지킬수록 동시성이 떨어져 느려지므로, DBMS는 여러 단계를 제공하고 개발자가 고르게 한다. 격리 수준
지속성(Durability)이 있으니 커밋된 것은 시스템이 죽어도 남는다. HDD나 SSD 같은 비휘발성 저장소에 기록되고 로그가 남는다.
커밋과 롤백을 실제로 수행하는 쪽은 DBMS다. 개발자가 챙길 것은 트랜잭션의 단위를 어떻게 끊을지, 어떤 문제가 났을 때 롤백할지다.
트랜잭션 경계
경계를 어디에 둘 것인가가 애플리케이션에서 실제로 부딪히는 문제다. 쿼리 하나씩 autocommit으로 돌리면 “말을 두 칸 움직인다”가 UPDATE 두 번, 트랜잭션 두 개가 된다. 첫 번째만 커밋된 채 예외가 터지면 데이터가 어중간하게 남는다.
그래서 비즈니스 로직 하나를 트랜잭션 하나로 잡는다. 서비스 계층에서 로직 시작 전에 열고 끝난 뒤 닫는 것을 트랜잭션 경계를 설정한다고 한다.
직접 하면 대가가 따른다. 서비스 코드가 트랜잭션 코드로 지저분해지고, 커넥션을 어떻게 물고 있을지 직접 관리해야 하고, 데이터 액세스 기술에 코드가 묶여 JDBC에서 JPA로 바꾸면 전부 고쳐야 하고, 비즈니스 로직과 관심사가 다른 코드가 한자리에 섞인다. 이 넷을 걷어내는 것이 선언적 트랜잭션이다.