긴급도를 저장하지 않기로 했습니다 (매초 UPDATE 치는 대신)

할 일 관리 앱을 만들면서 아이젠하워 매트릭스를 넣었습니다. 아시죠, 가로축 긴급도 세로축 중요도 놓고 네 칸으로 나누는 그거.
중요도는 쉬웠습니다. 사용자가 정하는 값이니까 그냥 저장하면 돼요.
긴급도에서 막혔습니다. 긴급도는 마감이 다가올수록 올라가야 하거든요. 오늘 저녁 마감인 일과 다음 달 마감인 일이 같은 칸에 있으면 안 되니까요.
그래서 처음 든 생각이 이거였습니다. "그럼 긴급도를 어떻게 저장하지?"
이 질문 자체가 잘못됐다는 걸 아는 데 좀 걸렸습니다.
저장하려고 했더니 나오는 선택지들
긴급도를 컬럼으로 들고 있으려면 누군가는 그 값을 갱신해야 합니다. 방법을 세 개 떠올렸는데, 전부 마음에 안 들었어요.
1. 매초 전부 UPDATE 친다
가장 정직하고 가장 미친 방법입니다. 할 일이 200개면 초당 200번 씁니다. 앱을 안 켜놨을 때도 시간은 흐르는데 그럼 백그라운드에서 계속 돌려야 하고요. 배터리가 저를 미워할 겁니다.
2. 스냅샷을 찍어둔다
하루 한 번쯤 계산해서 박아둡니다. 쓰기는 줄어드는데, 대신 연속적인 움직임이 사라집니다. 마감 3시간 전이나 20시간 전이나 "오늘 마감"으로 똑같이 보여요. 제가 원했던 건 축이 실시간으로 미끄러지는 건데, 계단으로 뚝뚝 끊깁니다.
3. 알림으로 캐시를 무효화한다
"마감 1시간 전에 알림 쏘면서 그때 긴급도 갱신하자." 그럴듯한데, 이제 정확성이 알림 시스템의 신뢰성에 묶입니다. 알림 하나 놓치면 그 할 일의 긴급도는 옛날 값으로 남아 있어요. 조용히 틀립니다. 제일 무서운 종류의 버그죠.
세 개를 나란히 놓고 보다가 깨달았습니다. 셋 다 같은 전제를 깔고 있었어요. "긴급도는 어딘가에 저장돼 있어야 한다."
근데 왜요?
안 저장하면 됩니다
긴급도는 이미 저장된 값들로부터 계산이 되는 값입니다.
urgency = f(마감까지 남은 시간)
마감은 저장돼 있습니다. 지금 시각은 시스템이 알려줍니다. 그럼 긴급도는 저장할 게 아니라 읽을 때마다 그 자리에서 만들면 됩니다.
mobLevel = importance(저장됨) × urgency(파생됨)
이렇게 바꾸고 나니 아까 그 세 문제가 전부 사라졌습니다. 문제를 푼 게 아니라 문제가 성립하지 않게 된 거예요.
- 매초 UPDATE? 쓸 게 없습니다.
- 스냅샷이 낡음? 스냅샷이 없습니다.
- 캐시 무효화? 캐시가 없습니다.
앱을 일주일 꺼놨다 켜도 화면에는 정확한 긴급도가 뜹니다. 백그라운드에서 아무것도 안 돌았는데도요. 시간이 흐른 것만으로 값이 달라져 있습니다. (이 부분이 좀 마법 같아서 처음엔 진짜 맞나 몇 번 확인했습니다.)
이 방식의 성질 네 가지
정리하면 이렇습니다.
- 시간 가변 — 마감이 다가오면 긴급도가 계속 올라갑니다. 앱이 안 떠 있어도요.
- 파생 —
@Model에 절대 저장하지 않습니다. 뷰가 읽을 때마다 새로 계산합니다. - 결정적 — 같은 입력(마감, 현재 시각, 달력)이면 항상 같은 값입니다. 어디서 계산하든 같아요.
- 살아 움직임 — 명시적인 상태 변경 없이 매트릭스 축이 실시간으로 미끄러집니다.
세 번째가 은근히 중요합니다. 결정적이라는 건 이 값에 대해서는 "지금 값이 맞나?"를 물을 일이 없다는 뜻이거든요. 어느 화면에서 언제 읽든 같은 입력이면 같은 답이 나옵니다. 저장된 값이 아니니까 어긋날 사본 자체가 없습니다.
그래서 언제 이렇게 하면 되나
일반화하면 이렇게 물으면 됩니다.
이 값은 사실인가, 아니면 이미 있는 사실들의 함수인가?
사실이면 저장하세요. 사용자가 정한 중요도는 사실입니다. 어디서 유도할 수 없어요.
함수면 저장하지 마세요. 저장하는 순간 원본과 어긋날 수 있는 사본을 하나 만든 겁니다. 그리고 그 사본을 원본과 맞춰주는 코드(크론, 알림 훅, 무효화 로직)를 평생 관리하게 됩니다. 그 코드가 하는 일은... 애초에 사본을 안 만들었으면 필요 없었을 일이에요.
특히 시간이 입력으로 들어가는 값은 거의 항상 후자입니다. 시간은 계속 변하는데 저장된 값은 안 변하니까요. 둘을 맞추려는 시도가 곧 위에서 본 세 가지 나쁜 선택지입니다.
솔직한 단서
이 글은 설계 이야기지 최적화 이야기가 아닙니다. 저는 재계산 비용을 실측하지 않았고, 캐시 방식과 벤치마크로 비교해보지도 않았습니다. 할 일 목록 크기에서 뺄셈 한 번이 문제될 리 없다고 판단하고 넘어갔어요.
그러니까 "읽을 때 계산이 항상 빠르다"고 읽지 마세요. 계산이 무겁거나 목록이 아주 크면 이야기가 달라집니다. 제가 드리고 싶은 말은 성능이 아니라 이겁니다 — 저장 안 하면 동기화할 일이 없다.
혹시 지금 "이 값을 언제 갱신하지?"로 고민 중이시라면, 한 발 물러서서 "이거 갱신해야 하는 값이 맞나?"부터 물어보세요. 갱신 타이밍 문제의 상당수는 애초에 저장하지 않기로 하면 질문 자체가 없어집니다. 저는 그걸 세 개의 나쁜 선택지를 다 그려본 다음에야 알았습니다. 헤헤.