데이타베이스/PostgreSQL

PostgreSQL 트랜잭션 격리 수준, 이젠 마스터하자!

shimdh 2025. 10. 30. 18:23
728x90

안녕하세요, 데이터베이스 애호가 여러분! 데이터베이스를 다루는 개발자라면 트랜잭션이라는 단어가 익숙할 테지만, 이게 단순히 작업을 묶는 도구가 아니라 격리 수준을 통해 데이터의 무결성과 일관성을 어떻게 지키는지 제대로 아시나요? 오늘은 PostgreSQL을 비롯한 대부분의 관계형 데이터베이스(RDBMS)에서 핵심적인 트랜잭션 격리 수준에 대해 깊이 파헤쳐 보겠습니다. 이 글을 통해 동시성 제어의 본질을 이해하고, 애플리케이션 요구사항에 딱 맞는 격리 수준을 선택하는 통찰력을 얻어가세요. 자, 이제 시작해볼까요?

728x90

트랜잭션: 데이터 무결성의 기초

먼저 기본부터 짚고 넘어가죠. 트랜잭션은 하나 이상의 SQL 작업이 단일 논리적 단위로 실행되는 일련의 과정입니다. 예를 들어, 은행 이체처럼 "계좌 A에서 돈 빼고 계좌 B에 넣기"를 하나의 작업으로 묶는 거예요. 모든 트랜잭션은 ACID 속성을 만족해야 하며, 이는 데이터베이스의 신뢰성을 보장하는 핵심 원칙입니다.

  • 원자성(Atomicity): 트랜잭션의 모든 부분이 함께 성공하거나 함께 실패합니다. 'All or nothing' 원칙으로, 중간에 실패하면 모든 변경이 취소되죠.
  • 일관성(Consistency): 트랜잭션이 데이터베이스를 유효한 상태에서 또 다른 유효한 상태로만 전환합니다. 제약 조건(예: NOT NULL, CHECK)이나 트리거를 위반하지 않아요.
  • 격리성(Isolation): 동시 실행되는 트랜잭션들이 서로에게 미치는 영향을 제어합니다. (오늘의 메인 토픽!)
  • 지속성(Durability): 트랜잭션이 커밋되면 시스템 장애(예: 전원 끊김)에도 변경 사항이 영구적으로 저장됩니다.

PostgreSQL에서 트랜잭션은 BEGIN으로 시작하고, SQL 명령어를 실행한 후 COMMIT(변경 저장) 또는 ROLLBACK(변경 취소)으로 끝납니다. 이 사이에 격리 수준이 데이터의 신뢰성을 좌우하죠. 간단한 예시 코드로 보자면:

BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;  -- 성공 시 모든 변경 적용
-- 또는 ROLLBACK;  -- 실패 시 모든 변경 취소

격리 수준의 중요성: 동시성과 정확성의 균형

격리 수준은 한 트랜잭션의 변경이 다른 동시 트랜잭션에 얼마나 '보이는지'를 제어합니다. 이는 동시성(Concurrency, 성능)정확성(Consistency, 격리) 사이의 트레이드오프예요. 격리 수준이 낮으면 성능은 좋지만 데이터 이상 현상이 발생할 수 있고, 높으면 안전하지만 지연이 생길 수 있죠.

주요 이상 현상(anomalies)은 다음과 같아요:

  • 더티 리드(Dirty Read): 아직 커밋되지 않은 변경(더티 데이터)을 읽음. 롤백되면 잘못된 데이터로 작업.
  • 반복 불가능한 읽기(Non-Repeatable Read): 같은 트랜잭션 내에서 같은 쿼리를 두 번 실행할 때 결과가 달라짐. 다른 트랜잭션이 중간에 변경 커밋.
  • 팬텀 읽기(Phantom Read): 같은 트랜잭션 내에서 범위 쿼리(예: WHERE age > 20)의 결과 행 수가 변함. 새 행이 삽입/삭제됨.

이제 PostgreSQL의 네 가지 표준 격리 수준을 하나씩 뜯어보죠. SQL 표준을 따르며, SET TRANSACTION ISOLATION LEVEL [level];으로 설정할 수 있습니다.

PostgreSQL의 네 가지 표준 격리 수준 파헤치기

1. Read Uncommitted (읽기 비확정)

  • 특징: 다른 트랜잭션의 아직 커밋되지 않은 변경(더티 데이터)을 읽을 수 있습니다. (PostgreSQL에서는 Read Committed와 동등하게 동작하지만, 표준적으로는 이 수준을 지원합니다.)
  • 동시성: 최고 수준 – 최대한 빠름.
  • 이상 현상: 더티 리드 허용.
  • 예시: 트랜잭션 A가 UPDATE 중에 트랜잭션 B가 그 값을 읽음. A가 롤백되면 B는 "유령" 데이터를 본 셈.

