본문으로 건너뛰기
개발 머꼬
개발 노트API 설계
hohyeon.dev24

같은 목록을 반복해서 내려주던 API를 ETag로 줄인 과정

  • #API 설계
  • #Engineering Note
  • #Performance

문제 발생

모바일 앱이 30초마다 목록을 새로 받아갔습니다. 대부분 내용이 그대로였는데도 매번 전체 JSON이 나갔습니다.

편집 화면에는 다른 문제가 있었습니다. 두 사람이 같은 글을 열어 각자 저장하면 나중에 저장한 쪽이 앞의 수정을 조용히 덮어썼습니다.

원인 분석

두 문제 모두 "지금 내가 보고 있는 버전"을 표현할 방법이 없어서 생깁니다.

MDN은 ETag특정 버전의 리소스에 대한 식별자로 정의하고, 두 가지 용도를 함께 설명합니다 — 캐시를 더 효율적으로 만들어 대역폭을 아끼고(내용이 바뀌지 않았으면 서버가 전체 응답을 다시 보낼 필요가 없다), 리소스에 대한 동시 수정이 서로를 덮어쓰는 것("mid-air collision")을 막습니다.

동작은 단순합니다. 클라이언트가 캐시된 값의 ETag를 If-None-Match로 보내면, 값이 일치할 때 서버는 본문 없이 304 Not Modified로 응답합니다.

수정 쪽은 If-Match입니다 — 저장 요청에 원본의 ETag를 실어 보내고, 해시가 맞지 않으면(그 사이 문서가 수정됐다면) 서버가 412 Precondition Failed를 돌려주어 갱신 손실을 막습니다.

강한 검증자와 약한 검증자 구분도 있습니다. W/ 접두사가 붙은 약한 ETag는 만들기는 쉽지만 비교에는 덜 유용하고, 바이트 단위로 동일하지 않아도 의미상 같으면 같은 값이 될 수 있습니다.

해결 방안

  1. 읽기에는 If-None-Match를 지원합니다.
GET /api/posts    →   200, ETag: "v1-9f2c"
GET /api/posts    (If-None-Match: "v1-9f2c")   →   304, 본문 없음
  1. ETag 값은 실제 표현에서 만듭니다. 본문 해시나 updated_at + id 조합이 흔합니다. 응답이 사용자마다 다르면(권한에 따라 필드가 다르면) 그 차이도 값에 반영되어야 합니다.

  2. 쓰기에는 If-Match를 요구합니다. 조건 없는 PUT/PATCH는 마지막 요청이 이기는 구조입니다.

PATCH /api/posts/1
If-Match: "v1-9f2c"      →  값이 다르면 412
  1. 412를 사용자 언어로 바꿔줍니다. "다른 곳에서 이 글이 수정되었습니다. 새로고침 후 다시 저장해 주세요." 같은 안내가 없으면 사용자는 저장이 안 된 이유를 알 수 없습니다.

  2. 약한 검증자와 범위 요청의 관계를 알아둡니다. 문서 설명대로 약한 ETag는 바이트 범위 요청에서 캐싱을 막고, 강한 ETag는 범위 요청도 캐시될 수 있게 합니다. 큰 파일을 서빙한다면 이 차이가 실제 비용이 됩니다.

  3. DB 낙관적 잠금과 짝을 맞춥니다. If-Match 검사만 하고 실제 UPDATE에 조건을 걸지 않으면 검사와 실행 사이의 창이 남습니다 — UPDATE ... WHERE id = ? AND updated_at = ?처럼 조건을 SQL에도 넣습니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.