같은 목록을 반복해서 내려주던 API를 ETag로 줄인 과정
문제 발생
모바일 앱이 30초마다 목록을 새로 받아갔습니다. 대부분 내용이 그대로였는데도 매번 전체 JSON이 나갔습니다.
편집 화면에는 다른 문제가 있었습니다. 두 사람이 같은 글을 열어 각자 저장하면 나중에 저장한 쪽이 앞의 수정을 조용히 덮어썼습니다.
원인 분석
두 문제 모두 "지금 내가 보고 있는 버전"을 표현할 방법이 없어서 생깁니다.
MDN은 ETag를 특정 버전의 리소스에 대한 식별자로 정의하고, 두 가지 용도를 함께 설명합니다 — 캐시를 더 효율적으로 만들어 대역폭을 아끼고(내용이 바뀌지 않았으면 서버가 전체 응답을 다시 보낼 필요가 없다), 리소스에 대한 동시 수정이 서로를 덮어쓰는 것("mid-air collision")을 막습니다.
동작은 단순합니다. 클라이언트가 캐시된 값의 ETag를 If-None-Match로 보내면, 값이 일치할 때 서버는 본문 없이 304 Not Modified로 응답합니다.
수정 쪽은 If-Match입니다 — 저장 요청에 원본의 ETag를 실어 보내고, 해시가 맞지 않으면(그 사이 문서가 수정됐다면) 서버가 412 Precondition Failed를 돌려주어 갱신 손실을 막습니다.
강한 검증자와 약한 검증자 구분도 있습니다. W/ 접두사가 붙은 약한 ETag는 만들기는 쉽지만 비교에는 덜 유용하고, 바이트 단위로 동일하지 않아도 의미상 같으면 같은 값이 될 수 있습니다.
해결 방안
- 읽기에는
If-None-Match를 지원합니다.
GET /api/posts → 200, ETag: "v1-9f2c"
GET /api/posts (If-None-Match: "v1-9f2c") → 304, 본문 없음-
ETag 값은 실제 표현에서 만듭니다. 본문 해시나
updated_at+ id 조합이 흔합니다. 응답이 사용자마다 다르면(권한에 따라 필드가 다르면) 그 차이도 값에 반영되어야 합니다. -
쓰기에는
If-Match를 요구합니다. 조건 없는PUT/PATCH는 마지막 요청이 이기는 구조입니다.
PATCH /api/posts/1
If-Match: "v1-9f2c" → 값이 다르면 412-
412를 사용자 언어로 바꿔줍니다. "다른 곳에서 이 글이 수정되었습니다. 새로고침 후 다시 저장해 주세요." 같은 안내가 없으면 사용자는 저장이 안 된 이유를 알 수 없습니다.
-
약한 검증자와 범위 요청의 관계를 알아둡니다. 문서 설명대로 약한 ETag는 바이트 범위 요청에서 캐싱을 막고, 강한 ETag는 범위 요청도 캐시될 수 있게 합니다. 큰 파일을 서빙한다면 이 차이가 실제 비용이 됩니다.
-
DB 낙관적 잠금과 짝을 맞춥니다.
If-Match검사만 하고 실제 UPDATE에 조건을 걸지 않으면 검사와 실행 사이의 창이 남습니다 —UPDATE ... WHERE id = ? AND updated_at = ?처럼 조건을 SQL에도 넣습니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.