#운영 #프로덕트 #트렌드
AI 에이전트라고 하지만, 사실 이건 어플리케이션 잘 만드는 것과 다름없다.

슬랙 @멘션 하나로 PR·빌드까지
누군가 Slack에 씁니다. 
@코드몽 위 문서대로 구현하고 PR 올린 다음 빌드 좀” 
에이전트는 스레드를 훑어 노션에서 기획 문서 페이지를 읽고, 피그마 링크에서 디자인된 화면을 찾고, 워크트리를 만들어 코드를 수정해 PR을 날린 다음, iOS와 Android 빌드를 동시에 트리거하고, 완료되면 QR 코드를 스레드에 올립니다. 
사용자 입장에서는 멘션 한 번이 전부입니다.

단순한 멘션 한 마디

단순한 멘션 한 마디가 실제 빌드로 이어지기까지, 그 과정에는 프롬프트 한 장만으로는 설명할 수 없는 복잡한 어플리케이션 레이어가 있습니다. 스레드를 세션으로 묶고, 다양한 외부 도구들 사이에 흩어진 맥락을 모으고, 사용자의 요구사항을 검증 가능한 실행 단계로 분리해, Agent로 하여금 안전하고 정확하게 작업을 수행하게 만드는 — 눈에 보이지 않는 구조입니다. 이번 글에서는 그 설계 결정들을 정리합니다.

1. Slack thread를 session으로

LLM은 기본적으로 매 요청이 독립적입니다. 같은 스레드에서 두 번째로 멘션해도 앞서 무슨 대화가 있었는지 기억하지 못합니다. 코드몽은 이 문제를 Slack의 thread_ts(스레드 타임스탬프)를 세션 키로 쓰는 것으로 풀었습니다.

스레드에서 처음 멘션이 오면 새 세션 ID를 만들어 매핑하고, 이후 같은 스레드에서 오는 요청은 같은 세션으로 이어갑니다. 세션에는 단순한 대화 이력 외에도 Notion에서 어떤 페이지를 보고 있었는지, 마지막으로 읽은 메시지가 어디까지인지 같은 부가 상태도 함께 저장됩니다. 30일간 활동이 없으면 자동 만료됩니다.

이 상태를 별도 DB 없이 JSON 파일에 저장했습니다. 파일 기반이라도 TTL 자동 정리와 Effect Schema 기반 검증이 있으니 운영에 충분했고, 필요가 생기면 그때 DB로 넘어가면 된다고 판단했습니다. (Claude Code나 Codex가 실제로 세션 정보들을 json으로 저장하고 있다는 점에서 착안했습니다.)

2. 요청을 받는 두 가지 경로

코드몽에 들어오는 모든 요청을 LLM에 보낼 필요는 없었습니다. /changelog처럼 결과가 정해진 요청까지 에이전트를 깨우면 느리고 토큰만 낭비됩니다. 더 위험한 경우도 있었습니다. 모델이 자연어를 자의적으로 해석해서 의도치 않은 작업을 시작하는 것을 막아야 하는 기능들이 있었습니다.

그래서 요청을 두 경로로 나눴습니다.

커맨드로 만드는 기준은 두 가지였습니다. 
첫째, LLM 프로세싱이 아예 필요 없는 것. 
둘째, 실수로 실행되면 안 되는 것. 
이 두 기준에 해당하지 않는 요청들 — 코드 탐색, PR 생성, Notion 검색, Figma 조회 — 은 별도 커맨드 없이 자연어로 처리합니다. Slack 스레드에서 마치 사람이 들어간 것처럼 맥락을 이해하고 작업하게 두는 것이 목표였습니다.

3. LLM은 intent를 만들고, side effect는 코드가 맡는다

에이전트가 직접 빌드를 트리거하거나 PR을 만들게 하는 방법도 있었습니다. 하지만 코드몽이 동작하는 환경은 Slack입니다. CLI와 달리 사용자가 항상 화면 앞에 있지 않습니다. 에이전트가 "빌드 돌릴까요?"라고 물었을 때, 답이 5분 뒤에 올 수도 있고 30분 뒤에 올 수도 있습니다. 에이전트 실행을 멈추고 사용자 입력을 기다리는 것은 이 환경에 맞지 않았습니다.

