요약

  • HTTP 428을 통해 원 서버는 필수 조건이 없는 상태 변경 요청을 거부할 수 있습니다. 이는 조건부 쓰기를 선택적 클라이언트 주의 사항에서 서버가 강제하는 입장 정책으로 전환시킵니다.
  • 일반적인 보호 수단은 강한 엔터티 태그를 사용한If-Match입니다. 428은 조건 자체가 누락되었음을 의미하고, 412는 제공된 조건이 거짓으로 평가되었음을 의미합니다.
  • 이 상태 코드는 잠금, 병합 또는 트랜잭션을 수행하지 않습니다. 안전성은 여전히 유용한 검증자, 원자적 비교·변경, 그리고 태그를 무작정 갱신하지 않고 현재 상태를 조정하는 클라이언트에 달려 있습니다.

조용한 덮어쓰기

두 편집자가 같은 문서를 열었습니다. 둘 다 버전 7을 봅니다. 한 명은 단락을 수정하고 버전 8로 저장합니다. 다른 한 명은 여전히 버전 7을 보고 있으면서, 첫 번째 수정 사항을 전혀 포함하지 않은 편집으로 문서 전체를 대체합니다. 서버는 정당한 사용자 두 명, 유효한 페이로드 두 개, 성공한 요청 두 개를 모두 수용했습니다. 그러나 한 사람의 작업은 사라졌습니다.

이것이 가장 단순한 형태의 갱신 분실(lost-update) 문제입니다. 손상된 패킷이나 공격자도 필요하지 않습니다. 오류는 두 번째 쓰기가 대상을 지정하면서도 자신의 결정의 근거가 된 상태는 명시하지 않았기 때문에 발생합니다. "이 문서를 PUT하라"는 "현재 표현이 여전히 내가 읽은 것일 때에만 이 문서를 PUT하라"보다 약합니다.

HTTP에는 오래전부터 조건부 요청 필드가 있었습니다. 서버는 표현에 엔터티 태그를 붙일 수 있었고, 클라이언트는 그 태그를 조건으로 되돌려 보낼 수 있었습니다. 프로토콜에 없던 것은, 더 약한 무조건 형태 자체를 아예 수용하지 않겠다고 거부하는 원 서버를 위한 정확한 응답이었습니다.

2012년 4월,RFC 6585가 그것을 제공했습니다.428 Precondition Required는 원 서버가 요청에 조건을 요구함을 뜻합니다. RFC에 따르면 그 전형적인 용도는 클라이언트가 자신의 읽기 이후 다른 누군가가 변경한 상태를 덮어쓰는 일을 피하는 것입니다.

유용한 선택지에서 입장 규칙으로

조건부 요청은 흔히 캐싱을 통해 소개됩니다. 브라우저는 저장된 사본이 여전히 최신인지 묻고, 검증자가 일치하면 304를 만들어 본문 재전송을 피할 수 있습니다. 이 역사는 같은 증거의 훨씬 더 중요한 용도를 가릴 수 있습니다.

쓰기에서 검증자는 단순한 대역폭 힌트가 아닙니다. 그것은 작성자가 과거에 대해 하는 주장입니다.If-Match: "v7"을 통해 클라이언트는 현재 선택된 표현이 여전히 강한 태그"v7"을 갖고 있을 때에만 메서드를 수행하라고 원 서버에 지시합니다. 다른 쓰기가 태그를"v8"로 진전시켰다면 조건은 거짓이며 요청된 변경은 일어나서는 안 됩니다.

428 이전에는 신중한 클라이언트가 이 규율을 선택할 수 있었습니다. 428 이후에는 원 서버가 이 규율을 자원 정책으로 만들고, 무조건 요청이 왜 관문을 넘지 못했는지 설명할 수 있게 되었습니다. RFC 6585는 클라이언트에게If-Match를 시도하라고 알려주는 응답 예제까지 제공합니다.

그 예제는 보편적 법칙이 아니라 안내입니다. 어떤 연산은 존재를If-Match: *로 주장합니다. 생성 연산은 존재하지 않음을If-None-Match: *로 주장할 수 있습니다. 엔터티 태그를 쓸 수 없는 곳에서는If-Unmodified-Since가 시간 기반 조건을 표현할 수 있습니다. 원 서버는 자신의 연산이 어떤 증거를 수용하는지 밝혀야 합니다. 지속적인 혁신은 어느 하나의 헤더 철자가 아니라, 행동 이전에 조건을 요구할 원 서버의 권리입니다.

