트립픽 개발 기록 · 2/2
혼자 시작한다는 것 — 불안을 코드로 바꾸는 법 (비용 킬스위치 설계)
1인 개발 회고 시리즈 2장. '혼자라서 무섭다'는 감정을 어떻게 시스템으로 옮겼는지 — 자고 있는 사이 LLM 비용이 폭주하는 시나리오를 막는 3단계 예산 킬스위치를, 재배포 없이 DB 플래그로 끄는 구조까지 코드로 정리합니다.
이 글은 전자책 「혼자 만든 트립픽」 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인 개발자가 거대한 시장 조사 없이 "만들 가치가 있는가" 를 판단한 기준을 다룹니다.
이번 장의 킬스위치가 "혼자라서 무섭다" 를 푼 장치였다면, 다음 장은 "혼자라서 막막하다" 를 푸는 이야기예요.
💭 그때의 나에게: 너는 "혼자라서 빠르다"에 취해 있다가 "혼자라서 무섭다"에 자주 무너졌지. 둘 다 맞아. 중요한 건 무서움을 의지로 버티려 하지 말고 장치로 바꾸는 거였어. 잠을 지켜주는 코드가 가장 좋은 코드야.