그래서 모델은 **의도(intent)**만 텍스트로 표현하고, 실제 실행은 핸들러 코드가 맡는 구조를 택했습니다. 모델이 응답에 [BUILD_CONFIRM:ios:main:staging] 같은 마커를 남기면, 핸들러가 이를 파싱해서 Slack에 승인 버튼을 띄웁니다. 모델은 이미 실행을 마친 상태이고, 사용자가 버튼을 누르면 그때 빌드가 시작됩니다.


이 마커 기반 설계에서 중요한 것은 intent 종류에 따라 피드백 루프를 다르게 설계했다는 점입니다.

즉시 실행 가능한 것은 같은 루프 안에서 닫습니다. Notion API 호출이 대표적입니다. 모델이 [NOTION_API_REQUEST]를 내면 핸들러가 바로 API를 호출하고, 결과를 붙여서 같은 에이전트를 다시 돌립니다. 앱이 즉시 해석하고 실행할 수 있으니 굳이 루프를 끊을 이유가 없습니다.

비동기 승인이 필요한 것은 의도적으로 one-way입니다. 빌드 트리거나 스케줄 예약이 여기에 해당합니다. 결과는 Slack 스레드에 메시지로 남고, 다음에 사용자가 코드몽을 다시 멘션하면 스레드 히스토리를 통해 자연스럽게 재주입됩니다.

복잡한 워크플로우는 별도 상태 머신으로 뺐습니다. 코드 변경 요청이 여기에 해당합니다. 운영하면서 단순한 픽셀 수정 외에도 "새 폰트를 도입하면서 기존 폰트에 맞춰 억지로 만들어진 코드를 전부 찾아서 정리해 달라"는 식의 복잡한 요청이 들어왔습니다. 이런 작업은 실행 전에 플래닝이 있어야 퀄리티가 나왔습니다(Claude Code와 Codex를 일상적으로 쓰면서 체감한 것입니다). 그래서 변경 요청은 clarify → planning → awaiting_confirm → executing이라는 상태 머신을 거치도록 했습니다. 에이전트가 멈추는 것이 아니라, 상태 머신이 다음 단계를 기억하고 있다가 사용자 입력이 오면 새 에이전트를 실행합니다.

4. context assembly가 실제 제품이다

코드몽을 운영하면서 알게 된 가장 중요한 것은, 답변 품질을 바꾸는 건 프롬프트 문구가 아니라 맥락 조합(context assembly)이라는 사실이었습니다.

가장 기본은 Slack 스레드 전체를 구조화된 transcript로 만드는 것이었습니다. 사용자의 마지막 한 줄만 보는 게 아니라, 스레드의 블록, 첨부 파일, 사용자 이름, 이미지 레이블까지 정리해서 질문 뒤에 붙입니다. 이것만으로도 모델이 "지금 무슨 얘기 중인지"를 파악하는 정확도가 크게 올라갔습니다.

다음으로 중요한 것은 이 맥락을 delta로 바꾼 것입니다. 첫 턴에는 전체 스레드를 주지만, 이후에는 세션의 커서를 기준으로 새로 추가된 메시지만 넣습니다. delta를 도입하기 전에는 두 가지 실제 문제가 있었습니다. 매 턴마다 동일한 텍스트가 에이전트 안에서 계속 쌓여 컨텍스트 윈도우가 꽉 차는 것, 그리고 프롬프트 캐싱의 prefix matching이 깨져서 캐시 히트가 안 되고 토큰 소비량이 급증하는 것. 에이전트를 운영 수준으로 만든다면 이 delta 전환은 필수적인 부분이었습니다.

여기에 이미지, 파일, Figma 디자인 같은 실제 artifact를 추가하고, Notion처럼 도메인별 상태(어떤 페이지를 보고 있는지, 어떤 데이터베이스를 탐색 중인지)까지 세션에 저장해서 다음 턴에 재주입하면, "이 페이지", "아까 그 데이터베이스" 같은 모호한 표현도 정확하게 해소됩니다.

5. prompt도 infra처럼 다뤘다

세션을 이어가려면 이전 대화를 resume해야 합니다. 그런데 그 사이에 시스템 프롬프트가 바뀌었다면? 설정 파일이 달라졌다면? 이전 세션의 가정과 현재 프롬프트가 어긋나면서 답변이 이상해지는 문제가 실제로 발생했습니다.

