AI 에이전트만을 사용하여 현대적인 분산 SaaS를 처음부터 구축하는 방법
요약
AI 에이전트 팀을 오케스트레이션하여 현대적인 분산 SaaS 인프라를 구축하는 방법론을 소개합니다. 단순한 모델 래퍼를 넘어 멀티 테넌시, 빌링, 보안 등 복잡한 인프라를 구축하기 위한 에이전트 활용 전략과 운영 모델을 다룹니다.
핵심 포인트
- 단순 모델 래퍼가 아닌 복제하기 어려운 인프라 구축에 집중
- 플래너, 구현자, 리뷰어, 검증자로 구성된 AI 에이전트 팀 활용
- 주말의 집중적인 아키텍처 설계와 평일의 검증/병합 작업 분리
- Go, Postgres, Redis, NATS를 활용한 분산 시스템 구성
2026년에 또 다른 SaaS를 시작한다는 것은 이상한 일입니다. 담론에 따르면 이 카테고리는 포화 상태이며, AI가 모든 해자(moat)를 평평하게 만들었고, 이제 구축할 가치가 있는 유일한 것은 멋진 랜딩 페이지를 갖춘 프런티어 모델(frontier model)의 래퍼(wrapper)뿐이라고 합니다. 저는 그러한 견해들을 읽었으며, 그 진단에는 대체로 동의하지만 결론에는 완전히 동의하지 않습니다. 래퍼(wrapper)야말로 가장 먼저 사라질 대상입니다. 남는 것은 그 아래에 있는 지루하고 복제하기 어려운 인프라(infrastructure)입니다. 즉, 멀티 테넌시(multi-tenancy), 빌링(billing), 비용 제어(cost control), 비밀 정보 처리(secrets handling), 재전송 시의 멱등성(idempotency) 등이며, 모델이 더 똑똑해졌다고 해서 이러한 작업들이 더 저렴해진 것은 아닙니다.
이것은 그러한 것 중 하나를 구축하는 이야기입니다. 이름은 Aulinq입니다. 이것은 멀티 테넌트(multi-tenant), 다국어 지원 AI 에이전트(AI agent) 플랫폼입니다. 텔레그램(Telegram), 왓츠앱(WhatsApp) 또는 웹 채팅에서 고객에게 답변하고, 대화를 실제로 처리할 수 있는 가장 저렴한 모델로 라우팅하며, 테넌트(tenant)에게 토큰당 1센트 미만의 아주 작은 금액을 청구하고, 모든 테넌트의 데이터와 비밀 정보를 다른 테넌트로부터 격리하여 보호합니다. 이 배관(plumbing) 위에는 세 가지 제품 트랙이 자리 잡고 있습니다. 에이전트를 고객에게 재판매하는 에이전시를 위한 화이트 라벨(white-label) 경로, 스타트업이 자신의 제품에 에이전트를 내장할 수 있는 API, 그리고 인프라에 대한 박사 학위 없이 작동하는 봇을 원하는 1인 창업자를 위한 싱글 테넌트(single-tenant) 온램프(onramp)가 있습니다. 프로덕션 시스템은 여러 개의 Go 서비스, 하나의 Postgres, 하나의 Redis, 그리고 하나의 NATS JetStream 버스로 구성됩니다. 저는 유일한 엔지니어이며, 풀타임 직업을 가지고 있습니다.
사람들이 실제로 알고 싶어 하는 것은 바로 그 마지막 부분입니다. 직장을 다니면서 어떻게 혼자서 분산 플랫폼을 출시할 수 있을까요? 솔직한 답변은 두 가지이며, 둘 다 "코딩을 빠르게 한다"는 것은 아닙니다. 첫 번째는 제가 혼자 코딩하는 것을 멈추고 AI 에이전트 팀을 오케스트레이션 (orchestrating) 하기 시작했다는 것입니다. 즉, 각각 자신만의 컨텍스트 (context)와 도구 (tools)를 가진 플래너 (planner), 구현자 (implementer), 리뷰어 (reviewer), 검증자 (verifier)로 구성된 팀입니다. 두 번째는 매일 한 시간씩 끊어서 이 일을 하려는 시도를 멈췄다는 것입니다. 평일 밤의 한 시간은 diff를 검토하거나, 계획을 승인하거나, 검증자가 실행되는 것을 지켜보기에는 충분합니다. 하지만 스택 (stack) 결정을 위해 조사하거나, 아키텍처 (architecture) 초안을 작성하거나, 드리프트 (drifted) 상태에 빠진 실패한 서비스를 복구하기에는 충분하지 않습니다. 그런 작업에는 방해받지 않는 집중 시간이 필요하며, 저에게 그 시간은 토요일, 그리고 가끔 가족들이 외출하는 일요일입니다.
따라서 방법론과 운영 모델은 서로 의존합니다. 주말 배치 (weekend batch) 작업은 사고하는 작업이 이루어지는 곳입니다. 즉, 리서치 루프 (research loops), 아키텍처 반복 (architecture iterations), 라이브러리 공백 메우기, 에이전트 규칙 설정 등이 여기서 일어납니다. 평일 밤은 작은 검증 및 병합 (verify-and-merge) 작업이 이루어지는 시간입니다. 이 둘을 섞어버리면 화요일 밤 11시에 절반만 조사된 스택 선택을 하게 되는데, 이것이 바로 이 방법론이 피하고자 설계된 전형적인 실패 모드 (failure mode)입니다.
이 글은 그 방법론을 글로 적어 내려간 것입니다. 다듬어진 버전이 아니라, 막다른 길과 롤백 (rollbacks) 과정이 그대로 남아 있는 버전입니다. 왜냐하면 바로 그런 부분들이 실제로 가르침을 주기 때문입니다. 이 글은 여러분이 이미 유능한 엔지니어라는 점을 가정합니다. 에이전트는 그것을 대체하지 않습니다. 에이전트는 여러분이 가진 판단력을 증폭시키며, 잘못된 판단력 또한 똑같이 빠르게 증폭시킵니다.
스택에 대한 질문은 "무엇이 인기 있는가"가 아니다
여러분이 AI 코딩 어시스턴트에게 "분산 SaaS를 위해 어떤 스택을 사용해야 할까요?"라고 물었을 때, AI가 가장 먼저 할 일은 가장 인기 있는 답변을 주는 것입니다. Python, FastAPI, Postgres, Redis. 만약 여러분이 "이벤트 기반 (event-driven)"이라고 말한다면 아마 Kafka를 추천할 것입니다. 이것은 튜토리얼 (tutorial) 용으로는 정답이지만, 다른 AI 에이전트들이 향후 2년 동안 확장해 나갈 시스템을 구축하는 데 있어서는 빈번하게 오답이 됩니다.
대중적인 스택이 인기 있는 이유는 가장 많은 사람에게 가독성(legible)이 좋기 때문입니다. 하지만 가장 많은 인간에게 가독성이 좋다는 것이 가장 많은 에이전트(agent)에게 가독성이 좋다는 것과 같지는 않습니다. 에이전트는 방대한 튜토리얼 생태계로부터 이득을 얻지 못합니다. 그들은 이미 그것들을 다 읽었기 때문입니다. 대신 그들은 자신의 실수를 거부하는 타입 시스템(type system), 명확하게 오류를 알리는 런타임(runtime), 그리고 잘못될 수 있는 가동 부품(moving parts)이 가장 적은 배포 아티팩트(deployment artifact)로부터 이득을 얻습니다.
저는 스택에 관한 질문을 단순한 채팅이 아닌 딥 리서치(deep research)를 통해 수행했습니다. 네다섯 번의 반복 과정을 거쳤습니다. 먼저 광범위한 조사를 수행한 다음, 살아남은 두세 가지 옵션으로 범위를 좁혔고, 그다음 각 옵션을 심층적으로 읽었으며, 마지막으로 첫 번째 모델의 편향(bias)을 잡아내기 위해 두 번째 모델과 교차 검증을 수행했습니다. 이는 이 프로젝트에서 스택뿐만 아니라 모든 중대한 결정에 대해 제가 따랐던 패턴입니다. 한 번 물어보는 것보다 느리지만, 한 번 물어보고 4개월 뒤에 다시 구축하는 것보다는 빠릅니다.
Go가 승리했습니다. AI 에이전트 도구(AI-agent-tooling) 측면에서 인기가 많아서가 아니라 — 그 분야는 Python이 지배하고 있으므로 그렇지 않습니다 — Go 컴파일러가 에이전트의 실수 유형을 새벽 3시의 스택 트레이스(stack trace) 대신 빌드 에러(build error)로 바꿔주기 때문입니다. 에이전트가 이벤트 구조체(event struct)에 필드를 추가하고 컨슈머(consumer)를 업데이트하는 것을 잊어버리면 빌드가 실패합니다. Python의 경우, 컨슈머가 조용히 None으로 역직렬화(deserializes)되어 운영 환경(production)에서 문제를 발견하게 됩니다.
이러한 트레이드오프(trade-off)는 실재하며, 저는 이를 명시하고자 합니다. 저는 Python ML 생태계에 대한 직접적인 접근권을 포기했습니다. 플랫폼의 모든 모델 호출은 프로세스 내 라이브러리(in-process library) 대신 HTTP API를 통해 이루어집니다. LLM 라우팅(LLM-routing) 플랫폼에게는 괜찮은 선택입니다. 어차피 모델들은 제공자(provider) 뒤에 존재하니까요. 하지만 로컬 추론(local inference)이나 무거운 전처리(preprocessing)를 수행하는 팀에게는 실제적인 비용이 될 것입니다. Python이 올바른 선택이었을 사례를 포함하여, Go 대 Python에 대한 전체 논쟁은 별도로 작성하겠습니다.
라이브러리가 존재하지 않는다면, 직접 작성하라
스택을 정한 후에는 모든 레이어에 대해 동일한 연구 루프를 적용합니다. 이벤트 버스(Event bus): Kafka 대신 NATS JetStream을 선택했습니다. 주제 기반 라우팅(subject-based routing)은 에이전트가 추론할 수 있는 문자열인 반면, 스키마 레지스트리(schema registry)를 사용하는 토픽 기반 라우팅(topic-based routing)은 에이전트가 실수하기 쉬운 빌드 단계이기 때문입니다. 영속성(Persistence): 도메인별 스키마를 가진 단일 Postgres를 사용합니다. 에이전트의 가독성 측면에서는 "에이전트가 전체를 읽을 수 있는 하나의 데이터베이스"가 "각기 다른 마이그레이션 도구를 가진 10개의 데이터베이스"보다 훨씬 낫기 때문입니다.
그다음은 나중에 제가 이 내용을 설명했을 때 사람들을 놀라게 했던 부분입니다. 제가 필요로 했던 라이브러리 중 몇 가지는 제가 사용하고자 하는 형태로 존재하지 않았습니다. 모든 이벤트를 엔벨로프(envelope)로 감싸고, 가공되지 않은 Publish(subject, []byte) 대신 PublishInbound를 노출하는 타입이 지정된(typed) JetStream 퍼블리셔(publisher)가 필요했습니다. 단순한 연속 실패 카운터가 아닌, 슬라이딩 실패 윈도우(sliding failure window)를 갖춘 서킷 브레이커(circuit breaker)도 필요했습니다. 도구 레이어(tools layer)에 평문(plaintext)을 유출하지 않으면서 엔벨로프 암호화(envelope encryption)를 지원하는 멀티 테넌트(multi-tenant) 비밀 저장소(secrets vault)도 필요했습니다.
다른 엔지니어들의 반응은 대개 "그냥 라이브러리 X를 사용하세요"였습니다. 때로는 그 말이 맞았습니다. 하지만 때로는 기존 라이브러리가 얇은 래퍼(wrapper)에 불과하여, 에이전트가 모든 서비스에서 동일한 보일러플레이트(boilerplate) 코드를 작성하도록 강제할 수도 있었습니다. 격차가 실재할 때, 라이브러리를 직접 작성하는 것은 가장 큰 문제가 아닙니다. 에이전트와 함께 몇 번의 주말을 보내면, 시스템의 나머지 부분이 기대하는 정확한 인터페이스를 얻게 됩니다. 이제 플랫폼은 모든 서비스가 임포트(import)하는 일련의 내부 Go 라이브러리(go-events, go-ai-providers, go-resilience, go-secrets 및 기타 수십 개)를 보유하고 있습니다. 이 라이브러리들은 공개적인 대안을 사용했을 때 에이전트가 접착 코드(glue code)를 줄이는 대신 더 많이 작성해야 했을 것이기에 존재합니다.
서킷 브레이커는 그 형태를 보여주는 좋은 예시입니다. 제가 찾은 공개 Go 서킷 브레이커 라이브러리들은 연속된 실패를 카운트하고 타이머에 따라 리셋되었습니다. 제가 원했던 것은 슬라이딩 윈도우(sliding window) 방식이었습니다. 즉, 지난 60초 동안의 실패를 카운트하고, 임계값을 넘으면 오픈(open)하고, 탐색을 위해 하프 오픈(half-open) 상태가 되며, 단 한 번의 성공 시 클로즈(close)되는 방식입니다. 이는 Go로 작성된 60줄 정도의 코드입니다:
func (cb *SlidingWindowCircuitBreaker) Call(fn func() error) error {
cb.mu.Lock()
switch cb.state {
...
에이전트가 단 한 번의 시도로 이 코드의 첫 번째 버전을 작성했습니다. 저는 이를 검토하여 가지치기 루프 (pruning loop)에서 오프 바이 원 (off by one) 오류를 발견했고, 에이전트에게 이를 수정하도록 했습니다. 핵심은 이 라이브러리가 이제 어댑터 레이어 (adapter layer) 없이 시스템의 나머지 부분이 호출하는 정확한 인터페이스를 갖게 되었으며, 이를 임포트 (import)하는 모든 서비스가 동일한 실패 시맨틱스 (failure semantics)를 갖게 되었다는 점입니다. 만약 제가 대중적인 라이브러리를 래핑 (wrapping)했다면, 각 서비스마다 서로 다른 임시 래퍼 (ad-hoc wrapper)가 생겼을 것이고, 해당 서비스를 확장하는 에이전트들은 그 각각을 모두 학습해야 했을 것입니다. 라이브러리 자체에 관한 시리즈가 계속 이어질 예정입니다. 이벤트 발행기 (events publisher), AI 제공자 인터페이스 (AI-provider interface), 비밀 저장소 (secrets vault), RAG 셀렉터 (RAG selector) 등 각각의 사례는 공개적인 옵션을 선택했을 때 에이전트용 글루 코드 (agent glue)가 줄어드는 것이 아니라 오히려 더 많아졌을 상황들입니다.
반복을 통한 아키텍처 계획
개발에 앞서, 저는 에이전트들과 함께 전체 아키텍처를 계획했습니다. 이는 대부분의 사람들이 건너뛰는 단계이지만, 가장 느린 부분처럼 느껴졌음에도 불구하고 나중에 가장 많은 시간을 절약해 준 단계였습니다.
제가 배운 핵심은 에이전트에게 당신이 가진 만큼의 초기 컨텍스트 (context)를 최대한 많이 제공해야 한다는 것입니다. 왜냐하면 에이전트의 기본 설정은 가능한 한 최소한의 일만 수행하는 것이기 때문입니다. 에이전트에게 "멀티 테넌트 (multi-tenant) AI 플랫폼을 위한 아키텍처를 설계하라"고 요청하면, "AI 서비스"라고 라벨링된 박스가 포함된 3계층 다이어그램을 받게 될 뿐입니다. 하지만 제약 조건—여기에 메시징 플랫폼이 있고, 여기에 비용 계층 라우팅 (cost-tier routing) 요구사항이 있으며, 여기에 테넌시 (tenancy) 규칙이 있고, 여기에 내가 기대하는 이벤트 흐름이 있으며, 여기에 내가 피하고 싶은 것이 있다—을 포함하여 요청하면, 실제로 구축 가능한 결과물을 얻을 수 있습니다.
나는 반복했습니다. 네 번, 다섯 번, 여섯 번의 라운드 말이죠. 계획을 받고, 읽고, 특정 결정에 대해 반대 의견을 내며 수정을 요청했습니다. 에이전트는 선택을 방어했고, 나는 그것이 실패하는 구체적인 시나리오를 제시하며 반박했고, 에이전트는 수정했습니다. 지루하고 짜증 나는 과정이었습니다. 때로는 차라리 직접 코딩을 하는 게 낫겠다는 생각도 들었습니다. 하지만 "이런 케이스는 어떨까"라는 질문이 다섯 라운드 동안 견뎌낸 계획은, 에이전트가 진행 과정에서 아키텍처를 임의로 만들어내지 않고도 구현할 수 있는 계획입니다.
이러한 반복 과정에서 내가 가장 가치 있게 여기게 된 점은, 아직 취하지 않은 방향들에 대해 생각하는 것이었습니다. 현재의 계획은 지금 구축하고 있는 것들만 다루면 되지만, 아키텍처는 다음에 구축할 수도 있는 것들을 향해 유연하게 휘어질 수 있어야 합니다. 한 가지 예로, 당시에는 테넌트(tenant)가 하나뿐이라 엄격하게 분리할 필요가 없었음에도 불구하고, 나는 에이전트 설정을 불변 템플릿(immutable template)과 테넌트별 오버라이드(per-tenant override)로 설계했습니다. 6개월 후, 두 번째 테넌트가 등장하고 화이트 라벨(white-label) 케이스가 나타났을 때, 오버라이드 모델은 이미 이를 처리할 수 있었습니다. 만약 첫 번째 단계에서 더 단순한 "에이전트당 하나의 설정" 방식으로 구축했다면, 나중에 시스템을 확장하는 에이전트는 부하가 걸린 상태에서 데이터 모델을 마이그레이션해야 했을 것이며, 이는 에이전트가 잘 수행하지 못하는 종류의 작업입니다. 아키텍처 결정 자체—Python 대신 Go를 선택한 것, 모놀리스(monolith) 대신 10개의 마이크로서비스(microservices)를 선택한 것, 서비스당 하나의 데이터베이스 대신 하나의 Postgres를 선택한 것 등—에 대해서는 나중에 각각 별도로 작성하겠습니다. 이 글은 그 과정에 관한 것이지, 정당성을 입증하기 위한 것이 아닙니다.
개발 루프 (The development loop)
계획이 안정화되면, 개발은 구체적이고 작은 작업들로 나뉜 하나의 커다란 최상위 계획이 되었습니다. "메시징 서비스를 구축하라"가 아니라, "이벤트 라이브러리에 PublishInbound 메서드를 추가하라. 여기 구조체(struct)가 있고, 여기 주제 패턴(subject pattern)이 있으니, 단위 테스트(unit test)를 작성하라"와 같은 식입니다. 그다음은 그다음 작은 작업, 그다음 또 그다음 작업으로 이어졌습니다.
순서가 중요했습니다. 먼저 라이브러리(Libraries)를 구축하며, 각 라이브러리가 제대로 작동함을 증명하는 작은 예제들을 만들고 단위 테스트(unit test)로 계약(contract)을 확정했습니다. 그다음은 핵심 서비스(core services)였으며, 두 개의 서비스가 존재하게 되면 서로를 대상으로 교차 테스트(cross-testing)를 수행했습니다. 그다음은 초기 UI였는데, 이는 또 다른 연구 루프(research loop)를 의미했습니다. 이번에는 프론트엔드 프레임워크(frontend frameworks)에 대한 연구였습니다. 왜냐하면 에이전트(agent)의 첫 번째 답변은 수많은 보일러플레이트(boilerplate)가 포함된 React였는데, 제가 원한 것은 빌드 단계(build-step)의 갈등 없이 정적(static)으로 컴파일되고 국제화(i18n)를 처리할 수 있는 가장 작은 것이었기 때문입니다.
이 지점이 역할 분리(role separation)가 존재하는 곳입니다. 첫 번째 에이전트는 코드베이스(codebase)를 읽고 작업 계획(plan)을 작성합니다. 두 번째 에이전트는 그 계획에 따라 구현(implement)합니다. 세 번째 에이전트는 차이점(diff)을 읽고 버그를 찾습니다. 네 번째 에이전트는 빌드(build)하고, 재시작하고, 테스트를 실행합니다. 그리고 시스템이 정상(healthy)임을 관찰하기 전까지는 작업이 완료되었다고 선언할 수 없습니다. 어떤 에이전트도 자신의 작업을 승인(promote)하지 않습니다. 구현자는 검토(review)하지 않고, 검토자는 검증(verify)하지 않으며, 검증자는 승인(approve)하지 않습니다.
여기서 다시 도출하지는 않겠습니다. 다음 기사에서는 각 역할이 잡아낸 구체적인 버그와 파이프라인이 과잉(overkill)이었던 단 한 번의 사례를 포함하여, 에이전트 팀에 대해 자세히 다룰 것입니다. 이 기사의 핵심은 각 작업이 단일 에이전트가 맥락(thread)을 놓치지 않고 완료할 수 있을 만큼 충분히 작아야만 루프(loop)가 작동한다는 점입니다. "결제 서비스(billing service)를 리팩터링(Refactor)하라"는 작업이 아닙니다. "결제 보류 RPC(credit-hold RPC)를 별도의 함수로 추출하고, 보류되었다가 해제된 신용이 원래 값으로 잔액을 반환하는지 확인하는 테스트를 추가하라"가 작업입니다. 에이전트가 이를 완료하면, 검증자(verifier)가 테스트 통과와 서비스 로그의 깨끗함을 확인하고, 루프는 다음으로 넘어갑니다.
명문화되고 간결하게 유지되는 에이전트 규칙
빌드 중간쯤에 에이전트들이 경로를 이탈(drifting)하고 있다는 것을 알아차렸습니다. 테넌트 ID(tenant ID)로 라우팅되어야 할 서비스가 계정 ID(account ID)로 라우팅되고 있었습니다. 마이그레이션(migration) 과정에서 권한 부여(privilege grant) 없이 컬럼을 추가하여 새로운 역할(role)이 이를 읽을 수 없게 되기도 했습니다. 모두 수정 가능한 작은 문제들이었지만, 새로운 세션을 시작하는 에이전트가 규칙을 알지 못했기 때문에 동일한 형태의 실수가 세션 전반에 걸쳐 반복되었습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기