내가 LLM으로 소프트웨어를 만드는 방법
(stavros.io)- LLM을 활용한 소프트웨어 개발에서 아키텍트-개발자-리뷰어 다중 에이전트 워크플로우를 통해 수만 줄 규모의 프로젝트를 낮은 결함률로 유지하는 구체적인 방법론 공유
- 프로그래밍 자체보다 무언가를 만드는 것에 관심이 있었으며, LLM이 코딩을 잘하게 되면서 만들기에 집중할 수 있게 됨
- 코드 작성 능력보다 시스템 아키텍처 설계와 올바른 선택을 내리는 엔지니어링 스킬이 훨씬 더 중요해짐
- 서로 다른 모델을 혼합 사용하여 코드 리뷰 품질을 높이고, 각 모델의 강점과 약점을 역할별로 분리 활용
- 실제 이메일 기능 추가 세션 전체를 공개하며, 아키텍처 결정부터 QA까지 인간이 주도하는 LLM 협업 과정을 상세히 기록
LLM으로 만들기의 이점
- 프로그래밍을 좋아한다고 생각했지만, 실제로는 만드는 것 자체를 좋아했으며, LLM이 프로그래밍을 잘하게 되면서 쉬없이 무언가를 만들고 있음
- Codex 5.2 출시 무렵, 그리고 최근 Opus 4.6에 이르러 매우 낮은 결함률로 소프트웨어 작성이 가능해짐. 직접 손으로 코딩할 때보다 결함률이 유의미하게 낮을 수 있음
- 이전에는 2~3일 작업 후 코드가 유지보수 불가 상태로 빠졌지만, 현재는 수 주간 연속 작업하며 수만 줄의 유용한 코드를 안정적으로 성장시키는 중
- 엔지니어링 스킬이 쓸모없어진 것이 아니라 이동한 것: 코드를 올바르게 작성하는 능력 대신, 시스템을 올바르게 아키텍처하고 사용 가능하게 만드는 판단력이 핵심
- 기반 기술을 잘 아는 프로젝트(예: 백엔드)에서는 수만 SLoC에서도 문제가 없지만, 잘 모르는 기술(예: 모바일 앱)에서는 여전히 잘못된 선택의 누적으로 코드가 엉망이 됨
- LLM 초기(davinci 이후)에는 모든 코드 라인을 검토해야 했고, 이후 세대에서는 함수 단위, 현재는 전체 아키텍처 수준에서만 확인하면 되는 추세. 내년에는 그조차 불필요해질 수 있음
이 방식으로 만든 프로젝트들
- Stavrobot: 보안에 초점을 맞춘 LLM 개인 비서. 캘린더 관리, 가용성 판단, 리서치, 코드 작성으로 자기 확장, 리마인더, 집안일 자율 관리 등 수행. 하나의 킬러 기능이 아니라 수천 개의 작은 불편을 해소하는 것이 핵심 가치
- Middle: 음성 메모를 녹음하고 텍스트로 변환해 웹훅으로 전송하는 작은 펜던트 장치. 항상 휴대 가능하고 마찰 없이 사용 가능한 것이 핵심
- Sleight of Hand: 초 단위로 불규칙하게 째깍거리지만 분 단위에서는 항상 정확한 벽시계 아트 프로젝트. 500ms~1500ms 가변 틱, 인지하기 어려운 속도 변화 후 무작위 정지, 이중 속도로 달려가 30초 대기 등 다양한 모드 제공
- Pine Town: 무한 멀티플레이어 캔버스로, 각자 작은 땅을 받아 그림을 그리는 인터랙티브 메도우 프로젝트
- 이 모든 프로젝트를 LLM으로 만들었고, 대부분의 코드를 직접 읽은 적이 없지만 각 프로젝트의 아키텍처와 내부 작동 방식을 잘 알고 있음
하네스(Harness) 도구
- 하네스로 OpenCode를 사용 중이며, Pi도 좋은 경험이 있었음
- 하네스의 필수 요건 두 가지:
- 여러 회사의 다양한 모델 사용 가능: 대부분의 퍼스트파티 하네스(Claude Code, Codex CLI, Gemini CLI)는 자사 모델만 지원하므로 이 요건 불충족
- 커스텀 에이전트가 서로 자율적으로 호출 가능
복수 모델 사용의 필요성
- 특정 모델을 한 사람으로 간주할 수 있으며, 컨텍스트를 초기화해도 같은 의견/강점/약점을 유지하는 경향
- 모델에게 자기가 쓴 코드를 리뷰하게 하면 자기 동의 경향이 있어 거의 무의미하지만, 다른 모델에게 리뷰를 맡기면 품질이 크게 향상됨
- 현재 기준 Codex 5.4는 꼼꼼하고 까다로워서 리뷰에 적합, Opus 4.6는 본인이 내렸을 결정과 잘 일치, Gemini 3 Flash도 다른 모델이 놓친 해법을 제시하는 경우 있음
- 최적의 결과를 위해 모든 모델의 혼합 사용이 필요
워크플로우: 아키텍트 → 개발자 → 리뷰어
- 워크플로우는 아키텍트, 개발자, 1~3명의 리뷰어로 구성. 이들은 OpenCode 에이전트(스킬 파일)로 설정됨
- 복수 에이전트 사용의 세 가지 이유:
- 비싼 모델(Opus)은 계획 수립에, 저렴한 모델(Sonnet)은 코드 작성에 사용하여 토큰 절약
- 서로 다른 모델로 리뷰하면 각기 다른 문제를 포착
- 역할별 권한 분리 가능(예: 읽기 전용 vs 쓰기 가능)
- 같은 모델, 같은 권한으로 두 에이전트를 사용하는 것은 한 사람이 다른 모자를 쓰는 것과 같아 큰 의미 없음
- 스킬 파일은 직접 수작업으로 작성. LLM에게 스킬을 쓰게 하면, 누군가에게 "훌륭한 엔지니어가 되는 법"을 쓰게 한 뒤 그 지침을 돌려주며 "이제 훌륭해져라"라고 하는 것과 같음
아키텍트 역할
- 아키텍트(현재 Claude Opus 4.6)는 유일하게 직접 대화하는 에이전트이며, 가장 강력한 모델이어야 함
- 매우 구체적인 기능이나 버그 수정 목표를 제시하고, 목표·제약·트레이드오프를 확정할 때까지 최대 30분간 대화 진행
- 결과물은 개별 파일과 함수 수준의 상당히 저수준의 계획
- 단순 프롬프팅이 아니라 LLM의 도움으로 계획을 형성하는 과정. LLM이 틀리거나 본인 방식과 다를 때 많이 교정하며, 이것이 프로젝트를 "자기 것"으로 만드는 핵심 기여
- "approved"라는 단어를 명시적으로 말할 때까지 구현을 시작하지 않도록 설정. 일부 모델은 스스로 이해했다고 판단하면 성급하게 구현에 착수하는 경향
- 승인 후 아키텍트가 작업을 태스크로 분할하여 계획 파일에 상세히 기록하고 개발자를 호출
개발자 역할
- 개발자는 더 약하고 토큰 효율적인 모델(Sonnet 4.6) 사용 가능
- 계획이 재량의 여지를 최소화하므로, 역할은 엄격하게 계획의 변경사항을 구현하는 것
- 구현 완료 후 리뷰어를 호출
리뷰어 역할
- 각 리뷰어가 독립적으로 계획과 diff를 검토하고 비평
- 최소 Codex를 항상 사용하고, 때로 Gemini 추가, 중요 프로젝트에는 Opus도 추가
- 피드백은 개발자에게 돌아가며, 리뷰어 간 의견 불일치 시 아키텍트에게 에스컬레이션
- Opus는 올바른 피드백을 선택하는 데 뛰어나며, 너무 까다로운(구현 대비 실질적 문제 가능성이 낮은) 피드백은 무시하기도 함
전반적 접근 방식과 실패 모드
- 이 방식으로 함수 수준 이상의 모든 선택을 파악하고, 후속 작업에 해당 지식을 활용
- LLM이 코드베이스에서 특정 요소를 놓치는 맹점이 있을 때, "Y를 사용해야 한다"고 지시하면 LLM이 Y의 존재를 인식하고 더 나은 방법으로 전환
- 기술에 익숙하지 않을 경우 LLM의 잘못된 결정을 포착하지 못하고, 그 위에 잘못된 결정이 누적되어 결국 풀 수 없는 상태에 도달
- "코드가 작동하지 않는다"고 반복 지적하면 LLM이 "알겠습니다! 고치겠습니다"라며 오히려 더 망가뜨리는 것이 전형적 실패 패턴
- 따라서 특정 기술에 익숙하지 않더라도 계획 단계에서 가능한 한 많이 이해하려고 노력
실제 세션: Stavrobot에 이메일 지원 추가
- 실제 주석 달린 세션 전문을 공개하며, 도구 호출과 장황한 부분은 생략했지만 대화와 의사결정 과정은 그대로 유지
-
초기 대화: 높은 수준의 목표 제시("이 봇에 이메일 지원을 추가하고 싶다") → LLM이 코드를 읽고 현재 패턴(인바운드 웹훅 → enqueueMessage → LLM 처리 → 응답) 파악 후 설계 질문 제시
- 인바운드 방식(IMAP 폴링, 웹훅, SMTP 서버), 아웃바운드 방식, 양방향 여부, 아키텍처(별도 컨테이너 vs 인프로세스), HTML 이메일 처리, 스레드 추적, 첨부파일 등
- 설계 결정: Cloudflare Email Worker 웹훅으로 인바운드, SMTP 클라이언트로 아웃바운드, 완전 양방향 대화, 인프로세스 처리, 마크다운 변환, 독립 이메일 취급, 첨부파일 지원
-
LLM의 상세 설계 제안: MIME 파싱(mailparser 사용), 웹훅 인증(공유 시크릿), 아웃바운드 제목줄 필요성, HTML 전용 이메일 처리, From 주소 정체성, 전달된 이메일 처리, 아웃바운드 첨부파일 등 7개 우려사항과 구체적 설계안 제시
- Worker 페이로드를
{ from, to, raw }로 단순화하고 서버 측에서 파싱하는 방식 제안 - Config 구조, 인바운드/아웃바운드 흐름, 수정 파일 목록, 명시적 비목표(YAGNI) 항목 정리
- Worker 페이로드를
- 계획 정제: README.md와 config.example.toml 업데이트, 이메일 허용목록 페이지에서 E.164 검증 제거 등 누락 사항을 인간이 지적 → LLM이 통합
- 6개 태스크로 분할: Config/의존성, 허용목록, 허용목록 UI/백엔드 검증, 인바운드 이메일, 아웃바운드 이메일, README/테스트
- 추가 개선: SMTP 설정 없이도 인바운드 이메일만 작동하도록 SMTP 필드를 선택사항으로 변경하는 아이디어 제안 → 구현
-
QA에서 발견된 버그: 소유자 이메일이 등록되지 않아 메시지가 드롭되는 문제 →
seedOwnerInterlocutor가 이메일 채널을 누락한 것이 원인 → 수정 -
코드 개선 제안: 채널별 하드코딩된 if 블록 대신 공유 채널 목록을 순회하도록 리팩토링. Telegram의 숫자 변환 특수 케이스 논의 후,
seedOwnerInterlocutor에만 루프를 적용하고getOwnerIdentities는 유형 차이가 본질적이므로 유지하기로 결정 -
와일드카드 이메일 허용목록:
*@example.com형태의 도메인 수준 와일드카드 지원 추가. 일회용 이메일 주소를 사용하는 실제 유스케이스에서 필요- 보안 고려:
"me@mydomain.com"@evildomain.com같은 공격 방지를 위해*를[^@]*로 변환하여 @ 경계를 넘지 못하도록 처리 -
myusername+*@gmail.com같은 부분 와일드카드도 지원 - 정규식 사용 시 이메일 주소의 다른 모든 문자를 이스케이프해야 함을 인간이 지적
- 보안 고려:
- 소유자 필드와 허용목록 모두에서 와일드카드 작동 확인, 동일한
matchesEmailEntry헬퍼 함수 사용 - 전체 기능 구현에 약 1시간 소요
에필로그
- 극도로 화려한 셋업은 아니지만 매우 잘 작동하며, 프로세스의 신뢰성에 만족
- Stavrobot을 거의 한 달간 24/7 운영 중이며 매우 안정적
20여년전 웹에디터나 양산형 블로그의 유행으로 아무도 보지 않을 홈페이지나 포스트들이 대거 양상되었던 것처럼 인공지능 시대를 맞이하여 비슷한 양상이 있으나 커스텀앱을 만들고 그 프로세스나 루틴을 공유하는 것은 분명 훌륭하고 큰 자산이라고 생각함. 개인적으로 지금 시대는 인공지능으로 돈이 되는 앱이나 서비스를 만드는 것이 아니라 내가 필요한 커스텀 도구를 손쉽게 만들어서 생산성을 높이는 것이라고 봄
비싼 모델(Opus)은 계획 수립에, 저렴한 모델(Sonnet)은 코드 작성에 사용하여 토큰 절약
계획은 Sonnet. 코드 구현은 Opus 으로 하시는 경우도 많던데 여긴 반대네
https://code.claude.com/docs/ko/model-config#opusplan-모델-설정
저도 클로드의 opusplan 으로 모델 설정해놓고 쓰고있어요