Showing Posts From
캐시
- 01 Dec, 2025
Redis 캐시 전략으로 밤새 디버깅한 날
Redis 캐시 전략으로 밤새 디버깅한 날 그건 화요일 저녁 6시의 일이었다 일반적으로 배포는 금요일 오후 3시에 한다. 근데 이번엔 달랐다. 기획팀이 수요일 자정까지 급하게 처리해야 할 기능이 있다고 했고, 우리 팀은 당연히 전쟁 준비를 했다. 사실 나는 지난주부터 Redis 캐시 도입을 준비하고 있었다. 회사 서비스가 요즘 느리다는 컴플레인이 계속 들어오고 있었다. 사용자가 늘어나면서 데이터베이스 쿼리가 답답해 지기 시작한 거다. 데이터는 자주 바뀌지 않는데 매번 디비에서 갱신하고 있었으니까 말이다. 그래서 내가 제안했다. "Redis 캐시 레이어 추가하면 어때요?" 팀장은 "좋지, 김개발이가 해봐" 라고 했다. 요즘 팀장은 항상 그렇다. 뭔가 새로운 기술이 나오면 나한테 건넨다. 직책도 없으면서. 아무튼. 나는 지난주 내내 Redis 연결, TTL 설정, 캐시 무효화 로직을 짰다. 로컬에서는 완벽했다. DB 응답 시간이 200ms에서 20ms로 뚝 떨어졌다. 이거야말로 진짜 최적화다. 나는 자신감에 차 있었다. 화요일 저녁 6시, 배포가 시작되었다. 테스트 서버부터. 로그를 봤다. 모든 게 정상이었다. 프로덕션 배포. 여전히 정상. "좋네. 이제 집 갈까" 라고 생각했다. 아내한테 카톡했다. "늦게 먹을 거 같아" 라고. 아내는 "알겠어 조심해" 라고 했다. 근데 밤 10시. 슬랙에서 핑이 울렸다. 안 좋은 예감이 들었다. 모니터가 시커먼 밤 10시의 공포 [Alert] User ID 12345 - 잘못된 데이터 제공 반복 이 메시지가 모니터에 떴을 때, 나는 한숨을 쉬면서 생각했다. "또 시작이네." 기획팀에서 온 리포트였다. 몇몇 사용자가 캐시된 데이터가 맞지 않는다는 피드백을 주었다는 거다. 사용자 A의 정보를 조회할 때 사용자 B의 정보가 나온다는 식. 이건 내가 가장 싫어하는 케이스였다. "대체 뭐가 문제지?" 하고 한참 고민해야 하는 상황 말이다. 로컬에선 진짜 문제가 없었는데. 나는 일단 프로덕션 서버의 로그부터 뒤지기 시작했다. CloudWatch 대시보드를 열었다. 레이턴시는 확실히 내려갔다. 그건 좋은 신호였다. 그럼 캐시 정책이 작동한다는 뜻인데... 오류 로그를 보니 패턴이 보였다. 2024-01-17 22:10:45 - Cache HIT for user:12345 - result from Redis 2024-01-17 22:10:50 - user:12345 updated in DB 2024-01-17 22:11:02 - Cache HIT for user:12345 - result from Redis (STALE DATA)아. 이거다. 내가 짠 캐시 무효화 로직이 DB 업데이트를 감지하지 못하고 있었다.나는 잠깐, 내가 뭘 놨더라 하면서 코드를 다시 봤다. 캐시 무효화 로직은 이렇게 짜여 있었다: @Transactional public void updateUser(User user) { userRepository.save(user); redisTemplate.delete("user:" + user.getId()); }당연하지. 이렇게 간단할 리가 없다. 문제는 우리 시스템이 마이크로서비스 아키텍처로 되어 있다는 거였다. 사용자 정보는 User Service에서 관리하지만, 프로필 이미지 같은 건 Content Service에서 관리했다. 그리고 두 서비스는 다른 데이터베이스를 쓰고 있었다. 즉, 내 무효화 로직은 User Service의 캐시만 날리고 있었다. Content Service에서 업데이트된 데이터는 Redis에 남아있었던 거다. "아 그거요" 라고 중얼거렸다. 이제 문제를 알았으니까 고칠 수 있다. 근데 문제는... 10시가 넘었다. 새벽 3시, 커피가 식어가는 시간 나는 일단 핫픽스를 생각했다. Redis의 모든 관련 캐시를 무조건 날려버리는 방식으로. 그럼 성능이 또 떨어질 텐데, 일단 버그는 없을 거다. @Transactional public void updateUser(User user) { userRepository.save(user); // 일단 관련된 모든 캐시 다 날려버림 redisTemplate.delete("user:" + user.getId()); redisTemplate.delete("profile:" + user.getId()); redisTemplate.delete("content:" + user.getId()); // ... 기타등등 }팀 슬랙에 메시지를 띄웠다. "일단 핫픽스 할게요. 내일 제대로 리팩토링합시다." 배포는 밤 11시에 끝났다. 버그는 사라졌다. 근데 나는 계속 앉아 있었다. 뭔가 불안했다. "이게 진짜 완벽한 해결책인가?" 밤 12시, 커피를 또 마셨다. 셋째 잔. 나는 비즈니스 로직을 다시 생각했다. 지금 우리 시스템은 여러 마이크로서비스가 동일한 사용자 정보를 캐시하고 있었다. 어느 한 서비스에서 업데이트되면, 다른 서비스도 알아야 한다. 근데 어떻게? 메시지 큐? Kafka? 그건 복잡할 것 같은데. 아니면 그냥 캐시 TTL을 짧게 가져가? 그럼 매번 다 조회해야 하는데 결국 성능 개선이 없는 거잖아. 나는 다시 생각했다. "근본적인 문제가 뭐지?" 아. 여러 서비스가 같은 데이터를 각각 캐시하고 있다는 게 문제다. 그럼 캐시를 중앙화해야 한다. 모든 데이터 조회는 하나의 서비스를 거쳐서, 그 서비스가 Redis를 관리하도록. 밤 2시. 나는 구조를 다시 그렸다. 손으로 노트에다가. 종이와 펜이 제일 빠르다. User Service → [Redis Cache Manager Service] → Redis Content Service ↓ Profile Service ↓이렇게 하면 어떻게 되지? 음... 네트워크 호출이 하나 더 늘어난다. 하지만 모든 캐시가 일관성 있게 유지된다. 밤 3시, 슬랙에 긴 스레드를 남겼다.팀장님, 내일 아침에 얘기하고 싶은데, 현재 캐시 설계에 근본적인 문제가 있는 것 같습니다. 마이크로서비스 간 캐시 무효화가 제대로 안 되고 있어요. 긴급 핫픽스는 했는데, 장기적으로는 아래처럼 개선이 필요할 것 같습니다:Redis Cache Manager 서비스 신설 모든 캐시 쓰기/삭제 요청을 여기로 집중 TTL 정책 중앙화 ...그리고 나는 커피를 마셨다. 넷째 잔. (셋째가 아니라) 컴퓨터는 여전히 켜져 있었고, 모니터에는 코드가 떠 있었다. 나는 다시 생각했다. 혹시 내가 놓친 게 있나? 아. 하나 더 있었다. Cache Invalidation Pattern이었다. 우리는 지금 TTL-based invalidation을 쓰고 있었는데, 이건 보장되지 않는다. 업데이트가 되고 나서 TTL이 끝날 때까지의 그 시간 동안, 잘못된 데이터가 나간다는 뜻이다. 그럼 Event-driven invalidation은? 업데이트가 발생하면 즉시 이벤트를 발행하고, 그걸 듣는 모든 캐시 레이어가 반응한다. Kafka 같은 메시지 큐를 쓰면 돼. 근데 이건 더 복잡하다. 그리고 비용도 는다. 나는 한숨을 쉬었다. 개발이 정말 쉬워 보이기만 하지, 실제로는 이렇게 트레이드오프를 계속 해야 한다. 밤 4시, 나는 침대로 갔다. 결국 아내 옆에 누웠다. 아내는 자고 있었다. 새벽 5시, 깨어나면서 든 생각 꿈을 봤는데, 뭔지는 모르겠다. 그냥 코드를 계속 보고 있는 느낌이었다. 눈을 떴을 때 시간이 5시였다. 휴... 밤 11시에서 4시까지 거의 5시간을 일한 거다. 그런데 뭔가 좋은 느낌이 들었다. 문제를 찾았으니까. 그리고 해결 방법도 여러 개 생각했으니까. 근데 동시에 깨달았다. 이 문제는 사실 처음부터 예측할 수 있었어야 했다는 거다. 마이크로서비스 아키텍처에서 캐시를 도입할 때는 항상 무효화 전략을 먼저 생각해야 한다. 성능 개선은 나중이다. 나는 다시 한 번 생각했다. "내가 면접 준비를 해야 하나? 다른 회사에서는 이런 실수가 안 나나?" 아니다. 어디든 있다. 다만 시간에 여유가 있는 회사는 이런 문제를 천천히 해결한다. 우리처럼 급하게 배포하는 회사에서는 항상 이런 문제가 터진다. 아내가 깨어났다. "뭐 해?" "일." "밤에?" "응. 어제 배포에서 버그가 났어." 아내는 한숨을 쉬었다. "내일 출근 안 하면 되지?" "그럼 누가 고칠?" 아내는 다시 누웠다. 우리 부부는 서로의 일에 대해 깊이 있게 물어보지 않는다. 그냥 있다는 걸 알고 있을 뿐이다.아침 8시, 회의실에서의 대화 나는 2시간 반을 더 잤다. 당연히 피곤했다. 사무실 도착 시간은 오전 8시 30분. 평소보다 30분 빨랐다. 팀장에게 내가 쓴 스레드를 보여주고 싶었다. 팀장은 커피를 마시고 있었다. "팀장, 어제 그 버그 이야기 들으셨어요?" "응. 잘 고쳤네. 고마워." "네, 근데 이건 임시방편이고요. 우리 캐시 아키텍처에 근본적인 문제가..." 팀장이 손을 들었다. "알겠어. 문서화해서 보여줘. 그 다음에 우리 얘기할 거 있고." 나는 알았다. 진짜 얘기는 나중에 나온다는 뜻이다. 퀵스탠드업 미팅이 끝나고, 팀장이 나를 불렀다. "어제 밤을 새웠네." "...네. 문제를 찾아야 해서." "잘 했어. 근데 이런 식으로 일하면 안 돼. 네가 팀에서 가장 시니어인데, 뭔가 도움이 필요하면 후배들한테 물어봐. 너 혼자 다 해야 하는 건 아니야." 나는 대답을 못 했다. 팀장 말이 맞는데, 내 일의 성격상 그게 쉽지 않거든. 후배들한테 "이거 이상한데 뭐 같아?" 라고 물어보면, 결국 내가 다시 다 설명해야 한다. 그럼 시간이 더 든다. 하지만 입으로는 "알겠습니다" 라고만 했다. 이제는 무엇을 배울 것인가 지금 오후 2시다. 나는 캐시 무효화 로직을 다시 작성하고 있다. 이번엔 다르다. Event-driven 방식은 아니지만, 최소한 여러 서비스 간의 캐시 일관성을 보장하도록 했다. 간단한 로직이다: // User Service에서 업데이트 발생 @Transactional public void updateUser(User user) { userRepository.save(user); // 자신의 캐시만 날리지 말고, 다른 서비스에도 알려주기 cacheInvalidationService.invalidateUserCache(user.getId()); }// CacheInvalidationService public void invalidateUserCache(Long userId) { // 모든 관련 캐시 패턴을 한 곳에서 관리 List<String> patterns = getCachePatterns(userId); redisTemplate.delete(patterns); // 나중에는 메시지 큐로 다른 서비스에도 전파 }사실 이것도 완벽한 해결책은 아니다. 하지만 나는 이제 알았다. 완벽한 해결책은 없다. 있는 건 상황에 맞는 적절한 트레이드오프일 뿐이다. 귀가 길에, 나는 커피숍에 들어갔다. 커피 한 잔 더. 다섯째 잔. 바리스타가 내 얼굴을 봤다. 아마도 피곤해 보였을 거다. "힘든 하루였어요?" "네. 밤을 새웠어요." "또 배포 날씸?" "아뇨. 버그 때문에요." 바리스타는 웃었다. "개발자는 항상 뭔가 때문에 밤을 새네요." 나도 웃었다. "그래서 정신이 올빠르지." 집에 도착했을 때, 아내가 있었다. 보통 이 시간엔 없는데. "조금 일찍 나왔어?" "응. 넌 뭐해? 피곤해 보여." 나는 아내에게 어제 밤 일을 설명했다. Redis, 캐시 무효화, 마이크로서비스... 아내는 대부분 못 알아듣겠지만, 그냥 들어주는 것만으로도 충분했다. "고생했네. 밥 먹고 쉬자. 내가 뭐 사 올까?" "김치찌개." "또?" "점심으로 먹었어도 저녁으로 먹고 싶어." 아내는 한숨을 쉬고 나갔다. 나는 소파에 누웠다. 모니터는 꺼졌다. 슬랙도 무음. 일단 이 순간은 나의 것이다. 이제 깨달았다. 완벽한 코드를 짜려다 밤을 새는 것보다, 괜찮은 코드를 짜고 시간을 자기는 게 훨씬 나낫다는 걸. 적어도 내 정신 건강을 위해선. 내일부터는 어떻게 할까? 유사한 문제가 또 나오면? 나는 아마 또 밤을 새우겠지. 왜냐하면 내가 팀의 일인당 전문가니까. 근데 적어도 이번엔 알았다. 혼자 다 하려고 하지 말고, 처음부터 아키텍처를 제대로 생각해야 한다는 걸. 그리고 또 하나. 우리 팀은 이제 Redis 캐시 가이드 문서가 필요하다. 마이크로서비스 환경에서의 캐시 무효화 패턴, TTL 설정 기준, 모니터링 방법 등등. 나는 내일부터 그걸 작성해야 한다. 그럼 또 다른 후배가 같은 실수를 하지 않겠지. 아내가 돌아왔다. 김치찌개 냄새가 풍겼다. "먹자."결국 버그는 나를 성장시켰고, 밤샘은 나를 피로하게 했지만, 그 둘의 조합이 내를 개발자로 만들었다.