HTTP cached static resources
서버에서 기능 추가 후 배포했지만, 사용자가 기능 추가 이전의 파일을 캐싱해둔 상태, 업데이트 기능을 사용하지 못하는 문제 발생.
원인 추적
로컬 환경에서 스프링 부트 앱이 돌고있는 포트로 직접 요청을 넣으면 WAS 가 리소스 폴더 아래의 정적 자원을 반환하는데,
이 떄 WebContentGenerator
가 동작하면서 별도의 expires
헤더가 없고, Cache-Control: max-age=0
인 경우 Cache-Control: no-store
로 응답함.
즉, 앱에 직접 정적 자원을 요청하면 브라우저가 캐싱하지 않게 됨.
그렇다면, nginx 도입 이후, 정적 파일을 웹 서버가 반환하면서 문제가 발생한 것이라고 원인을 규정.
초기 nginx 도입 후, 정적 자원 응답 헤더에 Cache-Control 지시를 하지 않았음.
이 시기에 서비스를 이용한 사용자의 브라우저는 정적 자원을 캐싱, 이후 같은 파일에 대한 요청을 서버에 검증하거나, 직접 요청하지 않고,
메모리에 캐시된 파일을 불러와서 적용.
때문에 캐시된 파일을 서버에서 업데이트 하더라도, 파일명이 같아서 브라우저는 서버에 파일을 요청하지 않고,
메모리에 캐싱된 파일을 재사용. 서버의 업데이트 파일을 받지 못하는 문제.
해당 문제의 원인을 명확히 규정하고 나니, 사용자가 브라우저 사용기록 및 데이터 삭제를 사용자가 진행하지 않는 이상 해당 파일의 업데이트 내용을 적용할 수 없음을 확인.
방안
업데이트된 파일명 변경하는 방법을 선택.
사용자 브라우저가 캐시 데이터에서 찾지 못하는 파일을 서버에 요청하게 될 것이기 때문.
본격 작업 전, 추후 동일 문제를 방지하기 위해, 서버측 자원의 변경 사항을 대해 브라우저가 알 수 있도록 nginx 정적 자원 응답 중, 업데이트 가능성이 있는 파일에 대해 “add_header Cache-Control: no-cache” 지시를 설정.
- no-cache 는 데이터를 캐싱하되, 유효성 확인을 위한 요청만을 서버로 보냄, 변경되었다면 변경된 파일을 응답 본문을 통해 받고, 변경되지 않았다면 304 Not Modified 응답으로 검증 종료.
- 브라우저는 Last-Modified, E-Tag 헤더를 통해 파일의 유효성(변경 여부)을 판단함.
- E-Tag 는 파일 내용 기반 해싱한 문자열로 반환되는데, 공백 역시 변화로 인식해 새 해시값을 만든다.
- 브라우저는 데이터 캐싱 후, E-Tag, If-Modify-Since 헤더 값을 기반으로 유효성 검증을 요청한다.
- 이것을 통해 브라우저는 캐싱을 통해 통신 비용을 아끼고, 최신화된 정적 자원을 항상 받아볼 수 있게 됨.
- 17 라인 코드가 가지는 데이터 1kB 를 312B 크기의 통신으로 검증할수 있음.
- 이것을 통해 브라우저는 캐싱을 통해 통신 비용을 아끼고, 최신화된 정적 자원을 항상 받아볼 수 있게 됨.
- 이것은 캐시 전과 비교할 때 68.8% 만큼 자원 절약. (크기가 큰 파일이라면 더 큰 폭으로 감소할 것.)
- 브라우저는 Last-Modified, E-Tag 헤더를 통해 파일의 유효성(변경 여부)을 판단함.
결과
결과적으로 사용자가 캐시된 데이터를 직접 삭제하지 않고, 서버 차원에서 업데이트 내용을 전달할 수 있었음.
플러스
HTML 파일은 WAS 에서 서빙하고 있기 때문에 no-store, 항상 최신의 파일을 서빙함.
나의 경우 한가지 더 고려할 사항은 다음과 같았음.
index.html
-> <script type="module" src="/path/to/index.js">
-> import AuthChecker from "/path/to/AuthChecker.js"
의 방식으로 호출한다면,
HTML 파일만 최신화 되기 때문에 index.js 내부 import 절에 속한 AuthChecker.js 의 이름만 변경하면 브라우저는 변경된 사실을 알아차릴 수 없음.
HTML 이 초기화(호출)하는 index.js 는 여전히 파일명이 같기 때문에, 캐싱된 데이터를 읽어 변경 이전의 import 절을 그대로 사용.
때문에 브라우저가 변경사실을 알 수 있도록, <script>
태그로 초기화하는 index.js 의 이름을 바꿔줘야 import 호출까지 최신화를 해줄 수 있음.
코드 관리 용이성을 이유로 module 타입 import 절을 사용하고 있었는데, 추가된 기능을 html 에 직접 <script>
태그로 초기화 했다면, 이런 문제는 발생하지 않았을 것을 알게 됨. 이를 통해 구조, 방식을 선택함에 따르는 장단점이 있다는 것을 몸으로 느낌. 선택에 있어 이유와 영향을 잘 알고 선택하는 것이 중요함을 잘 새겨야 겠음.
- webpack 을 이용하면 이런 문제를 보다 손쉽게, 효율적으로 해결할 수 있는 것으로 보임.
- 토스 기술 블로그를 참조하면, 배포 마다 버전을 식별하는 디렉토리 해시값, 파일 해시값을 사용해, 캐시 기간을 길게 가져가되, 파일의 최신화를 사용하고 있다고 함. 링크