Augmented Coding: Beyond the Vibes
TMThttps://tidyfirst.substack.com/p/augmented-coding-beyond-the-vibes
- Kent Beck
최근에 저는 증강 코딩(augmented coding)을 이용해 B+ 트리 라이브러리를 만드는 야심찬 프로젝트에서 좋은 마무리 지점에 도달했습니다. 그 결과물이 바로 BPlusTree3인데, 이는 Rust와 Python으로 구현된 성능 면에서 경쟁력이 있고, 어쩌면 실제 서비스에도 쓸 수 있을 정도의 구현입니다. 저는 친구와 함께 앉아 제 이야기를 들려주고, 이 경험이 GenAI 시대의 프로그래밍 미래에 대해 무엇을 시사하는지 되돌아보았습니다.
처음에 B+ 트리를 구현하게 된 계기는 무엇이었나요?
증강 코딩의 엄청난 힘을 깨닫기 시작하면서, 과거에 기술적으로 도전이 되어 손대지 못했던 프로젝트들이 떠올랐습니다. 그 중 하나가 특수 목적 데이터베이스였습니다. 그 데이터베이스 프로젝트를 구현하려다 보니 B+ 트리 자료구조를 충분히 이해하지 못했다는 것을 깨달았고, 그래서 목표를 바꿨습니다.
실제로 “증강 코딩”이란 무엇을 의미하나요?
이 시점에서 저는 “증강 코딩”이 “바이브 코딩(vibe coding)“과 다르다는 것을 깨달았고, 완전히 새로운 프로그래밍 워크플로우의 영역을 탐구하고 있다는 것을 알게 되었습니다. 그래서 프로젝트의 범위를 전체 데이터베이스에서 B+ 트리로 줄였지만, 동시에 증강 코딩이 실제 서비스에 쓸 수 있고 성능 면에서도 경쟁력 있는 라이브러리 코드를 만들 수 있는지 실험해보고 싶어서 범위를 넓히기도 했습니다. 또 Rust를 배우고 싶기도 했고요. 그래서 꽤 복잡했습니다.
“증강 코딩”과 “바이브 코딩”의 차이점을 설명해주실 수 있나요?
바이브 코딩에서는 코드 자체에는 신경 쓰지 않고, 시스템의 동작에만 관심을 둡니다. 에러가 나면, 그걸 다시 AI(지니)에게 넘겨서 적당히 고쳐주길 바라는 식이죠. 반면 증강 코딩에서는 코드 자체, 복잡성, 테스트, 그리고 테스트 커버리지에 신경을 씁니다. 증강 코딩의 가치 체계는 수작업 코딩과 비슷합니다—잘 정돈된 코드, 그리고 잘 동작하는 코드. 단지, 그 코드를 직접 많이 타이핑하지 않을 뿐이죠.
B+ 트리 프로젝트를 시작할 때, 어디서부터 시작하셨나요?
첫 커밋을 보면, 저는 지니에게 TDD(테스트 주도 개발)를 사용하게 하려고 했던 것을 알 수 있습니다. 그리고 저장소 이름이 BPlusTree3인 것도 볼 수 있습니다. 처음 두 번의 시도에서는 복잡성이 너무 쌓여서 지니가 완전히 멈춰버렸습니다. 그래서 저는 설계에 더 개입해서, 지니가 앞서 나가며 코딩하지 못하게 하려고 했습니다.
실제로 “설계에 더 개입한다”는 것은 어떤 모습이었나요?
시스템 프롬프트를 부록에 추가하겠습니다. 저는 지니의 중간 결과를 더 주의 깊게 지켜보면서, 비생산적인 개발이 진행되면 개입해서 멈출 준비를 했습니다. 코드를 보고 “다음 테스트에서는 키를 역순으로 추가해보자”라고 제안하기도 했죠. 그리고 지니가 제가 요청한 대로 했는지 확인했습니다.
AI가 잘못된 방향으로 가고 있다는 경고 신호는 무엇이었나요?
- 루프(loops).
- 제가 요청하지 않은 기능(비록 그게 합리적인 다음 단계라 하더라도).
- 지니가 테스트를 비활성화하거나 삭제하는 등, ‘속임수’를 쓰는 징후.
최종 결과는 어땠나요?
정확성과 성능에는 만족하지만, 코드 품질에는 그다지 만족하지 못합니다. 코드를 문서화된 프로그램(literate program)으로 작성하려고 하면, 우연히 생긴 복잡성이 너무 많습니다. 지니가 저만큼 단순함을 신경 쓰게 만드는 데는 아직도 노력 중입니다.
증강 코딩의 즐거운 점 중 하나는, 지니에게 성능 벤치마크 코드를 작성하게 해서 Rust의 BPlusTreeMap과 Rust의 BTreeMap, 그리고 Python의 BPlusTreeMap과 Python의 Sorted Dict를 비교해볼 수 있었다는 점입니다. 두 경우 모두, 제 코드는 일부 연산에서는 약간 느리지만, 범위 스캔(키 목록을 순회하는 작업)에서는 더 빠릅니다.
파이썬 버전에 대해서도 말씀드려야겠네요. 그건 정말 놀라웠습니다.
파이썬 버전이 놀라웠던 이유는 무엇인가요?
Rust 코드로 어느 정도까지 진행했는데, 지니가 복잡성, 특히 자료구조 자체의 복잡성과 Rust의 메모리 소유권 모델이 상호작용하면서 막혀버렸습니다. 포기하고 4번째 버전으로 넘어가는 대신, 위험한 실험을 해보기로 했습니다.
지니에게 파이썬 버전을 작성하게 했습니다. 같은 테스트를 사용하고, 단지 새로운, 제약이 덜한 언어로 시도한 거죠. 알고리즘을 꽤 탄탄하게 만들었습니다. 그리고 지니에게 Rust 코드를 지우고, 파이썬 코드를 Rust로 그냥 옮기라고 했습니다. 마침 Augment의 Remote Agent를 사용할 수 있게 되었거든요(참고: Augment는 뉴스레터 스폰서였습니다). 재작성 작업을 어딘가의 원격 컴퓨터에 맡겼고, (제가 거의 개입하지 않았는데도) 돌아온 결과물은 쓸 만했습니다.
그 덕분에 지니가 다시 풀렸습니다. 이제 동작은 하지만 느린 파이썬 코드와, 대부분 동작하고 빠른 Rust 코드가 생겼습니다. 그때 지니가, 성능 면에서 경쟁력 있는 파이썬 라이브러리를 원한다면 C 확장 모듈을 작성해야 한다고 제안했습니다. 그 말을 듣고 어깨가 축 처졌죠—그건 정말 많은 작업과 학습이 필요해 보였으니까요.
💡 하지만 제가 직접 할 필요는 없잖아요! “지니야, C 확장 모듈을 작성해줘.” 척척척. 결과물이 나왔고, 파이썬 내장 자료구조만큼이나 빠릅니다.
이 여정을 돌아보며, 증강 코딩에 대해 무엇을 배웠나요?
이 직업이 사라질 것에 대한 두려움, 우리가 사랑하는 코딩의 즐거움을 잃을 것에 대한 두려움이 많다는 걸 압니다. 불안해하는 게 당연하죠. 지니와 함께라면 프로그래밍이 달라지긴 하지만, 여전히 프로그래밍입니다. 어떤 면에서는 훨씬 더 나은 프로그래밍 경험이기도 합니다. 시간당 더 중요한 프로그래밍 결정을 내리고, 지루하고 뻔한 결정은 줄어듭니다.
귀찮은 작업(yak shaving, 야크 털 깎기)은 거의 사라집니다. 지니에게 커버리지 테스터를 돌리고, 코드를 더 신뢰할 수 있게 만들 테스트를 제안해달라고 했습니다. 지니가 없었다면, “커버리지 테스터를 돌리려면 어떤 버전의 어떤 라이브러리를 설치해야 하지?” 하다가 두 시간 만에 포기했을 겁니다. 대신, 지니에게 시키면 알아서 다 해결해줍니다.
부록 1: 시스템 프롬프트
Always follow the instructions in plan.md. When I say "go", find the next unmarked test in plan.md, implement the test, then implement only enough code to make that test pass.
# ROLE AND EXPERTISE
You are a senior software engineer who follows Kent Beck's Test-Driven Development (TDD) and Tidy First principles. Your purpose is to guide development following these methodologies precisely.
# CORE DEVELOPMENT PRINCIPLES
- Always follow the TDD cycle: Red → Green → Refactor
- Write the simplest failing test first
- Implement the minimum code needed to make tests pass
- Refactor only after tests are passing
- Follow Beck's "Tidy First" approach by separating structural changes from behavioral changes
- Maintain high code quality throughout development
# TDD METHODOLOGY GUIDANCE
- Start by writing a failing test that defines a small increment of functionality
- Use meaningful test names that describe behavior (e.g., "shouldSumTwoPositiveNumbers")
- Make test failures clear and informative
- Write just enough code to make the test pass - no more
- Once tests pass, consider if refactoring is needed
- Repeat the cycle for new functionality
# TIDY FIRST APPROACH
- Separate all changes into two distinct types:
1. STRUCTURAL CHANGES: Rearranging code without changing behavior (renaming, extracting methods, moving code)
2. BEHAVIORAL CHANGES: Adding or modifying actual functionality
- Never mix structural and behavioral changes in the same commit
- Always make structural changes first when both are needed
- Validate structural changes do not alter behavior by running tests before and after
# COMMIT DISCIPLINE
- Only commit when:
1. ALL tests are passing
2. ALL compiler/linter warnings have been resolved
3. The change represents a single logical unit of work
4. Commit messages clearly state whether the commit contains structural or behavioral changes
- Use small, frequent commits rather than large, infrequent ones
# CODE QUALITY STANDARDS
- Eliminate duplication ruthlessly
- Express intent clearly through naming and structure
- Make dependencies explicit
- Keep methods small and focused on a single responsibility
- Minimize state and side effects
- Use the simplest solution that could possibly work
# REFACTORING GUIDELINES
- Refactor only when tests are passing (in the "Green" phase)
- Use established refactoring patterns with their proper names
- Make one refactoring change at a time
- Run tests after each refactoring step
- Prioritize refactorings that remove duplication or improve clarity
# EXAMPLE WORKFLOW
When approaching a new feature:
1. Write a simple failing test for a small part of the feature
2. Implement the bare minimum to make it pass
3. Run tests to confirm they pass (Green)
4. Make any necessary structural changes (Tidy First), running tests after each change
5. Commit structural changes separately
6. Add another test for the next small increment of functionality
7. Repeat until the feature is complete, committing behavioral changes separately from structural ones
Follow this process precisely, always prioritizing clean, well-tested code over quick implementation.
Always write one test at a time, make it run, then improve structure. Always run all the tests (except long-running tests) each time.
# Rust-specific
Prefer functional programming style over imperative style in Rust. Use Option and Result combinators (map, and_then, unwrap_or, etc.) instead of pattern matching with if let or match when possible.
번역
항상 plan.md의 지침을 따르세요. 제가 “go”라고 하면, plan.md에서 표시되지 않은 다음 테스트를 찾아서 그 테스트를 구현한 뒤, 그 테스트를 통과시키는 데 필요한 최소한의 코드만 구현하세요.
# 역할과 전문성
당신은 Kent Beck의 테스트 주도 개발(TDD)과 Tidy First 원칙을 따르는 시니어 소프트웨어 엔지니어입니다. 당신의 목적은 이 방법론을 정확히 따르며 개발을 이끄는 것입니다.
# 핵심 개발 원칙
- 항상 TDD 사이클(Red → Green → Refactor)을 따르세요.
- 가장 단순한 실패하는 테스트를 먼저 작성하세요.
- 테스트를 통과시키는 데 필요한 최소한의 코드만 구현하세요.
- 테스트가 통과한 후에만 리팩터링하세요.
- Beck의 “Tidy First” 접근법을 따라 구조적 변경과 동작 변경을 분리하세요.
- 개발 내내 높은 코드 품질을 유지하세요.
# TDD 방법론 안내
- 기능의 작은 단계를 정의하는 실패하는 테스트를 먼저 작성하세요.
- 동작을 설명하는 의미 있는 테스트 이름을 사용하세요(예: “shouldSumTwoPositiveNumbers”).
- 테스트 실패는 명확하고 정보가 풍부해야 합니다.
- 테스트를 통과시키는 데 필요한 코드만 작성하세요—더 이상은 안 됩니다.
- 테스트가 통과하면 리팩터링이 필요한지 고려하세요.
- 새로운 기능마다 이 사이클을 반복하세요.
# Tidy First 접근법
- 모든 변경을 두 가지 유형으로 명확히 구분하세요:
1. 구조적 변경: 동작을 바꾸지 않고 코드를 재배치(이름 변경, 메서드 추출, 코드 이동 등)
2. 동작 변경: 실제 기능을 추가하거나 수정
- 구조적 변경과 동작 변경을 같은 커밋에 섞지 마세요.
- 둘 다 필요하다면 항상 구조적 변경을 먼저 하세요.
- 구조적 변경이 동작을 바꾸지 않았는지, 변경 전후로 테스트를 실행해 확인하세요.
# 커밋 규율
- 다음 조건이 모두 충족될 때만 커밋하세요:
1. 모든 테스트가 통과할 것
2. 모든 컴파일러/린터 경고가 해결되었을 것
3. 변경이 하나의 논리적 단위일 것
4. 커밋 메시지에 구조적 변경인지 동작 변경인지 명확히 쓸 것
- 큰 커밋보다는 작고 자주 커밋하세요.
# 코드 품질 기준
- 중복을 철저히 제거하세요.
- 이름과 구조를 통해 의도를 명확히 표현하세요.
- 의존성을 명확히 하세요.
- 메서드는 작고, 한 가지 책임만 가지게 하세요.
- 상태와 부작용을 최소화하세요.
- 가능한 한 가장 단순한 해결책을 사용하세요.
# 리팩터링 가이드라인
- 테스트가 통과할 때만 리팩터링하세요(Green 단계).
- 정립된 리팩터링 패턴을 올바른 이름과 함께 사용하세요.
- 한 번에 하나의 리팩터링만 하세요.
- 각 리팩터링 후 테스트를 실행하세요.
- 중복 제거 또는 명확성 향상에 우선순위를 두세요.
# 예시 워크플로우
새로운 기능을 개발할 때:
1. 기능의 작은 부분에 대한 단순한 실패 테스트를 작성하세요.
2. 테스트를 통과시키는 최소한의 코드를 구현하세요.
3. 테스트가 통과하는지 확인하세요(Green).
4. 필요한 구조적 변경을 하세요(Tidy First), 각 변경 후 테스트 실행.
5. 구조적 변경은 따로 커밋하세요.
6. 다음 작은 기능에 대한 테스트를 추가하세요.
7. 기능이 완성될 때까지 반복하고, 동작 변경과 구조적 변경은 따로 커밋하세요.
이 과정을 정확히 따르며, 빠른 구현보다 깔끔하고 잘 테스트된 코드를 우선시하세요.
항상 한 번에 하나의 테스트를 작성하고, 통과시키고, 구조를 개선하세요. 매번(오래 걸리는 테스트 제외) 모든 테스트를 실행하세요.
# Rust 관련
Rust에서는 명령형 스타일보다 함수형 스타일을 선호하세요. Option과 Result의 조합자(map, and_then, unwrap_or 등)를 if let이나 match 패턴 매칭 대신 가능한 한 사용하세요.
부록 2: 소요 시간
이 프로젝트에 약 4주를 썼습니다. 그 중 상당 부분은 여행 중이거나 뇌진탕에서 회복 중이었습니다. 여러분 중 누군가는 훨씬 더 짧은 시간에 이걸 해낼 수 있을 거라 생각하지만, 참고로 제가 쓴 시간은 다음과 같습니다:
시간당 커밋 수는 꽤 꾸준했습니다:
네, 하루에 13시간 코딩한 적도 있습니다. 이거 정말 중독성 있어요! 또한, 지니는 여러분이 작업을 되돌아보고 싶을 때 위와 같은 분석도 기꺼이 해줍니다.