LabHub
Get started
배우기 러닝패스 코스

CS for Building Good Services — Relearning Textbook Ideas by Measuring

Check-Then-Write Breaks; Constraints Hold

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

한 줄 요약

불변식을 애플리케이션이 "확인하고 쓰면" 확인과 쓰기 사이의 틈에서 깨집니다. 데이터베이스가 제약으로 들고 있으면 그 틈이 없습니다. 제약으로 표현할 수 없는 여러 행의 규칙만 SERIALIZABLE 에 맡기고, 그때 짝이 되는 재시도 루프는 40001 만 다시 해야 합니다.

왜 이게 필요했나

"이 이메일이 없으면 가입시킨다" 를 SELECT 다음 INSERT 로 쓰면, 두 요청이 동시에 '없다' 를 보고 둘 다 넣습니다. 예약 시간 겹침 검사도 같은 모양입니다. 격리 수준과 쓰기 편향은 '데이터베이스 개념' 코스의 트랜잭션 실습이, 행 잠금은 'PostgreSQL 심화' 코스가 다룹니다. 이 모듈은 그다음 질문을 봅니다 — 규칙 자체를 데이터베이스에 넘기려면 무엇을 알아야 하나. 멱등 키에 UNIQUE 와 ON CONFLICT 를 쓰는 설계는 '프로덕션 백엔드 API 캡스톤' 이 다루니, 여기서는 그 문장이 정확히 무엇을 돌려주는지와 UNIQUE 로는 표현되지 않는 규칙으로 넓힙니다.

어떻게 동작하나

UNIQUE 와 ON CONFLICT. INSERT 문서에 따르면 ON CONFLICT DO NOTHING 은 넣는 대신 아무것도 하지 않고, DO UPDATE 는 동시성이 높아도 INSERT 나 UPDATE 가운데 하나가 원자적으로 일어남을 보장합니다. 함정은 RETURNING 입니다. 문서는 실제로 넣거나 고친 행만 돌려준다고 적습니다. DO NOTHING 으로 건너뛴 행은 나오지 않으니 "빈 결과 = 이미 있었다" 이고, 기존 행의 id 가 필요하면 DO UPDATE 를 써야 합니다. SET 절에서 기존 행은 표 이름(별칭)으로, 넣으려던 행은 EXCLUDED 로 가리킵니다. 둘을 헷갈려 EXCLUDED.visits + 1 이라고 쓰면 방문 수가 늘지 않고 매번 같은 값이 됩니다. 제약 문서는 기본적으로 두 NULL 을 같다고 보지 않아 UNIQUE 가 있어도 NULL 이 든 행은 중복될 수 있고, NULLS NOT DISTINCT 로 바꿀 수 있다고 말합니다.

겹침은 EXCLUDE 로. "같은 방의 예약 시간이 겹치지 않는다" 는 같음이 아니라 겹침 관계라 UNIQUE 로 쓸 수 없습니다. 배제 제약은 두 행을 지정한 연산자로 비교했을 때 적어도 하나가 거짓이거나 NULL 이어야 한다는 규칙이고, 걸면 그 종류의 인덱스가 자동으로 생깁니다. 범위 타입 문서[ ] 를 포함, ( ) 를 제외 경계로 쓰고, 두 인자 생성자는 아래 포함·위 제외인 [) 표준형을 만든다고 적습니다. 10–11시와 11–12시 예약은 [) 에서는 겹치지 않지만 [] 로 잡으면 11시 한 점에서 겹칩니다. 방 번호 같은 스칼라의 = 를 GiST 안에서 쓰려면 btree_gist 가 필요하고, 그래야 EXCLUDE USING gist (room WITH =, 시간범위 WITH &&) 한 제약에 둘을 묶습니다. CREATE TABLEWHERE (predicate) 를 붙이면 취소된 예약처럼 표의 일부에만 제약을 걸 수 있습니다(안에서는 부분 인덱스가 생깁니다).

