변경을 확정하고 다른 세션에 보이게 합니다. 이 시점에 잡고 있던 행 락이 풀립니다.
이 파트에서 다루는 내용
Oracle에는 트랜잭션 시작 명령이 없습니다
다른 DBMS처럼 BEGIN TRANSACTION 을 쓰지 않습니다. DML을 실행하는 순간 트랜잭션이 자동으로 시작되고, COMMIT이나 ROLLBACK을 만날 때까지 이어집니다.
이 차이 때문에 생기는 실무 사고가 있습니다. 개발자가 SQL 도구에서 UPDATE를 실행하고 커밋하지 않은 채 자리를 비우면, 그 행은 계속 잠겨 있습니다. 다른 세션은 무한정 기다리고, 운영 화면이 멈춥니다.
트랜잭션 시작 이후의 모든 변경을 되돌립니다. UNDO에 보관해 둔 이전 이미지를 되살리는 방식입니다.
중간 지점을 표시해 두고 거기까지만 되돌립니다. 긴 배치 로직에서 일부만 취소할 때 씁니다.
SQL*Plus는 기본이 수동 커밋이지만 GUI 도구는 자동 커밋이 켜져 있을 수 있습니다. 운영 DB에 붙기 전에 반드시 확인합니다.
DDL은 앞의 작업을 몰래 커밋합니다
이건 Oracle에서 특히 조심해야 하는 동작입니다. CREATE, ALTER, DROP, TRUNCATE 같은 DDL을 실행하면 그 앞에 있던 DML이 자동으로 커밋됩니다. DDL 자체도 커밋됩니다.
되돌릴 생각으로 작업하다가 중간에 임시 테이블을 하나 만들면, 그 순간 앞의 변경이 확정되어 ROLLBACK이 통하지 않습니다.
UPDATE orders SET status_cd = 'CANCEL' WHERE order_dt < SYSDATE - 365;
-- 아직 커밋 안 함. 결과를 확인하고 되돌릴 생각이었다
CREATE TABLE tmp_check AS SELECT * FROM orders WHERE ROWNUM <= 10;
-- DDL 실행 -> 위 UPDATE가 함께 커밋됨
ROLLBACK;
-- 아무것도 되돌아가지 않습니다TRUNCATE 도 DDL입니다. DELETE 는 되돌릴 수 있지만 TRUNCATE 는 되돌릴 수 없다는 차이가 여기서 나옵니다.
운영 DB에서 데이터를 고칠 때는 DML만 실행하고, 확인이 끝난 뒤 커밋합니다. 확인용 임시 테이블이 필요하면 작업을 시작하기 전에 미리 만들어 둡니다.
Oracle에서 읽기는 쓰기를 기다리지 않습니다
이것이 Oracle의 동시성 모델에서 가장 중요한 특징이고, MSSQL에서 넘어온 사람이 가장 크게 놀라는 부분입니다.
누군가 어떤 행을 수정 중이어도, 다른 세션은 그 행을 읽을 때 기다리지 않습니다. Oracle이 UNDO에 보관된 변경 전 이미지를 대신 보여 주기 때문입니다. 그래서 커밋되지 않은 값이 보이는 일도 없습니다.
정리하면 읽는 사람은 쓰는 사람을 막지 않고, 쓰는 사람도 읽는 사람을 막지 않습니다. 서로 다른 행을 수정하는 경우에도 막히지 않습니다. 오직 같은 행을 동시에 수정할 때만 대기가 생깁니다.
UNDO의 이전 이미지를 읽어 일관성을 만듭니다. 조회는 어떤 경우에도 대기하지 않습니다.
기본 설정에서는 수정 중인 행을 읽으려면 대기합니다. READ_COMMITTED_SNAPSHOT 을 켜야 Oracle과 비슷해집니다.
Oracle과 같은 방식입니다. 다만 이전 버전을 테이블 안에 두기 때문에 VACUUM 관리가 필요합니다.
UNDO는 무한하지 않습니다. 아주 오래 도는 조회가 필요한 이전 이미지를 UNDO가 이미 덮어썼으면 snapshot too old 오류가 납니다.
-- 09:00:00 에 시작해 5분간 도는 집계 쿼리
SELECT SUM(amount) FROM orders WHERE order_dt >= TRUNC(SYSDATE);
-- 09:02 에 다른 세션이 주문을 넣고 커밋해도
-- 위 쿼리 결과에는 반영되지 않습니다.
-- 쿼리가 시작된 09:00:00 시점의 데이터로 끝까지 계산합니다.그래서 긴 조회 중에 값이 뒤섞이는 일이 없습니다. 다만 그 시점 이미지를 UNDO에서 계속 찾아야 하므로, 조회가 길고 그동안 변경이 많으면 ORA-01555 위험이 올라갑니다.
Oracle이 지원하는 격리수준은 두 개뿐입니다
표준 SQL은 네 단계를 정의하지만 Oracle이 실제로 지원하는 것은 READ COMMITTED와 SERIALIZABLE입니다. 여기에 조회 전용인 READ ONLY가 있습니다.
READ UNCOMMITTED가 없는 이유는 구조상 필요가 없기 때문입니다. Oracle에서는 커밋되지 않은 데이터를 읽는 일 자체가 불가능합니다. REPEATABLE READ도 별도 단계로 두지 않고, 필요하면 SERIALIZABLE이나 READ ONLY를 씁니다.
문장 단위로 시점이 정해집니다. 같은 트랜잭션에서 같은 쿼리를 두 번 하면 결과가 다를 수 있습니다.
트랜잭션 시작 시점으로 고정됩니다. 다만 그사이 다른 세션이 같은 행을 바꿨으면 커밋 시 ORA-08177 로 실패합니다.
SERIALIZABLE 을 쓴다면 ORA-08177 을 잡아 재시도하는 로직이 반드시 있어야 합니다. 없으면 부하가 올라갈 때 실패가 그대로 사용자에게 갑니다.
MSSQL과 PostgreSQL은 READ COMMITTED, MySQL InnoDB는 REPEATABLE READ가 기본입니다. 이관하면 동작이 달라질 수 있습니다.
-- 기본값. 각 문장이 자기 시작 시점을 봅니다
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 트랜잭션 전체가 하나의 시점을 봅니다
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- 조회 전용. 여러 쿼리가 같은 시점을 보게 할 때
SET TRANSACTION READ ONLY;리포트 배치에서 여러 쿼리의 합계가 서로 맞아야 한다면 READ ONLY 가 간단한 답입니다. 지정 명령은 트랜잭션의 첫 문장이어야 합니다.
갱신 손실은 격리수준으로 막히지 않습니다
두 사람이 같은 화면을 열어 각자 수정하고 저장하면, 나중에 저장한 사람이 앞사람의 변경을 덮어씁니다. 둘 다 정상적으로 커밋했으니 오류도 없습니다.
이건 기본 격리수준에서 막아 주지 않습니다. 애플리케이션이 명시적으로 처리해야 합니다. 방법은 두 가지입니다.
-- 읽는 순간 잠급니다. 다른 세션은 대기합니다
SELECT amount FROM orders WHERE order_id = 100 FOR UPDATE;
-- 대기하지 않고 즉시 실패 (ORA-00054)
SELECT amount FROM orders WHERE order_id = 100 FOR UPDATE NOWAIT;
-- 3초만 기다리고 실패
SELECT amount FROM orders WHERE order_id = 100 FOR UPDATE WAIT 3;
-- 잠긴 행은 건너뛰고 가능한 것만 (작업 큐에 유용)
SELECT * FROM job_queue WHERE status_cd = 'READY'
FOR UPDATE SKIP LOCKED;화면을 열어 둔 채로 FOR UPDATE 를 잡고 있으면 사용자가 커피를 마시러 간 동안 다른 사람이 전부 막힙니다. 사용자 입력이 끼는 구간에는 쓰지 않습니다.
-- 조회 시 버전을 함께 읽어 둡니다
SELECT order_id, amount, version FROM orders WHERE order_id = 100;
-- 저장할 때 읽었던 버전과 같은지 확인합니다
UPDATE orders
SET amount = :amount,
version = version + 1
WHERE order_id = 100
AND version = :read_version;
-- 변경된 행이 0건이면 그사이 누군가 먼저 수정한 것입니다잠그지 않으므로 동시성이 좋습니다. 대신 충돌 시 사용자에게 다시 안내해야 합니다. 화면 기반 업무에는 대체로 이쪽이 맞습니다. JPA의 @Version 이 이 방식입니다.
트랜잭션은 짧을수록 좋습니다
긴 트랜잭션은 락을 오래 잡고, UNDO를 계속 쌓고, 커넥션을 점유합니다. 문제가 생기면 되돌릴 양도 커집니다.
특히 트랜잭션 안에 외부 시스템 호출이나 파일 처리, 사용자 입력 대기를 넣으면 안 됩니다. 상대가 늦어지는 동안 DB의 락이 그대로 유지됩니다.
- 조회만 하는 구간은 트랜잭션 밖으로 뺍니다.
- 외부 API 호출은 트랜잭션을 커밋한 뒤에 하거나, 실패 시 보정하는 방식으로 설계합니다.
- 대량 배치는 전부 한 번에 커밋할지 중간 커밋할지 정합니다. 중간 커밋을 하면 UNDO 부담이 줄지만 중간에 실패했을 때 어중간한 상태가 남습니다.
- 중간 커밋을 한다면 어디까지 처리했는지 진행 상태를 함께 기록해 재시작할 수 있게 만듭니다.
- 커밋을 한 건마다 하면 오히려 느려집니다. 수천 건 단위로 묶는 편이 일반적입니다.
여기까지가 트랜잭션의 범위와 시점 문제였습니다. 같은 행을 동시에 수정할 때만 대기가 생긴다고 했는데, 실제로 그 대기가 어떻게 걸리고 어떻게 풀리는지가 다음 파트입니다.