클로드미터는 어떻게 세션 고갈 시점을 예측할까

@inup· 7 min read

onboarding

이전 글에서 언급했듯, 사용자가 Claude 사용량을 확인할 때 가장 필요로 하는 정보는 "현재 몇 퍼센트를 썼는가"가 아니라 "이 속도로 작업하면 언제 끊기는가"일 것이다. 이를 위해 앱 내부적으로 사용 패턴을 분석하고 미래 시점을 추정하는 엔진이 필요했다. 이번 글에서는 UsageEstimator 클래스에 구현된 선형 회귀(Linear Regression) 알고리즘과 데이터 처리 로직을 다룬다.

데이터 수집과 관리

예측을 하려면 먼저 데이터가 쌓여야 한다. 하지만 무작정 모든 데이터를 쌓으면 메모리 낭비가 심하고, 너무 오래된 데이터는 현재의 작업 속도를 반영하지 못한다. 따라서 UsageEstimator는 데이터를 수집할 때 다음과 같은 원칙을 적용했다.

  1. 세션 리셋 감지: Claude API에서 제공받을 수 있는 사용량(utilization) 수치가 이전 측정값보다 낮아졌다면, 이는 Claude의 세션이 초기화되었음을 의미한다. 이때는 과거 기록을 모두 비워야 잘못된 예측을 막을 수 있다.
  2. 데이터 유효 기간: 최근 24시간 이내의 데이터만 유지한다. 또한, 최대 10분 간격으로 10개의 데이터 포인트만 반영하므로, 실제 회귀에 반영되는 데이터는 더욱 적다.
  3. 지역적 경향을 우선하여 반영: 전체 기록 중 가장 최근 10개의 데이터 포인트만 사용하여 예측을 수행한다. 이는 전체 평균보다는 '지금 당장의 작업 강도'를 반영하기 위함이다.
func addDataAndEstimate(item: UsageItem) -> Date? {
    let now = Date()
    let usage = item.usedPercentage
    
    // 세션 리셋 감지: 사용량이 줄어들면 기록 초기화
    if let last = history[item.type]?.last, usage < last.usage {
        history[item.type] = []
    }
    
    // 데이터 저장 및 24시간 지난 데이터 삭제
    history[item.type]?.append((date: now, usage: usage))
    history[item.type] = history[item.type]?.filter { $0.date > oneDayAgo }
    
    // 최근 10개 포인트만 추출하여 계산
    let recentPoints = Array(history[item.type]?.suffix(10) ?? [])
    return calculateLinearRegression(history: recentPoints)
}

선형 회귀 알고리즘 적용

사용량 그래프는 불규칙해 보이지만, 짧은 시간 단위로 보면 일정한 기울기를 가진다. 이 기울기를 구하기 위해 최소자승법(Least Squares Method)을 이용한 단순 선형 회귀를 사용했다.

목표는 시간(xx)과 사용량(yy)의 관계를 y=mx+by = mx + b (일차함수) 꼴로 나타내는 것이다. 여기서 mm은 기울기(사용 증가 속도), bb는 절편이다.

1. 데이터 정규화

계산을 단순화하기 위해 첫 번째 데이터 포인트의 시간을 0으로 기준 잡고, 이후 시간들은 timeIntervalSince를 이용해 경과 시간(초 단위)으로 변환한다.

2. 기울기와 절편 계산

수집된 점들의 합(X,Y\sum X, \sum Y)과 제곱의 합(X2\sum X^2), 곱의 합(XY\sum XY)을 구하여 기울기와 절편을 도출한다.

private func calculateLinearRegression(history: [(date: Date, usage: Double)]) -> Date? {
    // ... 변수 초기화 ...
    
    for point in history {
        let x = point.date.timeIntervalSince(baseDate) // X축: 시간
        let y = point.usage                        // Y축: 사용량
        sumX += x; sumY += y
        sumXY += (x * y); sumXX += (x * x)
    }
    
    // 분모 계산 (0으로 나누기 방지)
    let denominator = (n * sumXX) - (sumX * sumX)
    guard denominator != 0 else { return nil }
    
    // 기울기(slope) 계산
    let slope = ((n * sumXY) - (sumX * sumY)) / denominator
    
    // 기울기가 0이거나 음수면(사용량이 줄거나 그대로면) 고갈 시점을 예측할 수 없음
    guard slope > 0 else { return nil }
    
    // 절편(intercept) 계산
    let intercept = (sumY - (slope * sumX)) / n
    
    // ... 예측 로직 ...
}

3. 목표 시점 역산

우리가 알고 싶은 것은 yy1.0(100%)1.0(100\%)이 되는 시점의 xx 값이다. 따라서 방정식을 xx에 대해 정리하면 다음과 같다.

1.0=mx+b1.0 = mx + b x=1.0bmx = \frac{1.0 - b}{m}

코드로 구현하면 아래와 같다.

// 사용량이 1.0이 되는 시점(Target X) 계산
let targetX = (1.0 - intercept) / slope

// 기준 시간에 경과 시간을 더해 최종 예상 시간 반환
return baseDate.addingTimeInterval(targetX)

예외 처리와 한계

이 알고리즘은 사용자가 꾸준히 작업을 하고 있다는 가정 하에 작동한다. 따라서 다음과 같은 예외 상황을 고려해야 했다.

  • 데이터 부족: 점이 하나뿐이라면 선을 그을 수 없으므로 예측을 수행하지 않는다.
  • 기울기 \le 0: 사용자가 작업을 멈추거나 사용량이 줄어드는 경우, 고갈 시점은 무한대이거나 예측 불가능하므로 nil을 반환한다. (이 때에는 '현재 세션이 만료되는 시간'을 단순 출력한다.)

이러한 로직을 통해 UsageEstimator는 사용자에게 단순한 사용율이 아닌, "몇 시간 뒤에 한도에 도달함"이라는 태스크 관리를 위해 사용 가능한 유효한 수치를 제공할 수 있게 되었다. 다음 글에서는 완성된 앱을 패키징하고 배포하는 과정에 대해 다루겠다.

@inup
언제나 감사합니다 👨‍💻