직렬화 실패 처리
직렬화 실패 처리 (Serialization Failure Handling)
트랜잭션이 격리 수준(Isolation Level)을 Repeatable Read나 Serializable로 두고 돌다 보면, "이러다 데이터가 꼬이겠다" 싶은 상황을 데이터베이스가 미리 막느라 에러를 던질 때가 있어요. 이런 에러는 단순 버그가 아니라 설계된 동작이라서, 우리가 코드에서 잘 맞이하고 다시 시도(retry)해 주는 게 핵심이에요. 이번에 그 에러의 정체와 올바른 재시도 전략을 정리해 볼게요.
출처: 공식문서
직렬화 실패라는 에러
Repeatable Read와 Serializable 격리 수준에서는 직렬화 이상(serialization anomaly)이 생기지 않도록 하기 위해 특별한 에러를 만들어내요. 그래서 이 두 격리 수준을 쓰는 애플리케이션은, 직렬화 에러로 실패한 트랜잭션을 다시 시도할 준비가 되어 있어야 해요.
이 에러의 메시지 텍스트는 상황에 따라 조금씩 달라지지만, SQLSTATE 코드는 항상 40001(serialization_failure)로 고정되어 있어요. 에러 코드를 보고 판단하면 되니까 메시지 문구에 휘둘리지 않아도 돼요.
함께 재시도할 만한 에러들
직렬화 실패만 다시 시도할 수 있는 건 아니에요. 상황에 따라 아래 에러들도 재시도를 고려해 볼 만해요.
- 교착 상태(deadlock): SQLSTATE
40P01(deadlock_detected). 일시적인 충돌인 경우가 많아 재시도가 도움이 돼요. - 유니크 키 위반: SQLSTATE
23505(unique_violation). 예를 들어 애플리케이션이 현재 저장된 키를 조사한 뒤 그다음 값을 골라 쓰는 방식이라면, 다른 애플리케이션 인스턴스가 똑같은 새 키를 동시에 골랐을 때 생길 수 있어요. 이런 경합은 재시도로 풀려요. - 배타 제약 위반(exclusion constraint violation): SQLSTATE
23P01(exclusion_violation).
다만 직렬화 실패는 무조건 다시 시도하는 게 추천되는 반면, 이 에러들은 영구적인 상태 문제일 수도 있어서 조심스럽게 다뤄야 해요. 영구 문제인데 계속 재시도하면 헛수고가 되니까, 어떤 에러는 어떤 조건에서 재시도할지 설계가 필요해요.
어디부터 다시 시도해야 하나
중요한 포인트는 트랜잭션 전체를 통째로 다시 시도해야 한다는 거예요. 어떤 SQL을 실행할지, 어떤 값을 쓸지 결정하는 모든 로직까지 포함해서요. 그래서 PostgreSQL은 자동 재시도 기능을 제공하지 않아요. 옳음을 보장하면서 자동으로 재시도해 줄 방법이 없기 때문이에요.
재시도가 항상 한 번에 성공한다는 보장도 없어요. 여러 번 재시도가 필요할 수 있고, 경합이 아주 심한 상황에서는 성공까지 많은 시도가 걸릴 수도 있어요.
충돌한 PREPARED 트랜잭션이 있다면
충돌의 원인이 이미 준비된 트랜잭션(prepared transaction, 2PC로 준비 상태에 있는 트랜잭션)일 경우에는, 그 트랜잭션이 커밋되거나 롤백될 때까지는 진전을 못 할 수 있어요. 이 경우엔 단순 재시도로는 해결이 안 되고, 그 준비 트랜잭션이 정리되길 기다려야 할 수 있다는 점을 알아 두면 좋아요.
더 알아보기 (Learn more)
- PostgreSQL 격리 수준과 동시성 제어 전반: Chapter 13. Concurrency Control
serialization_failure/deadlock_detectedSQLSTATE 코드 목록: Appendix A. PostgreSQL Error Codes