문장 중간에 잠깐 깨지는 변경은 DEFERRABLE 로. 같은 문서의 호환성 절은 지연 불가능한 UNIQUE 를 PostgreSQL 이 행을 넣거나 고칠 때마다 즉시 검사하고, 표준은 문장 끝에 검사하라고 한다고 적습니다. 두 승객의 좌석을 맞바꾸는 첫 UPDATE 가 곧바로 23505 를 받는 이유입니다. DEFERRABLE INITIALLY DEFERRED 는 트랜잭션 끝에만 검사합니다. 받아 주는 것은 UNIQUE·PRIMARY KEY·EXCLUDE·외래 키뿐이고 NOT NULL 과 CHECK 는 지연되지 않습니다. ALTER TABLE 의 ALTER CONSTRAINT 는 지금 외래 키만 바꿀 수 있어서 UNIQUE 는 지우고 다시 만들어야 합니다. 그리고 지연 가능 제약은 ON CONFLICT 의 판정 기준(arbiter)이 될 수 없습니다 — 가입 이메일에 걸면 upsert 가 깨집니다.

여러 행의 규칙은 격리와 재시도로. 제약 문서는 CHECK 가 검사 중인 행 말고 다른 행을 참조하는 것을 지원하지 않는다고 못박습니다. "가족 지갑 합계가 음수가 되면 안 된다" 는 여기에 걸립니다. 이런 규칙은 SERIALIZABLE 이 지키고, 실패는 언제나 SQLSTATE 40001 로 옵니다. 직렬화 실패 처리 절은 어떤 SQL 을 낼지 정하는 로직까지 포함해 트랜잭션 전체를 다시 해야 하며, 그래서 PostgreSQL 은 자동 재시도를 주지 않는다고 설명합니다. 다시 할 대상은 40001 과 교착(40P01)이고, 23505·23P01 은 영구적인 오류일 수 있어 더 조심하라고 합니다. psycopg 3 는 예외의 sqlstate 로 코드를 주고, 오류 코드 부록은 메시지 문구가 아니라 코드로 분기하라고 권합니다.

불변식 둘 곳 도구
이메일 하나에 회원 하나 제약 UNIQUE + ON CONFLICT
같은 방 시간이 안 겹침 제약 EXCLUDE + btree_gist + [)
좌석 하나에 승객 하나 제약 DEFERRABLE UNIQUE
여러 행의 합계 조건 격리 SERIALIZABLE + 40001 재시도
외부 메일을 한 번만 애플리케이션 멱등 키·아웃박스

현장에서 만나는 모습

제약은 기존 행도 검사합니다. ALTER TABLE 문서는 제약을 추가하면 보통 모든 행이 조건을 만족하는지 표를 훑는다고 적습니다. 그래서 운영 표에 UNIQUE 하나 거는 일은 "이미 들어간 중복을 어떻게 정리하나" 부터 시작합니다. 예약처럼 지울 수 없는 기록은 지우지 말고 취소 상태로 돌린 뒤 활성 행에만 제약을 겁니다. 재시도 루프에서 가장 흔한 사고는 except Exception 으로 모든 오류를 다시 하는 코드입니다. 제약 위반은 백 번 다시 해도 같은 결과이고, 횟수가 다 차면 실패를 삼킨 채 성공이라고 보고하는 코드까지 나옵니다.

다음 실습에서 할 것

두 세션으로 중복 가입을 재현한 뒤 기존 중복을 정리하고 UNIQUE 와 ON CONFLICT 로 막습니다. 예약 표에서 기존 겹침을 찾아 취소하고 배제 제약을 걸고, 좌석 맞바꾸기를 DEFERRABLE 로 풉니다. psycopg 3 로 40001 만 다시 하는 재시도 루프를 쓰고, 채점기가 일부러 일으키는 오류 앞에서 그 루프가 제대로 멈추는지 확인한 뒤, 불변식마다 둘 자리를 표로 정리합니다.