들어가며 링크 복사
안녕하세요. 카카오모빌리티 클라이언트 개발자 케빈, 맥스웰, 휴, 헬레나입니다.
AI가 산업 전반의 핵심 기술로 자리 잡은 지금, AI 서비스는 대부분 클라우드 기반으로 구현되어 있습니다. 하지만 실제 사용자 환경에서는 응답 지연, 네트워크 의존성, 개인정보 보호 등의 문제가 지속적으로 발생합니다. 이런 한계를 극복할 수 있는 대안으로 On-Device AI, 즉 디바이스 자체에서 AI 연산을 수행하는 방식이 주목받고 있습니다.
On-Device AI는 개인정보를 외부로 전송하지 않고도 빠른 응답이 가능하며, 서버 비용까지 절감할 수 있는 장점이 있습니다. 그러나 지금까지는 주로 제조사나 플랫폼 애플리케이션(이하 앱) 위주로만 구현되어 왔고, 외부 서비스 기업이 실제 서비스에 적용한 사례는 드물었습니다.
카카오모빌리티는 구글 클라우드와 협력하여 Gemini Nano Early Access Program(이하 EAP)에 참여했고, Android에서는 Gemini Nano를, iOS에서는 자체 ML 모델을 통해 On-Device AI를 직접 구현했습니다.
나아가 On-Device AI의 한계를 극복하기 위해 Cloud AI와 결합한 하이브리드 전략을 도입했으며, 이를 실제 서비스에 적용해 안정성과 유연성을 확보했습니다. 이 글에서는 외부 서비스 기업으로서 이 기술을 어떻게 설계하고 운영했는지, 실제 경험을 공유하고자 합니다.
📚 추천 독자
On-Device AI와 하이브리드 전략에 관심 있는 분들, 특히 모바일 클라이언트 개발자, PM, 서비스 기획자분들이 읽으시면 더욱 좋아요!
Android 개발기: Gemini Nano와의 도전 링크 복사
구글의 Gemini Nano를 활용해 Android 앱에서 On-Device AI 기능을 구현한 과정을 소개합니다.
1. 동작 원리 링크 복사
Gemini Nano는 Android OS 내의 AICore에 탑재된 경량 모델입니다. 외부 서비스 기업은 AI Edge SDK를 통해 AICore와 통신하며 모델을 다운로드하고 질의할 수 있습니다. LoRA(Low Rank Adaptation)를 활용한 파인 튜닝과 프롬프트 입력, 결과 출력을 위한 안전 필터(Safety filter)로 구성되어 있으며, 특히 개인정보 보호를 위해 강력한 필터링이 적용됩니다.
2. 한계와 극복 링크 복사
초기에 Gemini Nano1로 테스트했을 때는 메시지에서 어느 정도 의미 있는 데이터를 찾아냈으나 성공률이 높지 않았습니다. 성능 향상을 기대했던 Gemini Nano2에서는 오히려 안전 필터 강화로 인해 성공률이 0%대까지 하락해 대부분의 추출이 실패했습니다.
주된 원인은 다음과 같습니다.
- 한국어 데이터 학습량 부족
- 한국식 주소, 이름, 전화번호 체계에 대한 이해 부족
- 강화된 개인정보 필터링 정책
이를 해결하기 위해 구글 클라우드팀과 정기적으로 협업하여 모델 학습 개선을 요청하고, 한국어 처리와 주소 체계 인식을 위한 학습 데이터를 보완했습니다. EAP 참여사로서 인증을 받아 보다 효과적인 필터링 적용한 결과, 성공률을 개선할 수 있었습니다.
3. 프롬프트 최적화 링크 복사
모바일 기기에서 On-Device AI를 실행할 때는 성능 제약으로 인해 프롬프트 최적화가 필수적입니다. 초기에는 “안녕”이라는 간단한 프롬프트에도 2~3초가 소요되었고, 실제 사용 환경에서 100자가 넘는 프롬프트에 사용자의 텍스트(50~300자)를 추가하면 응답 시간이 4초에서 최대 15초까지 걸렸습니다.
이를 개선하기 위해 토큰 수를 최소화하고 불필요한 문맥을 제거하는 방식으로 프롬프트를 최적화했고, 최종적으로 평균 4~5초 수준의 응답 시간을 달성했습니다.
이후에도 지속적인 성능 개선과 정책 개선 미팅을 통해 최소한의 응답 시간과 신뢰성을 확보했지만, 서비스 레벨에서는 아직 해결해야 할 과제가 남았습니다. 이에 저희는 Cloud AI와의 상호보완적인 해결 방안(하이브리드)을 모색하게 되었습니다.
iOS 개발기: Core ML로 자체 모델 개발 링크 복사
구글의 AI 지원을 받을 수 있는 Android와 달리, iOS에서는 OS 차원에서 제공되는 AI API가 제한적이어서 Text Recognition에 특화된 On-Device AI 모델을 자체적으로 학습시켜야만 했습니다.
1. 동작 원리 링크 복사
Create ML을 통해 자체 학습한 모델을 Core ML 포맷으로 변환하여 앱에 탑재했습니다. 이 모델은 CPU, GPU, Neural Engine을 활용해 네트워크 연결 없이 온전히 디바이스 내에서 실행됩니다
2. 데이터 전처리와 튜닝 링크 복사
Word Tagger를 활용해 필요한 정보를 추출하는 방식을 도입했습니다. 학습 데이터에서 이모티콘, 특수문자 등 노이즈를 제거하고 의미 있는 샘플 데이터로 모델을 학습시켰습니다.
태그의 종류와 개수는 모델 성능에 큰 영향을 미치므로, 다양한 조합을 실험하여 최적의 태그 구성을 찾는 데 집중했습니다.
Xcode Create ML의 시각적 튜닝을 활용해 모델을 최적화했습니다. 이렇게 완성된 모델을 iOS 앱에 탑재함으로써 특정 목적에 특화된 자체 On-Device AI 기능을 구현하는 기술력을 확보했습니다.
Cloud AI + On-Device AI = 하이브리드 AI 개발기 링크 복사
초기 단계인 On-Device AI는 결괏값의 정확도와 디바이스 환경에 따른 응답 시간 차이라는 문제점이 있었습니다. 이를 해결하기 위해 Cloud AI와의 상호보완적 방안을 모색했고, On-Device AI와 Cloud AI의 하이브리드 전략을 동적 제어 방식으로 구현하여 On-Device AI의 약점을 극복할 수 있었습니다. 이 방안에 대해 공유하려고 합니다.
툴팁에 마우스오버 시
툴팁에 마우스오버 시
HTTP/2 200
{
"name": "FIND_OWNER_DEVELOPER",
"payload": {
"developer": {
"account_status": "NORMAL",
"id": 576952,
"name": "Tube",
"account_id": 2137162,
"status": "REGISTERED",
"price_plan": "BASIC",
"created_at": "2020-02-17T06:43:02Z",
"updated_at": "2020-02-17T06:43:02Z"
},
"app_id": 123456,
"developer_id": 576952,
"role": "OWNER",
"created_at": "2020-02-21T05:42:16Z",
"updated_at": "2020-02-21T05:42:16Z"
}
}
HTTP/2 200
{
"name": "FIND_OWNER_DEVELOPER",
"payload": {
"developer": {
"account_status": "NORMAL",
"id": 576952,
"name": "Tube",
"account_id": 2137162,
"status": "REGISTERED",
"price_plan": "BASIC",
"created_at": "2020-02-17T06:43:02Z",
"updated_at": "2020-02-17T06:43:02Z"
},
"app_id": 123456,
"developer_id": 576952,
"role": "OWNER",
"created_at": "2020-02-21T05:42:16Z",
"updated_at": "2020-02-21T05:42:16Z"
}
}
class MutableStack<E>(vararg items: E) { // 1
private val elements = items.toMutableList()
fun push(element: E) = elements.add(element) // 2
fun peek(): E = elements.last() // 3
fun pop(): E = elements.removeAt(elements.size - 1)
fun isEmpty() = elements.isEmpty()
fun size() = elements.size
override fun toString() = "MutableStack(${elements.joinToString()})"
}
class MutableStack<E>(vararg items: E) { // 1
private val elements = items.toMutableList()
fun push(element: E) = elements.add(element) // 2
fun peek(): E = elements.last() // 3
fun pop(): E = elements.removeAt(elements.size - 1)
fun isEmpty() = elements.isEmpty()
fun size() = elements.size
override fun toString() = "MutableStack(${elements.joinToString()})"
}
function sum(a + b) {
return a + b;
}
const test = sum(1, 2)
console.log(test);
function sum(a + b) {
return a + b;
}
const test = sum(1, 2)
console.log(test);