2480ms를 815ms로
숫자 얘기를 먼저 하자.
Pepper Core에서 페퍼가 대답하기까지 걸리는 시간. 처음엔 2480ms였다. 지금은 815ms다. 67% 줄었다.
근데 이 편의 진짜 주제는 숫자가 아니다.
병목은 LLM이 아니었다
느리다고 느꼈을 때, 당연히 LLM 호출이 느린 거라고 생각했다. 그래서 모델을 바꿔보고, 프롬프트를 줄여보고.
실측해봤더니 달랐다.
DB INSERT: 241ms (로그 기록 시작) LLM 호출: 481ms (실제 AI 생성) DB UPDATE: 741ms (로그 기록 완료) ───────────────── 총합: 1463ms
LLM이 481ms인데, DB 작업이 982ms다. 로그 쓰는 시간이 AI 생각하는 시간의 두 배.
원인은 구조였다. INSERT 기다리고, LLM 돌리고, UPDATE 기다리고 — 전부 순서대로 줄 서 있었다.
병렬 + fire-and-forget
// Before: DB가 LLM 앞뒤를 블로킹 const log = await db.insert(...) // 241ms 기다림 const result = await llm.call() // 481ms 기다림 await db.update(log.id, ...) // 741ms 기다림 // 총: 1463ms // After: 병렬 + fire-and-forget const insertPromise = db.insert(...) // await 없음 — LLM과 동시 시작 const result = await llm.call() // 481ms (이게 실제 병목) insertPromise.then(({ data }) => { db.update(data.id, ...).catch(() => null) // 응답 후 백그라운드 }) // 총: ~481ms
INSERT는 LLM과 동시에 시작한다.
await 없이 그냥 던진다. UPDATE는 응답을 이미 보낸 다음 백그라운드에서 처리한다. 로그가 조금 늦게 완성돼도 사용자한테는 상관없다.
로깅은 critical path가 아니다. 근데 critical path를 블로킹하고 있었다.
근데 그것만으로는 부족했다
815ms가 됐다. 근데 여전히 느리게 느껴졌다.
0.8초가 길게 느껴지는 이유가 있다. 아무 피드백이 없기 때문이다. 전송 버튼을 눌렀는데 화면이 그대로면, 0.3초도 긴 것 같다.
그래서 두 가지를 추가했다.
하나, 전송 즉시 내 메시지를 화면에 올린다. 서버 확인을 기다리지 않는다. 전송 중에는 시계 아이콘, 완료되면 체크로 바뀐다.
둘, 페퍼가 생각하는 동안
... 타이핑 인디케이터를 먼저 띄운다. 실제 응답이 오기 전에 "페퍼가 입력 중"이 보인다.
// 응답 생성 전에 typing row를 먼저 DB에 삽입 const typingRow = await db.from('chat_messages').insert({ is_typing: true, content: null, // ... }) // 이후 LLM 호출, 완료되면 같은 row를 UPDATE
모바일에서 Realtime으로 이 row의 UPDATE를 받으면
...이 실제 메시지로 교체된다.
실제 속도는 0.8초 그대로다. 근데 체감이 달라진다. 뭔가 일어나고 있다는 걸 알면 기다릴 수 있다.
빠른 것보다 빠르게 느껴지는 게 때로는 더 중요하다.
지금 페퍼의 진짜 병목은 LLM 호출 시간이다. 이건 지금 어쩔 수 없다. 근데 그 시간을 기다리는 동안 사용자가 뭘 보는지는 내가 제어할 수 있다.