증거의 부재는 증거의 실패가 아니다

HTTP의 인접 상태 코드는 귀중한 구분을 보존합니다.

428 Precondition Required는 입장 질문에 답합니다: 이 자원 정책이 조건을 요구하는데, 요청이 수용 가능한 조건을 가져왔는가? 가져오지 않았다면 원 서버는 상태 변경 메서드를 시도하기 전에 거부합니다.

412 Precondition Failed는 평가 질문에 답합니다: 요청은 조건을 가져왔지만, 원 서버가 그것을 현재 상태와 비교한 결과 거짓이었습니다. 클라이언트는 자신이 본 과거를 명시했고, 그 과거는 더 이상 현재와 일치하지 않습니다.

이 두 응답을 하나로 뭉뚱그리면 운영 정보가 지워집니다. 428 응답이 많은 시스템은 동시성 계약을 모르거나 따르지 않는 클라이언트를 갖고 있다는 뜻입니다. 412 응답이 많은 시스템은 오래된 세션, 활발한 동시 편집, 혹은 너무 자주 바뀌는 검증자를 암시할 수 있습니다. 처방은 서로 다릅니다.

428은 409 Conflict와도 같지 않습니다. 409는 요청과 현재 자원 상태 간의 의미적 충돌을 서술할 수 있습니다. 428 정책은 원 서버가 덮어쓰기를 통해 충돌을 발견하느라 위험을 감수하기 전에 증거를 요구하는 것입니다. 그리고 WebDAV의 423과 달리 잠금을 보고하거나 만들지도 않습니다.

강한 태그는 의도적으로 엄격하다

RFC 9110의 현재 HTTP 의미론은If-Match에 강한 비교를 요구합니다. 약한 태그는 두 표현이 캐시 재사용에 충분히 동등할 때 유용합니다. 그러나 표현 데이터가 바뀔 때마다 메서드를 막으려는 작성자에게는 정확성이 부족합니다.

이 차이는 검증자 생성을 쓰기 API의 일부로 만듭니다. 보호 대상 상태가 바뀔 때 바뀌지 않는 태그는 오래된 갱신을 들여보낼 수 있습니다. 무관한 서식 잡음에도 바뀌는 태그는 무해한 작업을 거부하고 거짓 충돌의 폭풍을 만듭니다.If-Match를 요구하면서도 사용 가능한 강한 태그를 노출하지 않는 서비스는 회복 계약이 아니라 막다른 길을 만든 것입니다.

존재 조건도 같은 주의가 필요합니다.If-Match: *는 어떤 현재 표현이 존재해야 함을 뜻합니다.If-None-Match: *는 존재해서는 안 됨을 뜻하며, 동시 생성을 보호할 수 있습니다. 와일드카드는 정확한 바이트를 식별하지 않지만, 클라이언트의 가정을 명시적이고 검증 가능하게 만듭니다.

날짜는 덜 정확합니다.If-Unmodified-Since는 엔터티 태그가 없을 때 갱신 분실 문제에 쓸 수 있지만, RFC 9110은If-Match를 더 정확한 대체물로 다루고 둘이 함께 나타나면 날짜 조건을 무시합니다. 시계 동작과 조악한 타임스탬프는 날짜를 다른 등급의 증명으로 만듭니다.

비교는 커밋 바로 옆에 있어야 한다

HTTP는 조건 평가를 원 서버에서, 일반적인 요청 검사 이후이자 메서드의 행동 직전에 수행하도록 신중하게 배치합니다. 중개자는 자신이 소유하지 않은 원 서버 상태에 대해If-Match의 참·거짓을 판단해서는 안 됩니다. 답할 수 있는 곳으로 요청을 전달합니다.

이 순서는 애플리케이션 내부에서도 중요합니다. 어떤 핸들러가 현재 태그를 읽고, 조건이 충족되었다고 선언하고, 데이터베이스 트랜잭션을 해제한 뒤 나중에 쓴다고 가정해 보십시오. 경쟁하는 변경이 확인과 커밋 사이에 끼어들 수 있습니다. 외부의 HTTP 교환은 조건부로 보이지만, 구현은 이 조건이 막으려던 검사 시점/사용 시점 경쟁을 다시 만들어낸 것입니다.

