Claude Code나 Codex와 같은 에이전틱 코딩의 등장으로 비전공자도 손쉽게 앱을 만들어낼 수 있는 시대가 되었다. 기존 현업 프로그래머에게는 스스로를 증명해야 할 허들이 더욱 높아진 셈이다. 반면 그로 인한 기술 부채가 늘어나는 것 또한 자명하다. 간단한 취미용 앱은 몇 마디 문장만으로 뚝딱 만들어낼 수 있지만, 프로덕션에 올라갈 복잡한 프로젝트라면 이야기는 달라진다.
나는 기존에 경험해본 적 없는 도메인에서, 낯선 프레임워크와 언어로 새 프로젝트를 맡게 되었다. 잘 안되면 어쩌나 하는 걱정이 컸지만, 착수하기 전 어떻게 구조를 설계할지 충분히 준비했고, 아무런 파일 없이 시작한 지 3개월이 지난 지금은 안정적인 환경에서 개발 공정이 이루어지고 있다.
어떻게 가능했을까? 핵심은 이것이다. LLM은 입력에 대해 가장 그럴싸한 출력을 패턴 기반으로 찾아낸다. 따라서 LLM이 우리의 의도를 명확하게 이해할 수 있는 환경을 만들고, 이를 개발의 모든 프로세스에 녹여내야 한다. 지금 내가 이 방식을 어떻게 운용하고 있는지를 회고 겸 정리해본다.
아키텍처의 중요성
에이전틱 코딩을 하는 사람들에게서는 ‘마크다운이 곧 언어다’라는 표현이 널리 쓰인다. 예전에는 “talk is cheap, show me the code”가 덕목이었지만, 이제는 “code is cheap, show me the talk”이 주류가 되었다. 내가 모르는 언어라도 정확한 시나리오와 기술 스택, 데이터 흐름을 정의해두면 LLM은 구현해낼 수 있다.
LLM을 다루면서 가장 크게 느낀 것은 객관적인 지침이 매우 중요하다는 것이다. 예를 들어 테스트 코드를 작성할 때 네이밍 컨벤션이 적당한지는 그때그때 LLM의 기분에 따라 달라진다. 주관적인 개념을 항상 일관된 결과로 출력하는 것은 현 시점에선 불가능하다.
그래서 아키텍처 문서는 더욱 중요해진다. 프로젝트를 맡은 후 두 달 가까이를 아키텍처 문서의 완성도와 정합성을 끌어올리는 데에만 집중했다. 특히 모든 문서에서 일관된 용어를 사용하고 모호한 표현이 끼어들지 않도록 하는 것이 핵심이다. LLM이 최대한 객관적으로 판단하고 작성할 수 있게 하기 위해서다. 오랜 시간이 걸렸지만 지금 돌아보면 여기에 시간을 충분히 들인 것이 탁월한 선택이었다.
사실 이전에 MVP를 만들 때는 아키텍처 정의 없이 바이브 코딩으로 무작정 시작했다. 하지만 확장 단계에서 한계에 부딪혔다. 전체 구조를 뜯어고치는 과정에서, 쏟아지는 에러와 틀어진 정합성을 수습하는 것은 너무 어려운 일이었고, 차라리 새로 만드는 게 낫다는 결론에 이르렀다. 이런 실패를 경험하고 나서 코드를 작성하기 전에 충분한 기획이 필요하다는 것을 이해했다.[1]
우선 아키텍처를 별도의 git repository로 만들고 지침을 먼저 세웠다. 마크다운 100%로 구성하고, 실제 구현과 코드 스니핏을 담지 않기로 했다. 여기에 초기 몇 가지 시나리오를 담았다. README.md와 기본 폴더 구조를 잡은 다음, 필요한 내용을 별도 문서로 만들어 하나씩 구현했다.
아키텍처 문서에는 각 구성 요소의 제약사항과 스펙, 데이터베이스 스키마, 에러 핸들링과 같은 정책, API나 컨벤션과 같은 프로토콜, 시나리오 등을 마크다운으로 정의한다. 주요 흐름은 mermaid diagram로 설계한다. 글보다 흐름을 명확히 표현할 수 있고 LLM이 쉽게 이해하며 사람이 직접 보기에도 편하기 때문이다.
이 문서들을 디테일하게 그리고 정합성 있게 만들고 나서 본격적으로 작업을 시작했다. 처음에는 문서를 과도하게 작성하는 것이 사전 컨텍스트를 커지기 해서 과도하게 토큰을 소모하는 것이 아닐까 걱정했다. 하지만 계층별로 폴더를 관리하고 각 폴더에 명확한 규칙을 주입하면 생각보다 토큰 소모가 크지 않았다. 또한 마크다운 내 링크를 통해 중복 내용을 최대한 정리하는 것도 중요했다.
리뷰의 중요성
아키텍처 문서든 코드든, 모든 커밋 전에 unstaged changes를 빡세게 리뷰한다. 실제 구현에 걸리는 시간이 1이라면, 리뷰하고 수정하는 데 걸리는 시간은 5~6이 된다. 그렇기에 vibe coding에 비해 각 과정을 진행하는데 훨씬 오래 걸린다.
주로 아래와 같은 것들을 커맨드로 만들어 리뷰한다:
- 일관성 체크: 아직 커밋되지 않은 항목들에 대해 용어 일관성, 프로토콜 정합성, 시나리오 정합성, 정책 충돌, 모호한 표현을 전수 점검한다. 각 항목별로 서브에이전트를 만들어 결과를 취합해 보고받는다.
- CONTRIBUTING 체크: CONTRIBUTING.md에는 LLM이 작업할 때 지켜야 할 작업 수칙을 정의해두었다. 예를 들어 근거없는 내용 금지, 중복 표현 금지, 구현 관심사 포함 금지와 같은 것들이 있는데. 이런 컨텍스트를 줘도 LLM이 말을 듣지 않는 경우가 잦기 때문에, 사후 검증을 통해 유효한 이슈가 없을 때까지 리뷰를 반복한다.
- 이슈 필터링: 위 커맨드들이 보고한 이슈들을 다시 걸러내는 커맨드를 연이어 실행한다. 각 이슈에 대해 a) valid issue인지, false positive인지 판정, b) 우선순위를 P0/P1/P2로 분류하며 c) 판단 근거 d) valid issue인 경우 수정 방향을 제안하고 이 결과를 취합한다.
- 코드 필터링: 코드에는 위와 유사한 별도의 커맨드를 만들어 추가로 아래 두 가지를 false positive로 판단한다:
- 확률×영향의 규모: 예를 들어 몇 년에 한 번 일어날 것이라 추정되는, 아주 예외적인 상황에서 그 영향이 미미하다고 판단할 수 있는 이슈는 false positive로 처리한다. 이를 처리함으로서 발생하는 오버헤드가 더 크기 때문이다.
- 의도적 설계: git blame/log로 이슈 코드의 이력을 확인하여, 단순화나 성능을 위해 의도적으로 허용한 케이스는 false positive로 처리한다. 보통 이러한 이슈는 계속해서 필터링에 걸러지는 경우가 많다.
아키텍처와 코드의 결합
지금은 코드 repository 내에 아키텍처를 submodule로 가져와 관리하고 있다. 다소 번거롭지만 이렇게 구성하면 몇 가지 장점이 있다:
- 아키텍처 문서를 별도로 관리할 수 있다. 리뷰 범위를 한정할 수 있고, 변경사항의 추적이 용이하다.
- 내가 맡은 프로젝트는 여러 클라이언트로 배포되는데, 각각의 프로젝트에 동일한 아키텍처 문서를 SSoT(Single Source of Truth)로 지정할 수 있다.
- 코드베이스가 마음에 들지 않으면 다 날려버릴수도 있다. 아키텍처가 남아있으니 언제든 다시 시작할 수 있고, 개선점을 보완하면 더 나은 출발이 가능하다.
물론 이러한 구조의 단점도 있다:
- submodule을 업데이트 할 때마다 참조하는 repository에서도 포인터를 업데이트 후 커밋해야 한다.
- 구현하다 보면 아키텍처에 정의되지 않았거나 모호한 케이스를 다뤄야 할 때가 있다. 이때는 우선 코드를 의도에 맞게 구현하고, 아키텍처에서 바꿔야 할 사항을 TODO로 넘긴다. 그리고 코드에는 ARCH-GAP 주석을 달아둔다. 아키텍처에서 TODO를 해결한 다음 코드의 ARCH-GAP과 비교하여 제대로 반영되었는지 파악한다.
이러한 작업을 반복함으로써 아키텍처와 코드의 갭을 줄여나가고 있다. 다소 번거롭지만 이렇게 아키텍처의 정합성과 정확도를 높여가는 것이 안정적인 개발에 많은 도움이 된다.
루틴 만들기
지금까지 정의한 작업 방식을 일관적이고 반복되게 사용하고 있다. 각 단계를 루틴화하면 다음과 같다:
- 작업 선별: TODO에서 지금 해야 할 일을 고른다. 작업 단위는 Phase와 Slice로 나눈다. Phase는 Epic/Story에 대응하고, Slice는 Phase를 달성하기 위한 실행 단위다. 예를 들어 어떤 기능을 구현하기 위해 프로토콜 정의 → 서버 로직 작성 → 클라이언트 보완의 3개 Slice로 나눌 수 있다. Slice가 충분히 구체화되어 있지 않을 때는 LLM이 아키텍처 문서와 기존 코드베이스를 기반으로 Slice 분리 작업을 먼저 수행한다.
- 계획 수립: 해당 Slice를 어떻게 완수할지의 계획을 작성한다.
- 구현: TDD 기반의 Red → Green → Refactor 방식으로 코드를 작성한다.
- 아키텍처 정합성 체크: 작성된 코드가 아키텍처와 일치하는지를 확인하고, 이슈가 있으면 수정 후 재확인을 반복한다.
- 코드 리뷰: 여러 커맨드를 이용하여 엣지 케이스와 모호한 부분을 발견하고 valid issue인지 평가한다. 이슈가 있다면 수정 후 반복한다.
- E2E 테스트: 필요한 경우 실제로 돌려보며 발생하는 이슈를 해결한다.
- 1로 돌아가 다음 작업을 선별한다.
이 루틴을 반복하면서 일관적인 개발 페이스를 유지할 수 있게 되었다.
결국 판단은 사람이 한다
어떠한 이슈를 마주했을 때 고칠지 말지, 어떻게 고칠지는 사람이 판단한다. LLM도 실수하고 잘못된 판단을 할 수 있기 때문이다. 그래서 나는 또한 --dangerously-skip-permissions 같은 파라미터를 쓰지 않는다. 보수적일 수 있지만, 아키텍처든 코드든 모든 커밋은 최종적으로 내가 직접 리뷰하며 궁금한 것과 해결되지 않은 것은 짚고 넘어간다.
그래야만 나중에 실제로 문제가 터졌을 때 원인을 빠르게 파악하고 LLM에 적절한 지침을 내릴 수 있기 때문이다. 나는 내가 통제할 수 없는 일이 프로젝트에서 일어나는 것을 원하지 않는다. 그렇게 되지 않도록 관리하는 것이 LLM을 사용하는 나의 책임이다.
LLM과 함께 문제를 정의하고 해결하는 과정에서 CS 전공 지식을 갖추는 것은 불필요한 시행착오로부터 우리를 구원한다는 것을 경험했다. 그렇기에 아무리 에이전틱 코딩이 발전한다 해도, 경험과 직관을 가지고 있는 리뷰어가 없이는 성공적인 프로젝트가 완성될 수 없다는 확신을 갖게 되었다.
[1] 물론 이 방식은 일종의 워터폴 구조라 프로젝트의 성격에 따라 맞지 않을 수 있다. 명확한 목표가 있어서 가능한 방식이었다. 처음부터 모든 걸 정하지 않더라도 아키텍처를 점진적으로 확장하면서 개발하는 방식도 충분히 가능할 것이라고 생각된다.