블로거의 생각: 이름처럼 '확정되지 않은' 데이터를 읽으니, 데이터 정확성보다 속도가 생명인 분석 시스템(예: 실시간 대시보드)에만 고려하세요. 일반 앱에서는 피하세요 – 일관성 문제가 치명적이에요.

2. Read Committed (읽기 확정)

  • 특징: PostgreSQL의 기본 수준. 각 쿼리는 실행 직전 커밋된 데이터만 봅니다. 트랜잭션 내 쿼리마다 스냅샷이 새로 생성되죠.
  • 동시성: 균형 잡힌 수준 – 대부분의 앱에 적합.
  • 이상 현상: 더티 리드 방지, 하지만 반복 불가능한 읽기와 팬텀 읽기 허용.
  • 예시: 트랜잭션 B가 쿼리1 실행 후 A가 커밋하면, B의 쿼리2는 변경된 결과를 봅니다. (동일 트랜잭션 내 불일치!)

블로거의 생각: '확정된' 데이터만 보는 안전한 기본값. 금융 앱처럼 정확성이 핵심인 곳에서도 충분해요. PostgreSQL이 이걸 디폴트로 둔 이유죠. 초보자라면 여기서 시작하세요.

3. Repeatable Read (반복 가능한 읽기)

  • 특징: 트랜잭션 시작 시점의 스냅샷을 사용해 동일 쿼리가 항상 같은 결과를 줍니다. MVCC(Multi-Version Concurrency Control) 덕분에 가능.
  • 동시성: Read Committed보다 약간 낮음 – 락이 더 오래 걸림.
  • 이상 현상: 반복 불가능한 읽기 방지, 하지만 팬텀 읽기 허용.
  • 예시: 트랜잭션 내 두 쿼리가 같은 행을 읽으면 동일 결과. 하지만 범위 쿼리에서 새 행이 추가되면 팬텀 발생.

블로거의 생각: 트랜잭션 내 "일관된 뷰"가 필요할 때 딱! 보고서 생성이나 장기 분석 쿼리에 유용. 팬텀 걱정되면 Serializable로 업그레이드하세요.

4. Serializable (직렬화 가능)

  • 특징: 동시 트랜잭션이 순차 실행된 것처럼 보이게 합니다. SSI(Serializable Snapshot Isolation)로 구현.
  • 동시성: 가장 낮음 – 충돌 시 직렬화 실패로 재시도 필요.
  • 이상 현상: 모든 이상 현상(더티 리드, 반복 불가능한 읽기, 팬텀 읽기) 완전 방지.
  • 예시: 두 트랜잭션이 동시에 같은 키로 INSERT하면 하나만 성공, 나머지는 실패 후 재시도.

블로거의 생각: 이론적 완벽주의자! 하지만 성능 오버헤드와 재시도 로직 때문에 극한 상황(예: 금융 이중 지불 방지)에만. 운영 시 모니터링 필수예요.

실제 적용: 어떤 격리 수준을 선택해야 할까?

격리 수준 선택은 앱의 성능 vs. 일관성 트레이드오프예요. 아래 가이드라인으로 도와드릴게요:

  • 속도가 중요하고 일관성 2% 부족해도 OK: Read Uncommitted. (드물게 사용)
  • 기본적인 안전성과 균형: Read Committed (PostgreSQL 디폴트 – 80% 앱 추천).
  • 트랜잭션 내 일관된 읽기 필요: Repeatable Read. (보고서/분석 앱).
  • 완벽한 격리 필수: Serializable. (금융/의료 등 고위험 도메인).

선택 팁:

  • 테스트부터: pg_isolation_test 같은 도구로 이상 현상 시뮬레이션.
  • 성능 최적화: 인덱스, 락 타임 최소화, 재시도 로직 구현 (Serializable용).
  • 예시 시나리오:
    • 이커머스 주문: Read Committed – 재고 업데이트 중 더티 리드 피함.
    • 대시보드 리포트: Repeatable Read – 중간 업데이트 무시.
    • 은행 이체: Serializable – 팬텀으로 인한 중복 이체 방지.
격리 수준 동시성 더티 리드 반복 불가능 팬텀 읽기 추천 시나리오
Read Uncommitted 최고 허용 허용 허용 고속 분석
Read Committed 높음 방지 허용 허용 일반 비즈니스
Repeatable Read 중간 방지 방지 허용 보고서 생성
Serializable 낮음 방지 방지 방지 고위험 거래
728x90