learn

로컬 앱에 서버 의존 기능(LLM)을 결합하는 설계

로컬 앱에 서버 의존 기능(LLM)을 결합하는 설계

시간표 앱처럼 기본은 로컬로 충분한데, 자연어 처리(LLM) 같은 기능 하나 때문에 서버 통신이 필요한 경우. how-to-choose-tech-stack|기술 스택 선택의 깔끔한 3분류(Local / Server / Hybrid)에 딱 안 맞는 현실적 케이스다.

핵심 원칙: 서버 의존성을 격리한다

전체를 Client-Server로 만드는 게 아니다. 로컬 앱은 로컬 앱으로 유지하고, 서버가 필요한 부분만 플러그인처럼 붙인다:

CORE(핵심 로직)는 LLM의 존재를 모른다. CORE가 받는 건 항상 구조화된 명령이다:

직접 입력: UI → "6/15 18:00 순천역 약속" → CORE
LLM 경유: UI → LLM → {"date":"6/15","time":"18:00",...} → CORE

CORE 입장에서 둘은 동일하다. LLM은 "자연어를 구조화된 데이터로 변환하는 번역기"일 뿐이다.


이 격리가 주는 세 가지 이점

1) 서버 없어도 앱이 돌아간다. LLM 서버가 꺼져도, GPU가 고장나도, API 크레딧이 떨어져도 수동 입력으로 모든 기능을 사용할 수 있다. LLM은 편의 기능이지 필수 의존성이 아니다.

2) LLM을 교체할 수 있다. 로컬 모델에서 Claude API로 바꾸고 싶으면 LLM 인터페이스만 수정하면 된다. CORE는 건드릴 필요 없다.

3) 테스트가 가능하다. CORE 테스트에 LLM 서버가 필요 없다. 구조화된 명령을 직접 넣으면 된다.


데이터 조회가 필요한 경우

"내일 중요한 일정 있어?"처럼 LLM이 기존 데이터를 참조해야 할 때:

데이터를 가져오는 건 CORE, 해석하는 건 LLM. LLM이 직접 DB를 건드리지 않는다. 이 경계가 중요하다 — LLM에게 DB 접근 권한을 주면 복잡도가 폭발한다.


Layered Architecture에 맞춰보면

LLM 관련 코드는 두 곳에만 존재한다:

  • Application: LLMService — 자연어 파싱, 명령 변환
  • Infrastructure: LLM Client — HTTP 통신 (개인 GPU든 외부 API든)

Domain, ScheduleService, DB는 LLM의 존재를 모른다.


설계 순서

"클라이언트와 서버를 동시에 설계"하는 게 아니다:

  1. 로컬 앱을 먼저 완전하게 설계한다 (LLM 없이도 모든 기능이 돌아가도록)
  2. 서버 의존 기능을 별도 층으로 얹는다

판별 기준: "이 기능이 없어도 앱이 돌아가나?" Yes면 그 기능은 격리 대상이다.

how-to-scope-requirements|요구사항 단계에서 "LLM 서버가 꺼지면?"이라는 경계 질문에 "수동으로 되어야 한다"고 답하면, 자연스럽게 LLM을 격리하는 설계가 도출된다.