ETag로 덮어쓰기 충돌을 막는 API의 설계 원리
한 줄 요약
‘내가 읽은 버전일 때만 수정’이라는 조건을 DB의 쓰기 문장까지 전달해야 변경 유실을 막는다.
왜 이게 필요했나
A와 B가 같은 메모 v1을 읽었다. A가 제목을 수정한 뒤 B가 오래된 화면에서 저장하면 조건 없는 UPDATE는 A의 변경을 조용히 덮어쓴다. 저장 직전에 SELECT로 버전을 확인해도 확인과 UPDATE 사이에 다른 요청이 끼어들 수 있다. 비교와 갱신은 한 SQL 문장으로 묶고 변경된 행 수를 확인해야 한다. 여기서는 SQLite 파일을 실제로 열어 별도 연결의 두 요청이 경쟁하게 만든다. 프로세스 메모리의 딕셔너리만 검사하지 않는다.
A 와 B 가 같은 메모 v1 을 읽었다. 본문의 두 갈래 도식을 순서대로 옮겼다. 이 실습은 SQLite 파일을 실제로 열어 별도 연결의 두 요청을 경쟁시킨다.
- 둘 다 GET 으로 ETag v1 을 받는다A 와 B 모두 같은 버전 v1 을 읽었다. 이후 저장 요청은 If-Match 에 이 태그를 담아 보낸다.
- A 의 PUT: 조건이 맞아 적용된다UPDATE 문에 WHERE version=1 조건이 붙는다. 한 행이 바뀌고 204 가 나가며 version 은 2 가 된다.
- B 의 PUT: 같은 v1 을 들고 왔지만 조건이 맞지 않는다변경된 행이 0 이므로 412 가 나간다. 412 라는 숫자만 맞추고 뒤에서 UPDATE 를 해 버리는 구현은 통과하면 안 된다.
여기서 구분할 것 비교와 갱신은 한 SQL 문장으로 묶고 변경된 행 수를 확인해야 한다. 저장 직전에 SELECT 로 버전을 확인해도 확인과 UPDATE 사이에 다른 요청이 끼어들 수 있다. 상태 코드 배정은 이 실습 API 의 설계이다.
잠깐, 예측해 보세요 요청 B 가 SELECT 로 버전이 v1 임을 확인한 직후에 요청 A 의 UPDATE 가 끝났다. B 의 UPDATE 에는 WHERE version=1 조건이 붙어 있다. B 의 UPDATE 가 돌려주는 변경 행 수는 몇이고 서버는 무엇을 응답할까?
설명 확인 · 채점 없는 자가 점검
변경 행 수는 0 이고 서버는 412 로 응답한다. 확인과 갱신 사이에 다른 요청이 끼어들 수 있으므로 조건을 쓰기 문장까지 가져가야 하고, 그 결과를 변경된 행 수로 읽는다.
어떻게 동작하나
GET → ETag: "v1"
├─ A: PUT If-Match "v1" → UPDATE WHERE version=1 → 204, version=2
└─ B: PUT If-Match "v1" → 변경 행 0 → 412
ETag는 표현의 검증자다. 이 실습에서는 id별 title이 바뀔 때 version이 정확히 증가하고 응답 표현의 다른 요소는 바뀌지 않는다는 제한 아래 강한 태그를 만든다. 실서비스에서 표현의 압축·언어·권한별 필드가 달라진다면 같은 버전 문자열을 무조건 재사용하면 안 된다.
현장에서 만나는 모습
If-Match는 강한 비교를 사용한다. 이 교육용 API는 단일 태그만 받으며 weak 태그, 와일드카드, 목록은 지원하지 않는다. 이는 HTTP 전체 문법을 구현한 것이 아니라 API에 명시한 좁은 계약이다. 없는 메모는 404, 조건 누락은 428, 지원하지 않는 조건이나 오래된 버전은 412로 정한다. 클라이언트는 412를 무한 재시도하지 않고 최신 메모를 읽어 사용자에게 병합을 요청해야 한다. 입력 오류인 빈 제목은 422로 구분한다.
본문은 이 교육용 API 의 거절을 조건 쪽 문제와 그 밖의 문제로 갈라 코드를 정했다. 이것은 HTTP 전체 문법이 아니라 API 에 명시한 좁은 계약이다.
- 조건 헤더 때문에 막힌다조건 누락은 428 이다. 지원하지 않는 조건이나 오래된 버전은 412 이다. 이 API 는 If-Match 에 단일 강한 태그만 받고 weak 태그, 와일드카드, 목록은 지원하지 않는다.
- 조건과 무관하게 막힌다없는 메모는 404 이다. 입력 오류인 빈 제목은 422 로 구분한다.
여기서 구분할 것 이 표는 HTTP 전체가 아니라 이 API 의 좁은 계약이다. 표현의 압축이나 언어나 권한별 필드가 달라진다면 같은 버전 문자열을 무조건 재사용하면 안 된다.
잠깐, 예측해 보세요 클라이언트가 If-Match 에 W/ 로 시작하는 weak 태그 하나를 담아 PUT 했다. 이 API 는 어떤 코드로 답할까?
설명 확인 · 채점 없는 자가 점검
412 이다. 본문은 이 API 가 단일 태그만 받고 weak 태그, 와일드카드, 목록은 지원하지 않으며, 지원하지 않는 조건은 412 로 정한다고 한다.
다음 실습에서 할 것
스키마 → 생성 → 조회 → 태그 → 조건 해석 → 원자적 갱신 → 결과 코드 → FastAPI 순으로 완성한다. 원래 제목이 보존되는지도 확인한다. 412라는 숫자만 맞고 뒤에서 UPDATE를 해 버리는 구현은 통과하면 안 된다. DB 연결은 작업마다 닫아 테스트 간 상태 누출을 막는다.
참고: HTTP 조건부 요청