배경 단계구분도는 시군구 경계 230개(100 m 단순화)와 지표 값으로 그린다. 처음 만든 GET /regions/geojson?metric= 은
지표마다 같은 경계를 다시 실어 한 번에 1.95 MB(gzip 546 KB)를 보냈다 — 지역 지표 11개를 둘러보면 수 MB 를 받는다.
그런데 경계는 boundaryLoadJob 을 다시 돌릴 때만 바뀌고(연 1회 수준), 값은 매월 바뀐다. 바뀌는 주기가 다른 두 데이터를 한 응답에 묶고 있었다.
결정
GET /regions/boundaries?v={version}&simplify=100 — 도형만. 버전 = 경계 적재 시각(UTC) + 개수 (예: 20260924111731-230).
v 가 현재 버전과 같으면 Cache-Control: public, max-age=31536000, immutable — 브라우저가 1년 동안 다시 묻지 않는다.v 가 없거나 옛 버전이면 no-cache + ETag → 재검증하면 본문 없이 304.simplify 는 100(미리 계산한 커버리지 단순화) · 500 · 1000 만 — 임의 값마다 PostGIS 단순화와 캐시 항목이 생기지 않게.PayloadCache)에 UTF-8 바이트와 gzip(최고 압축) 바이트를 버전당 한 번 만들어 두고,
Accept-Encoding: gzip 이면 압축본을 그대로 보낸다(Content-Encoding 이 있으면 Tomcat 이 다시 압축하지 않음, Vary: Accept-Encoding).GET /regions?metric= 목록(약 5 KB)이 boundaryVersion 을 함께 준다. 화면은 목록을 먼저 받고 그 버전으로 경계 URL 을 만든다.
경계를 다시 적재하면 URL 이 바뀌므로 캐시를 무효화할 필요가 없다(cache busting). 경계와 값은 화면에서 useMemo 로 합친다.W/"…": 원본과 gzip 두 표현이 같은 내용이라 바이트 단위 동일성을 약속하지 않는다.
(처음에는 Tomcat 압축에 맡겼는데, Tomcat 은 강한 ETag 응답을 압축하지 않아 1.9 MB 가 그대로 나가는 것을 실측으로 확인했다.)Redis 대신 프로세스 안 캐시로 바꾼 이유 (2026-09-26 부하 시험) 처음에는 경계 JSON 을 Redis 조회 캐시에 두었다.
이 캐시는 값을 JSON 으로 직렬화해 저장하므로 문자열 응답이 JSON 문자열 리터럴로 한 번 더 감싸져, 요청마다 1.9 MB 를 Redis 에서 읽고
이스케이프를 풀고 다시 UTF-8 로 인코딩 · 압축했다. 옛 /regions/geojson 을 섞은 부하에서 api 의 힙 밖 메모리(네트워크 버퍼)가 급증해
1 GB 컨테이너 한도를 넘었다(VERIFICATION 47~49번). 버전 키로 불변인 값이므로 인스턴스마다 한 번 만들면 되고, Redis 를 거칠 이유가 없다.
옛 /regions/geojson(지표 값 + 경계를 매번 1.9 MB)은 화면이 쓰지 않고 요청 하나로 큰 응답을 만들 수 있는 경로라 삭제했다.
결과 지표를 바꿀 때 받는 양 546 KB → 약 5 KB. 경계는 버전당 한 번(gzip 약 530 KB), 재방문은 304. 최악 조건(50 VU 가 캐시 없이 경계 전체를 반복 수신)에서도 205 req/s · 111 MB/s · 오류 0, api 메모리 1 GB 한도 안.
대안
ST_AsMVT): 폴리곤 230개엔 과하다. 읍면동(약 3,500개)으로 내려가면 다시 검토한다.