AI 비서가 시간을 착각하기 시작했다: 세션 캐싱 최적화가 삼킨 '현재 시각'
항상 켜져 있는 AI 비서에게 "지금 몇 시야?" 물었더니 실제보다 두 시간 반 전을 "지금"이라고 답했습니다. 시계가 고장 난 게 아니었습니다. 응답 속도를 높이려고 넣은 세션 캐싱 최적화가, 매번 새로 계산해야 할 '현재 시각'까지 통째로 캐시에 얼려버린 것이 원인이었습니다. 이 글은 "변하지 않는 것"과 "매번 변하는 것"을 한 덩어리로 다뤘을 때 생기는 함정에 대한 기록입니다.
1. 발단: 대화가 길어질수록 시계가 느려진다
이 AI 비서는 대화 세션을 오래 유지합니다. 매번 처음부터 다시 시작하지 않고 이전 맥락을 이어받는데, 그래야 응답이 빠르고 비용도 줄기 때문입니다. 그런데 어느 날, 새벽에 나눈 대화에서 비서가 "벌써 새벽 3시네요" 같은 말을 했습니다. 실제 시각은 5시 47분. 2시간 46분이나 밀린 값을 태연하게 "현재"라고 말하고 있었습니다.
더 이상했던 건, 세션이 오래될수록 이 오차가 커진다는 점이었습니다. 세션을 새로 시작하면 시각이 정확했다가, 대화가 길어질수록 점점 과거에 머물렀습니다. 시각이 세션 시작 시점에 멈춰 있는 것처럼 보였습니다.
2. 배경: 왜 세션을 유지하는가
AI 에이전트에게 매 질문마다 "당신은 누구고, 사용자는 누구고, 쓸 수 있는 도구는 무엇이고, 지금 몇 시입니다"라는 안내문(시스템 프롬프트)을 통째로 다시 보내는 건 비쌉니다. 길고, 매번 똑같고, 토큰을 잡아먹으니까요. 그래서 "이어하기(resume)" 최적화를 넣었습니다. 세션을 살려두고, 두 번째 질문부터는 바뀐 부분만 보내는 방식입니다.
구현은 간단했습니다. 이어하기 호출에서는 시스템 프롬프트 전체를 생략(비워둠)하는 것. 정체성도 도구 목록도 이미 세션 안에 살아 있으니 다시 안 보내도 됩니다. 비용과 속도가 확실히 좋아졌습니다.
3. 원인: 안 변하는 것과 변하는 것을 한 덩어리로 묶었다
문제는 그 "시스템 프롬프트 한 덩어리" 안에 성격이 전혀 다른 두 종류가 섞여 있었다는 겁니다.
- 매번 안 바뀌는 것: 비서의 정체성, 사용자 정보, 사용 가능한 도구 목록. 이건 세션 내내 그대로라 다시 안 보내도 됩니다.
- 매번 바뀌는 것: 현재 시각. 이건 매 질문마다 새로 계산해야 하는 시변(時變) 데이터입니다.
둘이 같은 문자열 안에 나란히 들어 있었습니다. 그래서 "시스템 프롬프트를 통째로 생략한다"는 최적화가, 정체성만 아끼려다 현재 시각까지 같이 버린 겁니다. 이어하기 세션에서 두 번째 질문부터 새 시각이 전달되지 않으니, 비서는 세션이 처음 시작될 때 박혀 있던 낡은 시각을 계속 "지금"으로 참조했습니다. 세션이 오래갈수록 그 시각과 실제의 간극이 벌어진 것이고요.
이게 단순히 "말실수" 수준이 아니었던 이유가 있습니다. 이 비서는 시각에 따라 판단이 갈리는 기능들(예: 지금이 업무 시간인지, 시장이 열려 있는지)을 갖고 있었고, 그 판단들이 전부 오염된 시각을 근거로 삼고 있었습니다. 잘못된 시각은 잘못된 결정으로 번질 수 있었습니다.
4. 해결: 시변 데이터에 전용 채널을 준다
해법은 최적화를 되돌리는 게 아니라, 덩어리를 쪼개는 것이었습니다.
- 시각 블록 분리: 시스템 프롬프트에서 시각 계산 부분만 별도 함수로 떼어냈습니다.
- 전용 채널로 매번 전달: 이어하기 호출에서 시스템 프롬프트 전체를 생략하되, 그 자리에 시각 블록만 매 턴 새로 계산해 넣었습니다. 정체성·도구 목록은 여전히 생략되니 비용 절감은 그대로 유지됩니다. 오직 시각만 매 턴 신선하게 흐릅니다.
격리된 로직만 골라 테스트한 뒤 배포했고, 이후 시각 오차는 사라졌습니다. 최적화의 이득은 지키면서 시변 데이터의 신선함만 되찾은 것입니다.
5. 교훈
- 캐싱하기 전에 "이게 변하는 데이터인가"를 물어라. 최적화의 본질은 "안 변하는 걸 다시 안 하기"입니다. 그 안에 변하는 게 섞여 있으면, 최적화는 그 변화까지 얼려버립니다.
- 불변 데이터와 시변 데이터를 처음부터 다른 채널로 설계하라. "정체성 + 현재 시각"을 한 문자열에 합치는 순간, 둘 중 하나를 건드리는 모든 최적화가 나머지 하나를 오염시킬 위험을 안습니다. 성격이 다르면 그릇도 나눠야 합니다.
- 천천히 커지는 오차가 가장 늦게 들킨다. 시각이 처음부터 완전히 틀렸다면 바로 알아챘을 겁니다. 세션 초반엔 맞고 시간이 갈수록 조금씩 밀리니, "가끔 이상하네" 하고 넘어가기 쉬웠습니다. 서서히 어긋나는 버그일수록 재현 조건(세션이 오래된 상태)을 정확히 짚어야 잡힙니다.
- 사소해 보이는 시각이 결정에 스며든다. "몇 시인지"가 틀리면 그냥 말실수로 끝나지 않고, 시간에 의존하는 모든 판단이 함께 틀립니다. 시변 데이터의 정확성은 그 데이터를 쓰는 하위 로직 전체의 정확성입니다.
FAQ
Q. 그냥 매번 시스템 프롬프트를 전부 다시 보내면 안 됐나요?
그러면 이 버그는 안 생기지만, 세션 유지로 얻은 속도·비용 이득을 통째로 버리게 됩니다. 긴 정체성·도구 목록을 매 턴 재전송하는 비용이 상당했기 때문에 최적화를 넣은 것이고, 올바른 해법은 최적화를 포기하는 게 아니라 시변 데이터만 예외로 빼내는 것이었습니다.
Q. 어떻게 근본 원인까지 확신했나요?
실제 대화 기록을 직접 열어 재현했습니다. "언제 시작된 세션에서, 몇 시에, 무슨 시각을 말했는지"를 대조하니 오차가 정확히 세션 시작 시점부터의 경과 시간과 일치했습니다. 증상을 눈으로 재현하고 숫자로 맞춰봐야 "아마 이거겠지"가 "이거다"가 됩니다.
Q. 비슷한 함정을 다른 곳에서도 조심하려면?
캐시하거나 델타 전송하는 모든 묶음에서 "이 안에 시간·위치·환율·잔액처럼 매번 바뀌는 값이 섞여 있나"를 점검하세요. 있으면 그 값만 캐시 밖으로 빼내 별도 경로로 흘려야 합니다. 정적 데이터와 동적 데이터의 분리는 캐싱 설계의 첫 단추입니다.