안전한 경계는 원자적인 비교-및-행동입니다. 조건과 변경은 하나의 권위 있는 결정에서 만나야 합니다. 데이터베이스 버전 열, 비교 후 교환(compare-and-swap) 연산, 또는 동등한 트랜잭션이 그 경계를 제공할 수 있습니다. HTTP는 회선에서 보이는 의미를 규정할 뿐; 서비스 내부의 원자성을 만들어 줄 수는 없습니다.

보호되는 상태는 검증자의 범위와도 일치해야 합니다. 렌더링된 페이지의 태그는 여러 백엔드 레코드를 지키기에 부적합할 수 있습니다. 계정을 변경하고 외부 메시지를 발행하는 요청은 하나 이상의 상태 경계를 넘습니다. HTTP 428은 그 조각들을 하나의 트랜잭션으로 만들어 줄 수 없습니다.

거부는 조정이 아니다

412에 대한 위험한 클라이언트 대응은 새 태그를 가져와 같은 낡은 문서 전체 본문에 붙이는 것입니다. 새 조건은 통과하지만, 중간에 일어난 변경을 고려한 인간이나 병합 알고리즘은 없습니다. 클라이언트는 보이는 충돌을 승인된 덮어쓰기로 바꾼 것입니다.

올바른 회복은 보통 현재 상태를 다시 획득하고, 사용자의 보류 중인 의도와 비교하며, 병합·포기·대체 중 무엇을 할지 선택하는 것을 요구합니다. 어떤 데이터에는 필드 단위 패치나 가환 연산이 이 결정을 작게 만들어 줍니다. 다른 데이터에는 사람만이 의견 차이를 해결할 수 있습니다. 428은 이 작업의 필요성을 드러낼 뿐, 그 작업을 수행하지는 않습니다.

이 상태 코드는 모든 클라이언트가 이 보호를 받는다는 약속도 하지 않습니다. RFC 6585는 추가 코드를 선택 사항으로 만들고, 클라이언트가 갱신 분실을 막기 위해 428에 의존할 수 없다고 명시적으로 경고합니다. 자신의 갱신을 소중히 여기는 클라이언트는 원 서버가 불평할 때까지 기다리는 대신, API가 제공한다면 적절한 조건을 여전히 스스로 사용해야 합니다.

응답은 저장 가능한 정책이 되어서는 안 된다

RFC 6585는 428 응답을 캐시가 저장해서는 안 된다고 말합니다. 이 거부는 시도된 한 연산의 입장 요건을 서술한 것이지, 대상의 재사용 가능한 표현이 아닙니다. 캐시가 오래된 428을 나중에 제공하면 정책이나 상태가 바뀐 뒤의 요청을 막을 수 있고, 중개자를 원 서버 권한을 발명하는 역할에 놓이게 합니다.

대신 응답은 성공적으로 재제출하는 방법을 설명해야 합니다. 낯선 클라이언트에게 "Precondition Required"만으로는 부족합니다. 유용한 API는 현재 검증자를 노출하고, 지원하는 필드를 문서화하며, 클라이언트가 읽고 조정하고 재시도해야 하는지를 명확히 합니다.

IANA HTTP 상태 코드 레지스트리는 여전히 428을 RFC 6585 아래 기록합니다. 그 간결한 이름은 하나의 큰 설계 결정을 감춥니다. 인터넷에 필요했던 것은 "오류"의 또 다른 단어가 아니었습니다. 자신의 근거를 밝히지 못한 채 실패한 쓰기를 위한 단어였습니다.

출처와 근거의 한계

근거는 RFC 6585, RFC 9110의 현재 HTTP 의미론, 그리고 IANA 레지스트리입니다. 이들은 상태 코드의 존재, 조건부 필드의 동작, 평가 순서, 그리고 명시적 한계를 확립합니다. 그러나 현재 배포 점유율, 보편적인 라이브러리 동작, 이름이 붙은 사건, 측정된 충돌 손실, 정확히 한 번 실행을 확립하지는 않습니다. 428은 잠금, 트랜잭션, 병합 프로토콜이 아닙니다.