콘텐츠로 이동
Study NotePostgreSQL

트랜잭션과 실패 처리

결론부터
함께 성공해야 하는 변경은 한 트랜잭션으로 묶고, 실패했을 때 어디부터 다시 실행할지 정한다.

프로젝트를 만들었는데 첫 작업 생성에 실패하면 빈 프로젝트를 남겨도 되는가? 업무상 둘이 한 묶음이라면 둘 다 반영하거나 둘 다 취소해야 한다. 트랜잭션이 그 경계를 만든다.

이 장에서 처음 나오는 말3개
트랜잭션Transaction
함께 확정하거나 취소할 DB 작업의 단위.
COMMIT · ROLLBACK
변경을 확정하는 명령과 현재 트랜잭션을 취소하는 명령.
자동 커밋Autocommit
명시적으로 묶지 않으면 문장마다 트랜잭션을 끝내는 실행 방식.

공통 예제의 psql에서 실행한다. 마지막에는 취소해 원래 상태를 유지한다.

BEGIN;
INSERT INTO public.projects (name) VALUES ('복구 연습');
INSERT INTO public.tasks (project_id, title)
SELECT id, '첫 작업' FROM public.projects WHERE name = '복구 연습';
SELECT p.name, t.title
FROM public.projects p JOIN public.tasks t ON t.project_id = p.id
WHERE p.name = '복구 연습';
ROLLBACK;

트랜잭션 안에서는 새 프로젝트와 작업 한 행이 보인다. ROLLBACK 이후 같은 SELECT는 0행이다. COMMIT으로 끝내면 둘 다 확정된다. 내 트랜잭션은 자신의 미확정 변경을 볼 수 있지만 다른 연결의 일반 조회에는 아직 보이지 않는다. (트랜잭션 기초)

실패 뒤의 명령은 왜 실행되지 않나

섹션 제목: “실패 뒤의 명령은 왜 실행되지 않나”

명시적인 트랜잭션에서 SQL 오류가 나면 기본적으로 실패 상태가 된다. 오류 문장을 건너뛰고 다음 UPDATE를 보내는 것으로 회복되지 않는다. ROLLBACK하거나 미리 둔 SAVEPOINT로 돌아가야 한다.

BEGIN;
SAVEPOINT optional_title;
UPDATE public.tasks SET title = '' WHERE id = 1;
-- CHECK 제약 오류가 의도한 결과다.
ROLLBACK TO SAVEPOINT optional_title;
SELECT title FROM public.tasks WHERE id = 1;
ROLLBACK;

SELECT는 원래 제목 백업 확인을 보여 준다. 앱 드라이버가 자동으로 트랜잭션을 여는지, 오류 후 rollback을 하는지도 확인해야 한다. SAVEPOINT가 외부 API 호출까지 되돌려 주는 것은 아니다.

성질이 예제에서의 의미
원자성프로젝트와 작업 생성이 함께 확정되거나 취소된다
일관성선언한 제약을 깨는 상태로 확정하지 않는다. 업무 규칙 설계는 여전히 필요하다
격리성동시 실행이 서로 어떻게 보이는지를 격리 수준으로 제어한다
지속성확정한 변경을 장애 후에도 보존하도록 WAL과 저장 장치에 기록한다

이 네 가지를 ACID라고 부른다. “트랜잭션으로 감쌌으니 경쟁 상태가 없다”는 결론은 나오지 않는다. 다른 요청과의 관계는 동시성, 저장 보장은 WAL에서 다룬다.

연결이 끊기면 재시도해도 되나

섹션 제목: “연결이 끊기면 재시도해도 되나”

COMMIT 요청 뒤 응답을 받기 전에 연결이 끊기면 앱은 확정 여부를 모를 수 있다. 같은 주문을 무조건 다시 INSERT하면 중복 처리할 수 있으므로 요청 ID에 유일 제약을 두는 등 다시 실행해도 같은 업무 결과가 되도록 설계한다.

잠금 교착이나 직렬화 실패는 보통 트랜잭션 전체를 새로 시작해 재시도해야 한다. 트랜잭션 안에서 이메일까지 발송했다면 DB rollback이 이메일을 취소하지 못한다. 외부 부수 효과는 확정 이후 처리하거나 별도 전달 기록과 재처리 방식을 둔다.

이해 확인: ROLLBACK 뒤 identity 번호가 건너뛰면 원자성이 깨진 것일까? 아니다. 행 변경은 취소되지만 시퀀스에서 이미 사용한 번호는 되돌리지 않는다. (시퀀스의 동시성)