그래서 코드몽은 프롬프트를 단순 문자열이 아니라 세션 호환성 계약(compatibility contract)으로 다룹니다. 시스템 프롬프트의 안정적인 부분(stable prefix)을 분리해 해시를 구하고, 세션을 resume하기 전에 이전 세션에 저장된 해시와 비교합니다. 다르면 그 세션은 안전하지 않다고 판단하고, 과감하게 fresh run으로 다시 시작합니다. 설정 소스(어떤 CLAUDE.md 파일들이 적용되는지)가 달라진 경우에도 마찬가지입니다.

6. 외부 도구를 연결하는 방식

도구 연결이 없으면 코드몽은 결국 "코드를 잘 읽는 챗봇"에 머뭅니다. 그런데 실제 팀의 질문은 코드만으로 닫히지 않았습니다. 디자인 변경은 Figma에, 문서와 운영 데이터는 Notion에, 코드 리뷰와 변경 반영은 GitHub에, 빌드 실행은 GitHub Actions에, 대화 맥락 자체는 Slack 스레드에 있었습니다. 업무의 source of truth가 코드베이스 바깥에도 퍼져 있었기 때문에, 에이전트를 제품으로 만들려면 이 도구들을 연결해서 코드 바깥의 상태를 읽고, 필요한 side effect까지 이어줄 수 있어야 했습니다.

실제로 디자이너가 Figma 링크를 스레드에 던지면 코드몽이 컴포넌트와 스타일을 읽어 구현 맥락으로 붙이고, Notion 페이지에 대한 질문이 오면 API를 호출해 내용을 조회하고, 빌드 요청이 오면 GitHub Actions를 트리거하고 모니터까지 붙입니다. "언젠가 쓰려고 연결한 도구들"이 아니라, 실제 운영 질문과 요청이 흘러가는 surface였습니다.

이 도구들을 연결할 때 모든 것을 같은 프로토콜로 감싸지는 않았습니다. 도구의 성격에 따라 방식을 다르게 택하되, 코드몽이 제어권을 잃지 않는 것이 원칙이었습니다. Slack은 이벤트 기반이라 SDK로 직접 붙이고, GitHub는 gh CLI와 앱 레이어 헬퍼로 감쌌습니다. Figma는 링크에서 컨텍스트를 뽑아오는 용도라 HTTP fetch로 충분했고, Notion은 공식 SDK를 서버사이드에 직접 붙이되 앞서 설명한 마커 기반 intent/execution 분리를 적용했습니다.

외부 도구에 대한 맥락이 다양하게 포함된 요청사항을 코드몽에게 던지는 스레드

여기서 기본 전제는 오케스트레이션을 맡는 모델이 충분히 똑똑하다는 것이었습니다. 모든 걸 하드코딩된 워크플로우로 쪼개기보다, 가능한 도구와 맥락을 넓게 주고 프롬프트로 soft guide를 거는 쪽을 기본값으로 택했습니다. 코드 탐색, cross-repo 이해, 어떤 도구를 먼저 써야 할지 같은 판단은 모델을 꽤 많이 신뢰한 것입니다.

다만 완전히 맡기지는 않았습니다. side effect가 걸리는 순간에는 앱이 hard rails를 칩니다. 코드 변경은 허용 도구를 줄이고 worktree 안에 경로를 가두고, 외부 API 호출이나 빌드 같은 비가역적 작업은 정확한 포맷을 강제합니다. 평소에는 smart model + soft guidance를 믿고, 위험한 경계에서만 앱이 제약을 건다 — hard constraint를 없앤 게 아니라, 어디에 둘지를 선택한 설계입니다.

7. 작업에 맞는 모델을 골라 쓴다

코드몽의 핵심 오케스트레이션은 Claude Agent SDK 위에서 시작했고, 처음부터 역할별로 모델을 나눴습니다. Opus가 전체 흐름을 조율하고, Haiku가 코드 탐색을 빠르게 처리하고, Sonnet이 git 작업처럼 중간 정밀도의 작업을 맡는 식이었습니다. 중요한 것은 특정 모델 이름보다도, 각 역할이 독립된 컨텍스트 윈도우를 갖고 자기 일만 하게 한 구조였습니다.

메인 에이전트는 사용자 의도, 스레드 맥락, 최종 판단을 붙잡고 있어야 합니다. 그런데 파일 목록 수십 개, grep 결과 수백 줄, 긴 파일 본문까지 같은 컨텍스트에 쌓이면 금방 산만해집니다. 서브에이전트가 이런 저수준 탐색을 자기 컨텍스트 안에서 소모하고, 메인에는 요약만 올리는 구조가 오케스트레이터의 사고 공간을 보호합니다. 어떤 서브에이전트가 파일 크기 제한에 걸리더라도 그 영향이 메인 세션 전체로 번지지 않습니다.

