Tripick트립픽 블로그

트립픽 개발 기록 · 2/2

혼자 시작한다는 것 — 불안을 코드로 바꾸는 법 (비용 킬스위치 설계)

1인 개발 회고 시리즈 2장. '혼자라서 무섭다'는 감정을 어떻게 시스템으로 옮겼는지 — 자고 있는 사이 LLM 비용이 폭주하는 시나리오를 막는 3단계 예산 킬스위치를, 재배포 없이 DB 플래그로 끄는 구조까지 코드로 정리합니다.

12분 읽기

이 글은 전자책 「혼자 만든 트립픽」 2장을 기술 블로그 톤으로 다시 풀어 쓴 글입니다. 책은 회고·내러티브 중심, 이 시리즈는 의사결정의 기술적 근거를 더 깊이 다룹니다.

1장에서 트립픽이 푸는 문제를 4-변수 동시 최적화로 정의했어요. 이번 장의 질문은 그 다음입니다. 이걸 정말 혼자 풀 수 있을까? 혼자 푼다는 건 기술적으로 무엇을 의미할까?

결론부터 말하면, "혼자"라는 조건은 기능 명세서 어디에도 안 적혀 있지만 트립픽의 모든 아키텍처 결정을 조용히 지배한 가장 강력한 제약이었어요.

"할 수 있는 것"이 아니라 "혼자 감당 가능한 것"

큰 팀이라면 기술 선택의 기준이 "더 좋은 것"입니다. 1인은 다릅니다. 기준이 "내가 혼자, 끝까지, 자면서 운영할 수 있는가" 로 바뀝니다.

이 한 문장이 내린 결정들을 표로 정리하면 이래요.

선택지일반적 정답1인의 선택이유
아키텍처마이크로서비스모놀리식 Next.js배포·디버깅 지점이 하나
인프라직접 띄운 서버관리형(AWS Amplify)패치·모니터링을 외주
인증자체 회원 시스템소셜 로그인 3종비밀번호 분실·유출 CS 제거
DB분산·샤딩 고려단일 MySQL운영·백업·복제가 단순

각 선택의 기술적 디테일은 5~8장에서 따로 다루겠지만, 관통하는 원칙은 하나예요. "기능"이 아니라 "혼자 운영 가능성"이 기준이었다.

1인 개발의 진짜 비용은 코드를 짜는 시간이 아니라 운영하며 잠 못 드는 시간입니다. 그래서 나는 처음부터 "나중의 나" 를 가장 중요한 사용자로 두고 설계했어요.

가장 무서운 건 자는 동안 나가는 돈

혼자라서 가장 무서운 지점은 명확했어요. 돈이 자동으로 나가는 곳.

트립픽은 코스 설명문을 생성할 때 LLM(Claude Haiku)을 호출합니다. 여기엔 구조적 위험이 있어요.

  • 누군가 악의로 코스 생성을 반복 호출하면?
  • 버그로 무한 루프가 돌아 API를 두들기면?
  • 비회원 한도를 우회하는 방법을 누가 찾으면?

큰 회사라면 누군가 대시보드를 보다가 막습니다. 하지만 나는 자고 있을 거예요. 아침에 일어나 수십만 원짜리 청구서를 보는 시나리오 — 이게 1인 개발자의 진짜 악몽입니다.

그래서 이 두려움을 기능으로 바꿨어요. 막연한 걱정을 코드로 옮기면, 적어도 그 부분은 잠을 자도 됩니다.

3단계 예산 킬스위치

핵심 장치는 일일 비용 누적을 추적하다가 임계값을 넘으면 LLM을 통째로 꺼버리는 킬스위치예요. 책에는 이렇게 한 줄로 요약돼 있어요.

경고  ₩10,000 → 이메일 알림
긴급  ₩30,000 → 2차 알림
차단  ₩50,000 → LLM 즉시 OFF

블로그에서는 이걸 실제로 어떻게 구현했는지 풀어볼게요. 설계의 핵심은 세 가지였습니다.

(1) 호출 전에 먼저 예산을 확인한다

비용은 쓰고 나서 집계하면 늦어요. 이미 나간 돈은 못 막으니까. 그래서 모든 LLM 호출은 사전 예산 체크를 통과해야만 실행되도록 했어요.

// LLM 호출 직전에 항상 통과해야 하는 게이트
async function assertBudgetAvailable(): Promise<void> {
  const today = todayKST();                       // 자정 기준은 KST 고정
  const budget = await db.aiDailyBudget.findUnique({
    where: { date: today },
  });
 
  const spent = budget?.spentKrw ?? 0;
 
  // 차단선을 넘었으면 호출 자체를 막는다 (요청은 폴백 응답으로)
  if (spent >= LIMIT_BLOCK_KRW) {
    throw new BudgetExceededError(spent);
  }
}

BudgetExceededError가 던져지면 코스 생성은 멈추지 않아요. LLM 설명문만 빠지고, 알고리즘이 만든 코스 자체는 그대로 나갑니다. 이게 다음 핵심으로 이어져요.

(2) LLM은 '있으면 좋은 것'이지 '없으면 안 되는 것'이 아니다

이 구분이 비용 방어의 진짜 토대였어요. 트립픽에서 코스 순서와 동선은 알고리즘이 계산합니다(8장에서 자세히). LLM은 그 위에 자연어 설명을 입히는 역할일 뿐이에요.

async function buildCourse(input: CourseInput): Promise<Course> {
  const stops = optimizeRoute(input);             // 순수 알고리즘 — 비용 0원
 
  let description: string;
  try {
    await assertBudgetAvailable();
    description = await llm.describe(stops);       // 비용 발생 지점
  } catch (e) {
    // 예산 초과·LLM 장애 시 미리 만든 템플릿 설명으로 폴백
    description = renderTemplateDescription(stops);
  }
 
  return { stops, description };
}

