SERIALIZABLE은 PostgreSQL에서 트랜잭션 격리 수준 중 가장 높은 단계야. 이 수준에서는 병렬 트랜잭션의 결과가 마치 하나씩 차례대로 실행된 것처럼 보장돼. 그래서 Dirty Read, Non-Repeatable Read, Phantom Read 같은 병렬 실행의 이상 현상은 절대 발생하지 않아.
쉽게 말해서, SERIALIZABLE은 병렬 트랜잭션 사이에 완벽한 질서와 일관성을 만들어줘. 마치 PostgreSQL이 "모든 트랜잭션, 줄 서세요~!"라고 말하는 것처럼 말이지.
왜 SERIALIZABLE이 필요할까? 가끔은 데이터가 병렬로 바뀌더라도 100% 일관성을 보장하고 싶을 때가 있잖아. 슈퍼마켓에서 계산원들이 동시에 손님을 처리하는 장면을 상상해봐. 만약 순서를 제대로 안 지키면, 계산대에서 나온 상품이 실제로 산 것보다 많아질 수도 있겠지. SERIALIZABLE에서는 그런 일이 절대 안 일어나.
SERIALIZABLE 격리 수준 설정 예제
SERIALIZABLE 격리 수준을 설정하려면 아래 명령어를 쓰면 돼:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
예를 들어, 이 격리 수준을 사용하는 트랜잭션을 만들어보자:
BEGIN; -- 트랜잭션 시작
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; -- 격리 수준 설정
SELECT * FROM products WHERE category = 'Electronics'; -- 상품 목록 가져오기
UPDATE products SET stock = stock - 1 WHERE product_id = 123; -- 재고 업데이트
COMMIT; -- 변경사항 커밋
케이스: 영화관 티켓 예약
실제로 SERIALIZABLE이 꼭 필요한 상황을 예로 들어보자. 네가 온라인 영화관 티켓 예약 시스템을 만든다고 해봐. 유저들이 좌석을 고르는데, 같은 좌석이 동시에 두 명에게 팔리는 걸 막고 싶겠지?
먼저 좌석 테이블을 만들어보자:
CREATE TABLE seats (
seat_id SERIAL PRIMARY KEY,
is_booked BOOLEAN DEFAULT FALSE
);
이제 좌석 몇 개를 추가해보자:
INSERT INTO seats (is_booked) VALUES (FALSE), (FALSE), (FALSE);
SERIALIZABLE 트랜잭션 예시를 들어볼게.
안전하게 좌석을 예약하는 방법은 이렇다:
BEGIN; -- 트랜잭션 시작
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; -- SERIALIZABLE 격리 수준
-- 좌석이 비었는지 확인
SELECT is_booked FROM seats WHERE seat_id = 1;
-- 좌석 예약
UPDATE seats SET is_booked = TRUE WHERE seat_id = 1;
COMMIT; -- 예약 확정
만약 두 번째 병렬 트랜잭션이 같은 좌석을 예약하려고 하면 PostgreSQL이 혼란을 허용하지 않고, serialization conflict 에러를 던져줄 거야.
Phantom Read 방지
이제 우리가 그렇게 피하고 싶었던 "팬텀 리드"에 대해 알아보자. Phantom Read는 트랜잭션 도중에 다른 트랜잭션이 데이터를 추가하거나 삭제해서, 결과가 바뀌는 현상이야. 예를 들어, 트랜잭션이 특정 행 개수를 기대하고 있는데, 갑자기 다른 트랜잭션이 행을 추가하거나 삭제해서 결과가 달라지는 거지.
예시를 보자:
트랜잭션 시작 전 데이터
| id | balance | user |
|---|---|---|
| 1 | 1000 | 앨리스 |
| 2 | 500 | 밥 |
트랜잭션 1
BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- 잔고가 400보다 큰 유저 수 세기
SELECT COUNT(*) FROM accounts WHERE balance > 400;
-- 기대 결과: 2 (앨리스와 밥)
트랜잭션 2
다른 세션에서 병렬 트랜잭션이 실행돼:
BEGIN;
INSERT INTO accounts (id, balance, user) VALUES (3, 700, '찰리');
COMMIT;
트랜잭션 1로 돌아가기
-- 쿼리 다시 실행
SELECT COUNT(*) FROM accounts WHERE balance > 400;
SERIALIZABLE을 안 쓰면 결과가 2에서 3으로 바뀌어. 트랜잭션 1 도중에 찰리가 추가됐으니까. 이게 바로 Phantom Read야.
하지만 SERIALIZABLE에서는 PostgreSQL이 트랜잭션 1의 "세상"을 트랜잭션 시작 시점으로 고정시켜서, 찰리를 볼 수 없게 해줘.
SERIALIZABLE 수준의 특징과 한계
SERIALIZABLE이 완벽한 격리를 어떻게 제공하는지 알아봤지. 근데 세상에 완벽한 게 어디 있겠어? 단점도 솔직하게 얘기해보자.
성능 저하
SERIALIZABLE은 READ COMMITTED나 REPEATABLE READ보다 훨씬 많은 리소스를 써. 왜냐면 PostgreSQL이 모든 트랜잭션 충돌을 추적하면서, 실제로는 순차적으로 실행되는 것처럼 시뮬레이션해야 하거든.
직렬화 에러
PostgreSQL이 "완벽한 순서"로 트랜잭션을 실행할 수 없다고 판단하면, 직렬화 에러(serialization_failure)를 발생시키고 트랜잭션을 롤백해버려.
에러 예시:
ERROR: could not serialize access due to concurrent update
이런 상황에서는 트랜잭션을 다시 시도하는 게 일반적이야:
DO $$
DECLARE
done BOOLEAN := FALSE;
BEGIN
WHILE NOT done LOOP
BEGIN
-- 트랜잭션 시작
BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- 작업 실행
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 변경사항 커밋
COMMIT;
done := TRUE; -- 성공하면 루프 종료
EXCEPTION WHEN serialization_failure THEN
ROLLBACK; -- 에러 시 롤백
END;
END LOOP;
END;
$$;
SERIALIZABLE을 쓰는 시스템에서는 이런 방식이 흔해.
이 코드는 PL-SQL로 작성됐어. 나중에 다시 다룰 거야. 그냥 멋지고 실제로 돌아가는 코드를 보여주고 싶었어. 그리고 PL-SQL이 왜 필요한지도 보여주려고 :)
SERIALIZABLE 언제 써야 할까?
이 격리 수준은 실수의 대가가 엄청 클 때 써야 해:
- 결제 처리나 보너스 분배 같은 금융 트랜잭션
- 재고 관리 시스템에서 주문 중복을 막고 싶을 때
- 온라인 예약처럼 리소스 예약 충돌이 절대 일어나면 안 될 때
데이터가 100% 일관적이어야 하고, 성능은 좀 느려도 괜찮다면 SERIALIZABLE이 최고의 친구가 될 거야.
GO TO FULL VERSION