이 원칙은 나중에 PR 리뷰에서 더 강하게 드러났습니다. 일반 PR 리뷰를 조금 더 고도화한 Magi 리뷰는 리스크, 아키텍처, 스타일, 판정 역할을 분리해 병렬로 돌립니다 (그리고 내부적으로는 codex cli로 gpt-5.4를 호출합니다). 구현체는 일부 분화됐지만 핵심 설계는 같았습니다 — 메인 오케스트레이터는 최종 판단과 synthesis를 맡고, noisy한 탐색과 역할별 분석은 각 에이전트가 자기 컨텍스트 안에서 소화하게 한 것입니다.

8. 물리적 멀티레포, 에이전트에겐 모노레포

팀은 모바일, 백엔드, 웹뷰 등 여러 레포를 독립적으로 관리하고 있었습니다. 이를 모노레포로 합치면 Git 히스토리, 권한, CI, 배포까지 전부 건드려야 합니다. 그래서 레포 구조 자체는 바꾸지 않고, 코드몽이 시작할 때 모든 레포를 하나의 디렉토리 아래에 clone해서 에이전트에게만 가상 모노레포 시야를 부여했습니다.

팀의 운영 조는 그대로 두고, 에이전트만 레포를 넘나들 수 있게 한 것입니다.

이 구조를 만든 의도는 여러 레포를 동시에 수정하는 것보다, 질문의 경계가 레포 경계와 일치하지 않을 때 에이전트가 알아서 넘나들게 하는 것이었습니다. 실제 사용 로그가 이 판단이 맞았음을 보여줬습니다. 버킷리스트 입력/키보드 관련 질문이 들어오면 코드몽이 모바일, 백엔드, 웹뷰 레포를 한 번에 뒤져서 실제 구현이 모바일에만 있다는 걸 바로 정리해 줬고, 경계가 모호한 질문도 네다섯 개 레포를 훑어 범위를 빠르게 좁혔습니다. 사람이 하면 레포를 바꿔가며 다시 설명하고 다시 검색해야 하는 일을, 에이전트가 한 번에 흡수한 것입니다. 이 구조의 가장 큰 가치는 cross-repo understanding — 사람이 머릿속에서 하던 레포 간 맥락 전환을 에이전트가 대신 해주는 것이었습니다.

같은 레포에 변경 요청이 동시에 들어오는 경우에는 repo별 실행 큐로 순서를 보장하고, 각 실행은 별도 git worktree에서 격리합니다. 순서는 큐가, 작업 공간 충돌은 worktree가 막는 구조입니다.

배운 것, 그리고 다음 과제

  1. 에이전트는 모델이 아니라 state machine에 가깝습니다. 세션, 라우팅, 큐, 상태 머신 — 코드몽에서 모델이 직접 하는 일은 전체 시스템의 일부일 뿐입니다.
  2. prompt engineering보다 context assembly가 품질에 더 큰 영향을 줬습니다. 프롬프트 문구를 다듬는 것보다 맥락을 제대로 모으는 것이 먼저였습니다.
  3. 필요에 따라 모델이 낸 의도(Intent)와 실제 실행(Execution)을 분리하는 것이 좋습니다. 그래야 검증, 승인, 재시도를 사이에 끼울 수 있습니다.
  4. 모델이 충분히 똑똑하다면, 도구를 넓게 열고 위험한 곳에만 제약을 거는 게 낫습니다. 모든 걸 하드코딩하기보다, hard constraint를 어디에 둘지를 선택하는 설계가 더 유연했습니다.

앞서 멀티레포/모노레포에 대해 설명할 때, 목록에 codemon이 있는 걸 눈치채셨나요? 맞습니다. 코드몽은 자기 자신을 고칠 도구를 갖춘, 자가발전형 에이전트로 설계되었습니다. 다음 글에서는 에이전트가 자기 자신의 코드를 읽고, 고치고, PR을 올리는 구조 — 그리고 그것을 가능하게 만든 기술 선택에 대해 정리하겠습니다.


링크 복사

SumOne 모니모니 · Product Owner

커플을 위한 서비스 SumOne 만드는 모니모니입니다.

댓글 0
댓글이 없습니다.
추천 아티클
SumOne 모니모니 · Product Owner

커플을 위한 서비스 SumOne 만드는 모니모니입니다.

0