캐시를 무효화할 자리가 아예 없다면 (부제: 시계는 write를 안 한다)

캐시 무효화가 어렵다는 말은 다들 한다. 보통은 "무효화 코드를 어디에 넣을지 까먹는다"는 뜻으로 쓴다.

그런데 넣을 자리가 아예 없는 경우가 있다. 이 글은 그 경우 얘기다.

흔한 그림부터

공개 페이지에 남은 자리 수를 보여준다고 하자. 행사 상세 페이지 같은 것.

트래픽은 붙을 테니 캐싱하고 싶어진다. 자연스럽다. 그리고 우리는 무효화를 어디에 넣어야 하는지도 안다. 값이 바뀌는 곳에 넣으면 된다.

그럼 남은 자리 수는 언제 바뀔까? 세어보자.

  1. 누가 예약한다
  2. 환불한다
  3. 붙잡아둔 자리(hold)가 만료된다
  4. 마감 시각이 지난다

1번과 2번은 쉽다. 예약 처리하는 코드 끝에 무효화 한 줄, 환불 코드 끝에 한 줄.

3번과 4번에서 손이 멈춘다.

여기가 함정이다

3번과 4번은 write가 아니다.

hold가 만료되는 순간에 아무도 아무것도 안 한다. 관리자가 버튼을 누르지 않는다. 서버가 요청을 받지 않는다. DB에 아무 일도 안 일어난다.

그냥 시간이 흐른다. 그리고 화면에 떠 있던 숫자가 틀린 값이 된다.

무효화 코드를 넣을 호출 지점이 존재하지 않는다. 호출이 없으니까.

이게 "무효화를 까먹었다"와 결정적으로 다른 점이다. 까먹은 건 나중에 찾아서 넣으면 된다. 없는 자리는 못 넣는다.

그래서 캐시가 두 가지를 만든다

캐시를 걸면 팀이 떠안는 게 두 개가 된다.

하나, 모든 write 경로마다 무효화 호출을 기억해야 한다. 새 기능이 재고를 건드릴 때마다 하나씩 늘어난다.

, 어떤 write 경로로도 고칠 수 없는 새로운 종류의 낡음(staleness)이 생긴다. 이건 첫 번째와 달리 성실함으로 해결되지 않는다.

party-os의 구현 정책에 이 이야기가 적혀 있는데, 특이한 점은 결정만 적은 게 아니라 대안의 비용까지 적어놨다는 것이다. 정책 문서의 문장은 이렇다 — "캐시를 두면 무효화 호출 지점을 write path마다 기억해야 하고, 실제로 그 부류의 결함이 다섯 번 나왔습니다."

결정 문서에 "우리는 이래서 이걸 안 한다"까지는 흔한데, "안 했으면 이런 일이 몇 번 났을 것이다"를 숫자로 남긴 건 드물다.

"그래도 SEO 때문에" 라는 반사신경

공개 페이지 캐싱을 방어할 때 거의 항상 나오는 카드다. 캐싱 안 하면 검색 노출이 안 된다는 것.

정책은 여기에 정면으로 답한다.

SEO 이득은 서버 렌더링에서 나오는 것이지 캐싱에서 나오는 게 아니다.

요청마다 서버에서 렌더링하는 페이지는 완전히 색인된다. 크롤러 입장에서 그 페이지가 캐시에서 나왔는지 방금 만들어졌는지는 알 바가 아니다. HTML이 오면 된다.

캐시가 사주는 건 지연시간이지 크롤 가능성이 아니다.

이 구분이 중요한 이유는, 대부분의 팀이 무효화 부담을 떠안기로 결심하는 근거가 바로 저 SEO 카드이기 때문이다. 카드가 무효면 결심의 근거도 사라진다.

그럼 영원히 캐시 못 쓰나

아니다. 정책은 조건을 건다.

나중에 트래픽 때문에 캐시가 필요해지면, 먼저 안 변하는 부분과 자주 변하는 부분을 분리하라는 것. 행사 제목·설명·이미지는 거의 안 변한다. 남은 자리 수는 초 단위로 변한다. 둘을 다른 수명으로 다룰 수 있게 쪼갠 다음에 캐시를 얹으라는 얘기다.

그리고 명시적으로 금지한 게 하나 있다. 호출 지점을 하나 더 추가하는 식의 점진적 수정.

"아 여기도 무효화 넣어야 하는구나" 하고 한 줄 추가하는 것 말이다.

이게 왜 금지인지가 이 글의 핵심이다

각각의 무효화 호출은 국소적으로는 항상 옳아 보인다.

리뷰에서 "이 줄 왜 넣었어요?"라고 물으면 답이 명확하다. 여기서 값이 바뀌니까요. 맞는 말이다. 그 줄은 승인된다.

문제는 그 집합이 완전한지를 아무도 볼 수 없다는 것이다. 줄 하나하나는 검토 가능한데, "우리가 모든 자리를 다 찾았는가"는 검토할 방법이 없다. 그리고 자리 하나가 아예 존재하지 않는다는 걸 우리는 이미 알고 있다.

무효화가 어려운 이유는 코드가 어려워서가 아니다. 국소적으로 옳은 결정들이 모여서 전역적으로 틀린 상태를 만드는데, 그게 어느 시점에 틀려졌는지 아무도 짚을 수 없어서다.

이 이야기가 적용되는 범위

핵심 판별식은 하나다.

화면에 보이는 값이, 아무도 write 하지 않았는데 바뀔 수 있는가?

그렇다면 캐시는 고칠 수 없는 낡음을 만든다. 만료되는 hold, 카운트다운, 시간 지나면 리셋되는 할당량 — 전부 같은 모양이다.

반대로 값이 오직 write로만 바뀌는 페이지라면 이 글은 해당 없다. 거기엔 무효화를 넣을 진짜 호출 지점이 있고, 평범한 캐싱 트레이드오프가 그대로 적용된다.

캐시를 걸기 전에 이것만 세어보시라. "이 값이 바뀌는 경로 중에, write가 아닌 게 있나?"

하나라도 있으면, 캐시는 지연시간을 사주는 대신 영원히 못 갚는 빚을 하나 지운다. (그리고 그 빚은 보통 데모 시연 중에 청구된다. 꺄하하)