LLM이 죽어도, 예산이 바닥나도, 사용자는 코스를 받습니다. 설명문이 조금 덜 매끄러울 뿐이에요. 핵심 가치(동선)와 비용 발생 지점(설명문)을 분리해 둔 덕분에, 킬스위치가 발동해도 서비스는 안 죽습니다.

비용 방어의 첫 단계는 모니터링이 아니라 구조예요. 돈 드는 부분이 없어도 제품이 돌아가게 만들어두면, 최악의 경우에도 "기능 저하"지 "장애"가 아닙니다.

(3) 끄는 스위치는 재배포가 필요 없어야 한다

여기가 가장 중요해요. 비용이 폭주하는 그 순간에 코드를 고쳐서 배포할 여유는 없습니다. 자고 있을 테니까요. 그래서 차단 상태를 코드가 아니라 데이터로 뒀어요.

// SystemConfig: 런타임에 동작을 바꾸는 DB 기반 플래그
async function isLlmEnabled(): Promise<boolean> {
  const cfg = await db.systemConfig.findUnique({
    where: { key: "llm.enabled" },
  });
  return cfg?.value !== "false";
}

차단선을 넘으면 이 플래그를 false로 쓰고, 호출 게이트가 그걸 읽어요. 복구도 마찬가지로 DB 값 하나만 바꾸면 됩니다. 재배포 0번, 다운타임 0초. 알림 메일의 링크를 누르면 관리자 페이지에서 토글 한 번으로 끄고 켤 수 있게 만들었어요.

// 누적 비용을 갱신하면서 임계값마다 부수효과를 트리거
async function recordCost(krw: number): Promise<void> {
  const today = todayKST();
  const budget = await db.aiDailyBudget.upsert({
    where: { date: today },
    create: { date: today, spentKrw: krw },
    update: { spentKrw: { increment: krw } },
  });
 
  const spent = budget.spentKrw;
  if (spent >= LIMIT_BLOCK_KRW) {
    await setConfig("llm.enabled", "false");       // ← 재배포 없이 OFF
    await notify("🔴 LLM 차단됨", spent);
  } else if (spent >= LIMIT_URGENT_KRW) {
    await notify("🟠 비용 긴급", spent);
  } else if (spent >= LIMIT_WARN_KRW) {
    await notify("🟡 비용 경고", spent);
  }
}

이 구조를 처음 코드에 박아 넣던 날, 비로소 조금 마음 편히 잘 수 있었어요. 두려움은 없애는 게 아니라 시스템에게 맡기는 것이었습니다.

왜 메모리가 아니라 DB였나

작은 디테일 하나가 이 설계의 숨은 전제예요. 누적 비용과 차단 플래그를 프로세스 메모리가 아니라 DB에 둔 이유.

AWS Amplify는 Lambda 런타임 위에서 돌아요. Lambda는 요청이 뜸하면 컨테이너가 사라지고, 다음 요청에 새로 뜹니다(콜드 스타트). 즉 메모리에 저장한 누적 비용은 언제든 0으로 초기화될 수 있어요. 만약 비용 카운터를 메모리에 뒀다면, 콜드 스타트마다 "오늘 쓴 돈 0원"으로 리셋돼 킬스위치가 영영 발동 안 했을 겁니다.

[메모리 캐시]   짧은 TTL만, 사라져도 무방한 것 (예: 장소 목록 일시 캐시)
[DB 저장]       사라지면 안 되는 상태 (예: 일일 누적 비용, 차단 플래그)

이 "오래 살아야 할 상태는 DB로"라는 구분은 이후 캐시 전략 전체의 뼈대가 됩니다(7·10장 예정). 인프라의 제약(Lambda의 휘발성)이 거꾸로 설계 원칙을 강제한 사례예요.

혼자지만, 혼자가 아니게

오해는 없었으면 해요. 모든 걸 맨몸으로 한 건 아니에요. 검토자가 없는 빈자리는 Claude에게 코드리뷰를 맡겨 메웠고(9장 예정), 막히면 문서로, 끊임없이 설명하며 답을 찾았습니다.

"1인 개발"은 조력자가 0명이라는 뜻이 아니라, 최종 책임자가 1명이라는 뜻이었어요. 그래서 더더욱, 그 1명이 잠든 사이에도 돌아가는 안전장치가 필요했던 거예요.

다음 장 미리보기

다음 장(3장 "시장을 보는 눈")에서는 1장에서 잠깐 미뤄둔 질문 — "나만 불편한 거 아닐까?" 라는 의심을 어떻게 다뤘는지, 그리고 1인 개발자가 거대한 시장 조사 없이 "만들 가치가 있는가" 를 판단한 기준을 다룹니다.

이번 장의 킬스위치가 "혼자라서 무섭다" 를 푼 장치였다면, 다음 장은 "혼자라서 막막하다" 를 푸는 이야기예요.


💭 그때의 나에게: 너는 "혼자라서 빠르다"에 취해 있다가 "혼자라서 무섭다"에 자주 무너졌지. 둘 다 맞아. 중요한 건 무서움을 의지로 버티려 하지 말고 장치로 바꾸는 거였어. 잠을 지켜주는 코드가 가장 좋은 코드야.

트립픽이 어떤 서비스인지 직접 보기

이 시리즈에서 만들고 있는 서비스 — 날씨·이동거리 기반 제주 코스 자동 최적화. 비회원도 3회 무료로 체험할 수 있습니다.