![[실전 DB 14] 캐시 무효화와 인증 응답 누출 함정](https://img.thenullpage.com/posts/5706/5706_1_43ac68.webp)
앞 편에서 캐시가 읽기를 근본적으로 줄인다고 했다. 그런데 캐시에는 채우는 것보다 훨씬 어려운 부분이 있다. 언제 비우느냐, 즉 무효화다. 그리고 그보다 더 위험한 함정이 하나 있다. 남에게 보여선 안 될 데이터를 실수로 공개 캐시에 담는 것이다. 이번 편은 캐시 무효화의 어려움과, 내가 실제로 겪은 데이터 누출 사고를 다룬다.
캐시는 잘 쓰면 방패지만 잘못 쓰면 사고의 근원이다. 이 편은 캐시의 밝은 면을 다 다룬 뒤에 오는, 반드시 짚어야 할 어두운 면이다.
1. 무효화가 어려운 이유
업계에 오래된 농담이 있다. 컴퓨터 과학에서 어려운 문제가 두 가지인데, 그중 하나가 캐시 무효화라는 것이다. 우스개지만 뼈가 있다. 캐시에 무언가를 담는 건 쉬운데, 그걸 언제 버려야 하는지를 정확히 아는 건 정말 어렵기 때문이다.
어려운 이유는 원본이 바뀌는 시점과 캐시가 그걸 아는 시점이 다르기 때문이다. 데이터를 고쳤는데 캐시엔 옛것이 그대로 남아 있으면, 사용자는 바뀐 걸 못 보고 옛 화면을 본다. 글을 수정했는데 목록엔 옛 제목이 남아 있는 식이다. 캐시가 원본의 변화를 모르면 이 어긋남이 생긴다.
가장 단순한 해법은 유효 시간에 맡기는 것이다. 앞 편에서 다뤘듯, 짧은 시간이 지나면 캐시가 저절로 버려지고 새로 채워진다. 몇십 초 정도의 어긋남이 문제 안 되는 목록 같은 건 이걸로 충분하다. 시간이 알아서 무효화해주니 따로 신경 쓸 게 없다.
문제는 어긋남이 문제가 되는 데이터다. 글을 수정하거나 지웠으면 그 변화가 곧바로 반영돼야 하는데, 유효 시간만 믿고 있으면 그동안 옛것이 보인다. 이런 데이터는 시간에 맡기지 말고, 원본이 바뀌는 그 순간에 캐시를 함께 버려야 한다. 여기서 무효화의 진짜 어려움이 시작된다.
그래서 무효화 전략은 데이터마다 다르게 잡는다. 어긋나도 잠깐이면 괜찮은 것은 유효 시간에 맡겨 저절로 갱신되게 두고, 즉시 반영돼야 하는 것은 바뀌는 순간 손으로 비운다. 이 둘을 구분하는 게 첫걸음이다. 모든 걸 즉시 비우려 들면 관리가 복잡해지고, 모든 걸 시간에 맡기면 중요한 변화가 늦게 반영된다.
2. 무효화를 짝으로 묶기
핵심 원칙은 원본을 바꾸는 작업과 캐시를 버리는 작업을 짝으로 묶는 것이다. 글을 수정하는 흐름 안에, 그 글이 담긴 캐시를 버리는 작업을 함께 넣는다. 그러면 글이 바뀔 때마다 관련 캐시가 반드시 함께 비워진다. 앞서 비정규화 값 갱신을 흐름에 묶었던 것과 똑같은 발상이다.
어려운 건 그 글이 어느 캐시들에 담겨 있는지 다 아는 것이다. 글 하나는 상세 화면에도 있고, 목록에도 있고, 인기 글 위젯에도 있을 수 있다. 글을 고쳤으면 이것들을 다 비워야 하는데, 하나라도 빠뜨리면 거기엔 옛것이 남는다. 한 데이터가 여러 캐시에 퍼져 있을수록 무효화가 복잡해진다.
그래서 캐시를 설계할 때 무효화까지 함께 설계해야 한다. 이 데이터가 어느 캐시들에 들어가는지, 바뀌면 무엇을 비워야 하는지를 캐시를 만들 때 같이 정해두는 것이다. 채우는 것만 생각하고 비우는 걸 나중에 붙이면, 빠뜨리는 캐시가 생긴다. 채움과 비움은 한 쌍으로 설계하는 게 원칙이다.
내 경우 관련 캐시들을 한데 묶어, 글이 속한 범위가 바뀌면 그 범위의 캐시를 통째로 비우는 식으로 관리했다. 예전 위치와 새 위치가 다르면 양쪽을 다 비워야 한다는 것도 이때 배웠다. 글을 옮기면 떠난 곳과 도착한 곳 양쪽의 목록 캐시가 다 바뀌기 때문이다. 무효화는 늘 빠뜨린 곳에서 사고가 난다.
그래도 무효화를 아무리 잘 설계해도 빠뜨리는 곳은 생긴다. 그래서 나는 무효화를 유일한 안전장치로 두지 않고, 유효 시간을 함께 걸어 둔다. 손으로 비우는 걸 놓쳐도 시간이 지나면 저절로 갱신되게 이중으로 두는 것이다. 즉시 반영은 손 비우기가 맡고, 혹시 놓친 건 유효 시간이 결국 정리한다. 둘을 겹치면 어긋남이 오래 남지 않는다.
3. 인증된 응답을 공개 캐시하면 안 된다
무효화보다 더 무서운 함정이 있다. 로그인한 사용자에게만 보여야 할 응답을, 모두가 공유하는 공개 캐시에 담는 것이다. 이러면 그 사용자의 데이터가 캐시를 통해 다른 사람에게 새어 나간다. 성능 문제가 아니라 보안 사고다.
왜 이런 일이 생기냐면, 캐시는 기본적으로 누가 요청했는지 구분하지 않기 때문이다. 같은 주소로 온 요청엔 같은 캐시를 돌려준다. 그런데 로그인 사용자용 화면은 사람마다 내용이 다르다. 내 이름, 내 알림, 관리자에게만 보이는 정보 같은 것이다. 이런 화면을 주소만 보고 캐시하면, 한 사람의 화면이 모두에게 돌아간다.
특히 위험한 건 시간 기반의 공개 캐시에 이런 응답이 섞여 드는 경우다. 응답에 오래 캐시해도 된다는 표시를 무심코 붙이면, 중간의 공유 캐시가 그걸 붙잡아 다음 사람에게 그대로 준다. 만든 사람은 성능을 위해 캐시를 켠 것뿐인데, 그 캐시가 로그인 사용자의 사적인 화면까지 붙잡아버리는 것이다.
그래서 원칙은 명확하다. 사람마다 다른 응답, 로그인 상태에 따라 달라지는 응답, 관리자용 응답은 공개 캐시에 절대 담지 않는다. 이런 응답에는 공유 캐시에 저장하지 말라는 표시를 확실히 붙인다. 공개 캐시는 누가 봐도 똑같은, 공개된 데이터에만 쓰는 것이다.
여기서 성능 캐시와 사적인 응답을 헷갈리기 쉬운 지점이 있다. 공개된 목록 같은 건 누가 봐도 똑같으니 캐시해도 되지만, 그 목록 위에 로그인한 내 이름이나 내 알림 표시가 얹히는 순간 응답은 사람마다 달라진다. 겉은 공개 화면인데 속에 사적인 조각이 섞이는 것이다. 이 섞임을 못 보면 사고가 난다.
4. 내가 겪은 누출 사고
이건 내가 실제로 겪은 사고다. 성능을 높이려고 응답에 캐시 표시를 넉넉히 붙였는데, 그 표시가 로그인 사용자용 응답에까지 적용된 게 문제였다. 어느 순간 로그인한 사용자에게 보여야 할 정보가, 로그인하지 않은 사람의 화면에 잠깐씩 나타나는 걸 발견했다. 공유 캐시가 로그인 사용자의 응답을 붙잡아 다음 사람에게 돌려준 것이다.
발견하고 나서 등에 식은땀이 났다. 성능 최적화가 곧장 데이터 누출로 이어진 것이기 때문이다. 다행히 큰 피해로 번지기 전에 잡았지만, 최적화가 보안 사고가 될 수 있다는 걸 뼈저리게 배웠다. 빠르게 하려던 것이 안전을 깬 것이다.
고친 방식은 캐시의 기준을 시간이 아니라 조건으로 바꾸는 것이었다. 무심코 오래 캐시하라는 표시에 기대는 대신, 요청이 로그인 상태인지, 사적인 정보를 담는지, 공개된 데이터만 담는지를 코드에서 명시적으로 판단했다. 그 판단을 통과한 공개 응답만 캐시하고, 나머지는 절대 캐시하지 않도록 못을 박았다.
이 사고 이후 캐시에 대한 내 태도가 바뀌었다. 캐시는 기본적으로 끄고, 안전이 확인된 것만 켜는 쪽으로 방향을 뒤집은 것이다. 예전엔 다 캐시하고 위험한 것만 빼려 했는데, 그러다 빠뜨리면 누출이 난다. 반대로 다 끄고 안전한 것만 켜면, 빠뜨려도 성능만 조금 손해일 뿐 사고는 안 난다.
사고를 겪고 나서 최적화와 안전의 관계를 다시 생각하게 됐다. 최적화는 대개 무언가를 건너뛰거나 재사용해 빠르게 하는 것인데, 그 건너뛰기가 안전 확인까지 건너뛰면 사고가 된다. 그래서 빠르게 만들 때마다 무엇을 건너뛰고 있는지, 그중 건너뛰면 안 되는 게 있는지 되물어야 한다. 속도를 얻으려다 안전을 잃지 않는지 보는 것이다.
5. 캐시 대상을 좁히는 원칙
그래서 내가 세운 원칙은 캐시 대상을 좁게 잡는 것이다. 캐시는 세 조건을 모두 만족할 때만 켠다. 로그인하지 않은 요청이고, 공개된 데이터이고, 사람마다 다르지 않은 응답일 때다. 이 셋 중 하나라도 아니면 캐시하지 않는다. 안전한 것만 골라 캐시하는 것이다.
이 좁히기가 성능을 크게 해치지도 않는다. 봇 트래픽과 비로그인 방문의 대부분은 공개된 목록과 글을 보는 것이라, 정작 캐시가 필요한 곳은 다 이 안전한 범위에 든다. 읽기 폭증의 주범인 봇은 로그인하지 않으니, 비로그인 공개 응답만 캐시해도 봇 부담은 충분히 흡수된다. 안전과 성능을 둘 다 얻는 것이다.
결국 캐시의 교훈은 힘이 클수록 조심하라는 것이다. 캐시는 읽기를 0에 수렴시키는 강력한 도구지만, 그 힘이 잘못 향하면 데이터를 누출한다. 그래서 무엇을 캐시할지 좁게 정하고, 언제 비울지 짝으로 묶고, 사적인 것은 절대 담지 않는다. 켜기 전에 안전을 먼저 확인하는 습관이 캐시를 방패로 유지한다.
이렇게 대상을 좁히면 판단도 단순해진다. 캐시를 걸까 말까 고민될 때, 이게 세 조건을 다 만족하나만 보면 된다. 하나라도 걸리면 안 거는 쪽으로 기운다. 애매하면 안 하는 게 사적인 데이터를 다룰 때의 안전한 기본값이다. 성능은 나중에 조건을 확인하고 더 켜면 되지만, 한번 새어 나간 데이터는 되돌릴 수 없기 때문이다.
정리하면 캐시 무효화는 원본이 바뀌는 순간 관련 캐시를 짝으로 비워야 하는 어려운 일이고, 그보다 위험한 건 인증된 응답을 공개 캐시에 담아 데이터가 새는 것이니, 캐시는 비로그인 공개 응답으로 대상을 좁혀 안전한 것만 켠다. 다음 편에서는 데이터를 직접 건드릴 때 지켜야 할 원칙들을 다룬다.