Situation

ScreenRecording_07-28-2026 22-16-56_1.mp4

'소비학습'의 음성 추가 기능은 사용자가 말한 상호명을 CoreML kNN 모델로 카테고리 예측한 뒤, 확인 화면에 그 결과를 보여주도록 만들었습니다. 실기기에서 테스트를 하는데, "스타벅스"처럼 시드 데이터에 명확히 있는 흔한 상호명을 말해도 결과가 항상 "기타"로만 나왔습니다. (영상은 수정 完)

Task

정리하면 목표는 다음과 같았습니다.

Action

가장 먼저 의심한 건 예측 로직(predict 함수)이었습니다. (SpendLearning/Source/Data/Predictors/CategoryPredictor.swift) 증상 자체가 “예측이 틀렸다”가 아니라 “항상 폴백값(기타)만 나온다”는 것이었고, 예측 로직(predict 함수)을 보면 이 폴백값은 예측에 사용하는 모델인 MLModel? 프로퍼티가 nil 이거나 메모를 벡터로 바꾸는 함수인 vectorize(_:vocabulary:)가 실패(throw)했을 때만 반환되는 값이었습니다.

즉 모델이 잘못 예측한 게 아니라 아예 예측 자체를 시도하지 못하고 있다는 뜻이라, 이 함수 안에서 무엇이 실패하는지부터 확인해야 했습니다.

func predict(memo: String) -> (category: String, confidence: Double) {
    guard let model, let vector = try? Self.vectorize(memo, vocabulary: vocabulary) else {
        return (Self.fallbackCategory, 0)
    }
    ...
}

실기기에서 "스타벅스"를 말해보며 디버깅해보니, vocabularyvectorize 결과는 정상적으로 채워져 있었습니다. 반면 MLModel? 프로퍼티(model)는 nil이었습니다. 즉 벡터화 로직에는 문제가 없었고, 애초에 예측 로직(predict 함수)이 호출되기 전, init 시점의 모델 로드 단계에서 이미 실패하고 있었던 겁니다.


guard let bundledCompiledURL = Bundle.main.url(forResource: "CategoryClassifier", withExtension: "mlmodel") else {
    return
}

모델을 로드하는 코드는 번들에서 CategoryClassifier.mlmodel이라는 이름의 리소스를 찾는 방식이었습니다.

Bundle.main.url(forResource:withExtension:)nil을 반환한다는 뜻이었는데, Xcode 내비게이터에서도, train.py로 만들었던 CategoryClassifier.mlmodel 파일이 분명히 있는데, 번들에서 찾지 못한다는게 이상했습니다.

내비게이터에 보이는 파일 목록이 아니라, 실제로 앱에 설치되는 빌드 결과물을 직접 확인해보기로 했습니다. Xcode가 빌드한 산출물은 ~/Library/Developer/Xcode/DerivedData/ 아래, 프로젝트별 폴더 안의 Build/Products/Debug-iphonesimulator/(시뮬레이터 빌드 기준)에 실제로 생성됩니다. 이 안을 find로 뒤져봤습니다.

image.png