애플리케이션 레벨에서의 데이터 일관성 검사
애플리케이션 레벨에서의 데이터 일관성 검사
데이터 무결성에 관한 비즈니스 규칙을 지키는 건 생각보다 까다로워요. Read Committed 트랜잭션에서는 문장마다 데이터 뷰가 바뀌고, 쓰기 충돌이 생기면 단일 문장도 스냅샷에 머물지 않을 수 있거든요. 이번 섹션에서는 애플리케이션 레벨에서 데이터 일관성을 확실히 지키는 두 가지 방법, 직렬화 가능 트랜잭션과 명시적 잠금을 알아볼게요.
출처: 공식문서
13.4.1. 직렬화 가능 트랜잭션으로 일관성 강제하기
직렬화 가능(Serializable) 트랜잭션 격리 수준을 모든 쓰기와 일관된 데이터 뷰가 필요한 모든 읽기에 사용한다면, 일관성을 보장하기 위해 추가로 할 일은 없어요. 다른 환경에서 직렬화 가능 트랜잭션으로 일관성을 지키도록 작성된 소프트웨어라면 PostgreSQL에서도 별다른 수정 없이 "그냥 동작"할 거예요.
이 기법을 사용할 때, 직렬화 실패로 롤백된 트랜잭션을 자동으로 재시도해 주는 프레임워크를 애플리케이션이 사용한다면 개발자에게 불필요한 부담을 주지 않을 수 있어요. 그리고 default_transaction_isolation을 serializable로 설정해 두는 것도 좋은 생각이에요. 또한 트리거에서 트랜잭션 격리 수준을 검사해서, 다른 격리 수준이 실수로든 일관성 검사를 우회하려는 의도로든 사용되지 않도록 조치를 취하는 것도 현명해요. 성능 관련 제안은 13.2.3 절을 참고하세요.
경고: 직렬화 가능 트랜잭션과 데이터 복제 직렬화 가능 트랜잭션을 이용한 이러한 무결성 보호 수준은 아직 핫 스탠바이 모드(26.4 절)나 논리적 복제본에는 적용되지 않아요. 그래서 핫 스탠바이나 논리적 복제를 사용하는 경우에는 프라이머리에서 Repeatable Read와 명시적 잠금을 사용하는 편이 나을 수 있어요.
13.4.2. 명시적 차단 잠금으로 일관성 강제하기
직렬화 가능하지 않은 쓰기가 가능한 상황에서 행의 현재 유효성을 보장하고 동시 업데이트로부터 보호하려면 SELECT FOR UPDATE, SELECT FOR SHARE, 또는 적절한 LOCK TABLE 문을 사용해야 해요. (SELECT FOR UPDATE와 SELECT FOR SHARE는 반환된 행만 잠가 동시 업데이트를 막고, LOCK TABLE은 테이블 전체를 잠가요.) 다른 환경에서 PostgreSQL로 애플리케이션을 포팅할 때 이 점을 꼭 고려해야 해요.
다른 환경에서 전환하는 사람들이 주의할 또 다른 점은, SELECT FOR UPDATE가 동시 트랜잭션이 선택된 행을 업데이트하거나 삭제하지 못하게 보장하지는 않는다는 사실이에요. PostgreSQL에서 그렇게 하려면 실제로 행을 업데이트해야 해요. 설령 바꿀 값이 없더라도 말이죠. SELECT FOR UPDATE는 다른 트랜잭션이 같은 잠금을 획득하거나 잠긴 행에 영향을 주는 UPDATE나 DELETE를 실행하는 것을 일시적으로 막아요. 하지만 이 잠금을 보유한 트랜잭션이 커밋하거나 롤백하면, 잠금이 유지되는 동안 실제 UPDATE가 수행되지 않았다면 차단되었던 트랜잭션은 충돌하는 작업을 계속 진행하게 돼요.
비직렬화 가능 MVCC에서는 전역 유효성 검사에 특히 더 신중해야 해요. 예를 들어, 은행 애플리케이션이 한 테이블의 모든 입금 합계와 다른 테이블의 모든 출금 합계가 같다는 것을 확인하려고 한다고 해볼게요. 두 테이블 모두 활발하게 업데이트되고 있다면, 두 번의 연속된 SELECT sum(...) 명령 결과를 비교하는 것은 Read Committed 모드에서 신뢰할 수 없어요. 두 번째 쿼리에는 첫 번째 쿼리가 포함하지 않은 트랜잭션의 결과가 포함될 가능성이 높기 때문이에요. 두 합계를 단일 repeatable read 트랜잭션에서 수행하면 repeatable read 트랜잭션이 시작되기 전에 커밋된 트랜잭션의 효과만 정확히 보여줘요. 하지만 그 결과가 전달될 때쯤이면 그 답이 여전히 유효한지 의문이 들 수밖에 없어요. 만약 repeatable read 트랜잭션이 일관성 검사를 시도하기 전에 일부 변경을 직접 적용했다면, 검사의 유용성은 더욱 논쟁거리가 돼요. 트랜잭션 시작 후 변경 중 일부만 포함하고 일부는 포함하지 않기 때문이에요.
이런 경우라면 신중한 사람은 현재 현실을 명확하게 파악하기 위해 검사에 필요한 모든 테이블을 잠그고 싶을 거예요. SHARE 모드(또는 그 이상) 잠금은 현재 트랜잭션의 변경을 제외하고 잠긴 테이블에 커밋되지 않은 변경이 없다는 것을 보장해요.
또한 동시 변경을 막기 위해 명시적 잠금에 의존한다면 Read Committed 모드를 사용하거나, Repeatable Read 모드에서는 쿼리를 수행하기 전에 잠금을 획득하도록 주의해야 해요. repeatable read 트랜잭션이 획득한 잠금은 해당 테이블을 수정하는 다른 트랜잭션이 여전히 실행 중이지 않다는 것을 보장하지만, 트랜잭션이 본 스냅샷이 잠금 획득보다 이전 시점이라면, 그 스냅샷은 현재 커밋된 일부 변경보다 더 오래된 것일 수 있어요. repeatable read 트랜잭션의 스냅샷은 실제로 첫 번째 쿼리나 데이터 수정 명령(SELECT, INSERT, UPDATE, DELETE, 또는 MERGE)이 시작될 때 고정되므로, 스냅샷이 고정되기 전에 명시적으로 잠금을 획득하는 것이 가능해요.