조건부 요청은 '어떤 조건이 참일 때만 이 요청을 수행하라'고 서버에 지시하는 HTTP 기능입니다. 요청 헤더에 조건(precondition)을 담아 보내면, 서버가 그 조건을 검사해 참이면 정상 처리하고 거짓이면 특별한 상태 코드로 응답합니다. 이 기능은 크게 두 곳에서 빛을 발합니다 — 하나는 캐시 재검증(바뀌지 않았으면 다시 받지 않기), 다른 하나는 낙관적 동시성 제어(남의 수정을 덮어쓰지 않기)입니다.
검증자: ETag와 Last-Modified
조건을 판단하려면 리소스의 '버전'을 식별할 검증자(validator)가 필요합니다. ETag(Entity Tag)는 리소스 내용에서 파생된 불투명한 버전 지문으로, 큰따옴표로 감싼 문자열입니다(예: ETag: "a1b2c3"). Last-Modified는 리소스가 마지막으로 바뀐 시각입니다. 서버는 응답에 이 검증자들을 실어 보내고, 클라이언트는 그 값을 저장해 두었다가 다음 조건부 요청에 되돌려 보냅니다.
If-None-Match: 저장한ETag와 다를 때만 본문을 받음 — 캐시 재검증에 사용(GET).If-Modified-Since: 지정한 시각 이후 바뀌었을 때만 본문을 받음 —Last-Modified기반 재검증.If-Match: 내가 아는ETag와 일치할 때만 수정 수행 — 낙관적 잠금(쓰기).If-Unmodified-Since: 지정 시각 이후 바뀌지 않았을 때만 수행 — 시간 기반 잠금.If-Range: 검증자가 그대로일 때만 부분(206), 바뀌었으면 전체(200)를 받음 — 이어받기 안전장치.
강한 ETag vs 약한 ETag
ETag에는 두 종류가 있습니다. 강한(strong) ETag는 바이트 단위로 완전히 동일할 때만 같은 값을 갖습니다 — 범위 요청(range request)처럼 정확한 바이트 일치가 필요한 곳에 씁니다. 약한(weak) ETag는 앞에 W/가 붙으며(예: W/"a1b2c3"), '의미상 동등하면' 같은 값으로 취급합니다. 예를 들어 공백이나 타임스탬프만 다르고 내용은 사실상 같은 두 응답은 약한 ETag가 같을 수 있습니다. 압축본과 원본처럼 표현이 다르지만 같은 리소스인 경우에도 약한 검증이 유용합니다.
캐시 재검증: If-None-Match와 304
가장 흔한 조건부 요청은 캐시 재검증입니다. 클라이언트는 저장해 둔 ETag를 If-None-Match 헤더에 실어 '이 버전과 다를 때만 본문을 달라'고 요청합니다. 내용이 그대로면 서버는 본문 없이 304 Not Modified만 돌려주고, 클라이언트는 캐시를 재사용합니다. 내용이 바뀌었으면 서버는 새 ETag와 함께 200으로 전체 본문을 보냅니다. Last-Modified를 쓸 때는 대응 헤더가 If-Modified-Since입니다.
GET /api/doc/42 HTTP/1.1
Host: example.com
If-None-Match: "a1b2c3"
HTTP/1.1 304 Not Modified
ETag: "a1b2c3"
Cache-Control: max-age=0
낙관적 동시성: If-Match와 412
조건부 요청의 두 번째 용도는 쓰기 충돌 방지입니다. If-Match는 If-None-Match의 반대로, '내가 아는 이 버전일 때만 갱신하라'는 뜻입니다. 클라이언트가 문서를 읽을 때 받은 ETag를 수정 요청의 If-Match에 실어 보내면, 서버는 현재 버전과 비교해 일치할 때만 수정을 적용하고, 그 사이 다른 사람이 먼저 수정해 버전이 어긋났다면 412 Precondition Failed로 거절합니다.
PUT /api/doc/42 HTTP/1.1
Host: example.com
If-Match: "a1b2c3"
Content-Type: application/json
{"title":"updated"}
HTTP/1.1 412 Precondition Failed
갱신 유실(lost update) 문제
왜 이런 장치가 필요할까요? 두 사용자 A와 B가 같은 문서를 동시에 열었다고 합시다. A가 먼저 저장하고, 잠시 뒤 B가 (A의 변경을 모른 채) 자신의 옛 사본을 기준으로 저장하면, A의 수정이 흔적도 없이 사라집니다. 이것이 갱신 유실(lost update) 문제입니다. If-Match/ETag를 이용한 낙관적 잠금(optimistic locking)은 이를 우아하게 막습니다 — 대부분의 경우 충돌이 없다고 낙관하되, 실제로 버전이 어긋난 순간에만 412로 거절해 클라이언트가 최신 내용을 다시 읽고 병합하도록 유도합니다. 데이터베이스 행을 잠그는 비관적 잠금과 달리, 낙관적 잠금은 읽는 동안 자원을 붙잡지 않아 확장성과 응답성이 좋습니다.
정리하면, 조건부 요청은 불필요한 전송을 없애 성능을 높이고(304), 눈에 보이지 않는 데이터 손실을 막아 정확성을 지키는(412) 두 가지 목적을 하나의 우아한 메커니즘으로 달성합니다. REST API를 설계한다면 리소스마다 ETag를 제공하고 수정 엔드포인트에서 If-Match를 요구하는 것을 강력히 권합니다.