같은 값을 두 번 읽었는데 달라지는 이유: 격리 수준
격리 수준은 동시에 실행되는 트랜잭션에서 어떤 데이터 변화와 이상 현상을 허용할지를 정하는 규칙이다. 한 사용자가 읽는 동안 다른 사용자가 쓰고 커밋할 수 있는 환경에서는 실행 속도뿐 아니라 관찰 가능한 결과도 설계해야 한다. 같은 이름의 수준이라도 DBMS의 구현과 세부 보장은 다를 수 있다.
격리 수준과 스냅샷 규칙은 같은 트랜잭션의 반복 조회가 무엇을 볼지 결정한다.
아직 확정되지 않은 값과 새로 확정된 값
더티 읽기는 다른 트랜잭션이 아직 커밋하지 않은 변경을 읽는 현상이다. 그 변경이 나중에 취소되면 실제로 확정되지 않은 사실을 판단에 사용한 셈이 된다. 비반복 읽기는 같은 행을 다시 읽었을 때 다른 트랜잭션의 커밋으로 값이 달라지는 현상이다. 두 현상은 변경의 확정 여부에서 차이가 있다.
팬텀은 같은 검색 조건으로 다시 질의했을 때 해당하는 행 집합이 달라지는 현상으로 이해할 수 있다. 예를 들어 잔액이 100 이상인 계좌를 두 번 검색하는 사이 다른 트랜잭션이 조건에 맞는 계좌를 추가할 수 있다. 특정 기존 행의 값을 고정하는 것만으로 모든 검색 범위의 변화를 막을 수는 없다.
이름보다 실제 스냅샷 규칙을 확인한다
Read Committed는 대표적으로 커밋된 데이터만 읽게 한다. PostgreSQL의 이 수준에서는 각 문장이 시작할 때의 스냅샷을 사용하므로, 같은 트랜잭션의 두 SELECT 사이에 다른 커밋이 발생하면 새 값이 보일 수 있다. 처음에 잔액 100을 읽고 나중에 70을 읽는 결과가 가능하다.
Repeatable Read는 이름 그대로 반복 조회의 안정성을 목표로 하지만 구체적인 범위는 구현을 봐야 한다. PostgreSQL에서는 트랜잭션 스냅샷을 유지하는 방식으로 구현하며 팬텀 읽기도 막지만, 그 사실만으로 모든 동시 실행이 직렬 가능해지지는 않는다. 다른 제품의 같은 이름에 해당 규칙을 그대로 적용하면 안 된다.
스냅샷 격리에서는 서로 다른 행을 수정하는 두 트랜잭션이 각각 같은 과거 상태를 읽고 함께 커밋하여 여러 행에 걸친 규칙을 깨는 쓰기 편향이 가능하다. 예를 들어 최소 한 명의 당직자가 필요하다고 각자 확인한 뒤 서로 다른 자기 행을 비활성화하면 둘 다 빠질 수 있다. 같은 행의 동시 수정만 막아서는 이 규칙을 지키지 못한다.
Serializable은 커밋한 트랜잭션들이 어떤 직렬 실행과 같은 효과를 갖도록 하는 가장 강한 대표 수준이다. 잠금으로 진행을 조정하거나, 위험한 의존성을 감지하여 일부 트랜잭션을 중단하는 방법 등이 있다. PostgreSQL에서는 직렬화 실패가 발생할 수 있으므로 응용이 전체 트랜잭션을 다시 시도할 준비가 필요하다.
강한 보장은 비용과 처리 계약을 만든다
격리를 강화하면 추적과 대기, 재시도의 비용이 늘 수 있지만 모든 업무에서 반드시 더 느리다고 단정할 수는 없다. 낮은 수준에서 복잡한 응용 잠금을 직접 구현하는 비용도 있기 때문이다. 중요한 것은 처리량 수치 하나보다 어떤 규칙을 보장하고 충돌을 어떻게 사용자 경험으로 연결하는가다.
조회 중심의 보고서와 재고 수량을 동시에 차감하는 업무는 요구가 다를 수 있다. 실제 경계, 제약과 수정 조건을 함께 설계해야 한다. 격리 수준을 외울 때는 현상 이름만 나열하기보다 두 트랜잭션의 읽기·쓰기·커밋 순서를 그려 보고, 현재 설정에서 어떤 읽기 결과와 커밋 조합이 허용되는지 확인하는 편이 정확하다.