9장. 첫 에이전틱 코딩 — 버그 하나를 끝까지
8장에서 CLAUDE.md 와 권한 설정을 커밋했다.
이제 처음으로 코드를 고친다.
3장에서 개념으로 훑은 그 버그를,
이번에는 실제 세션으로 처음부터 끝까지 진행한다.
첫 작업은 고르는 것부터가 실력이다
무엇을 첫 작업으로 삼느냐가 결과의 절반이다.
좋은 첫 작업의 조건은 세 가지다.
| 조건 | 이유 |
|---|---|
| 재현 가능하다 | 고쳐졌는지 확인할 수 있다 |
| 영향 범위가 좁다 | Diff를 끝까지 읽을 수 있다 |
| 되돌리기 쉽다 | 실패해도 손실이 없다 |
반대로 첫 작업으로 최악인 것들이 있다.
⚠️ 성능 개선, 컨벤션 일괄 정리, 원인 불명 간헐적 장애.
셋 다 “고쳐졌다” 를 판정할 기준이 없다.
우리 과제는 조건에 맞는다.
결제 취소 시 포인트가 두 번 환급된다
1️⃣ 계획 모드로 시작한다
Shift+Tab 으로 계획 모드에 들어간 뒤 작업을 준다.
결제 취소 시 포인트가 두 번 환급되는 버그가 있다.
관련 코드를 찾아서 원인을 설명하고, 수정 계획을 제시해줘.
아직 코드는 수정하지 마.
- 영향받는 취소 경로를 전부 확인해줘 (전체 취소, 부분 취소, 관리자 강제 취소)
- 기존 테스트를 수정하는 방향은 제외해줘
두 번째 제약이 3장에서 본 위험한 재시도를 미리 막는다.
2️⃣ Agent가 찾아온 것을 검토한다
> grep -rn "refundPoint" --include=*.kt
point/PointRefundService.kt:42
order/OrderCancelFacade.kt:88
payment/PaymentCancelHandler.kt:61
이어서 호출 흐름 정리가 돌아온다.
OrderCancelFacade.cancel()
├─ PaymentCancelHandler.cancel()
│ → PaymentCancelledEvent 발행
│ └─ PointRefundListener.handle() → 환급 (1)
└─ PointRefundService.refund() → 환급 (2)
원인: 이벤트 리스너와 Facade의 직접 호출이 중복
여기서 개발자가 할 일은 승인이 아니다.
의심이다.
PointRefundListener는 언제 추가됐어? git log로 확인해줘.
> git log --oneline -- src/main/kotlin/point/PointRefundListener.kt
a3f9c21 feat: 결제 취소 이벤트 기반 포인트 환급 (3개월 전)
이 한 줄이 판단을 바꾼다.
세 달 전에 이벤트 방식이 추가되면서
Facade의 기존 직접 호출을 지우지 않은 것이다.
그러면 지울 쪽은 오래된 직접 호출이다.
🔥 이 확인을 건너뛰면 반대쪽을 지울 수도 있었다.
Agent는 코드만 보고, 사람은 이력을 본다.
3️⃣ 계획을 승인한다
수정 방향: OrderCancelFacade의 PointRefundService.refund() 직접 호출 제거
영향 범위: 전체 취소, 부분 취소, 관리자 강제 취소
검증: 재현 테스트 신규 1건 + 기존 취소 테스트 6건
동의하면 계획 모드를 벗어난다.
이 지점이 21장 Plan First의 실제 모습이다.
4️⃣ 재현 테스트를 먼저 만든다
먼저 이 버그를 재현하는 테스트를 추가해줘.
지금은 실패해야 정상이야.
@Test
fun `주문 취소 시 포인트 환급은 한 번만 발생한다`() {
val order = 주문_생성(usedPoint = 1_000)
orderCancelFacade.cancel(order.id)
val histories = pointHistoryRepository
.findAllByOrderId(order.id)
assertThat(histories).hasSize(1)
}
> ./gradlew test --tests '*OrderCancelTest'
주문 취소 시 포인트 환급은 한 번만 발생한다 FAILED
expected size: 1 but was: 2
실패를 확인하는 이 단계를 생략하지 않는다.
실패하지 않는 재현 테스트는 아무것도 증명하지 않는다.
5️⃣ 수정하고 다시 돌린다
> ./gradlew test --tests '*OrderCancel*'
BUILD SUCCESSFUL
7 tests completed
여기서 끝내고 싶어진다.
아직 아니다.
6️⃣ Diff는 반드시 읽는다
git diff
이 단계가 에이전틱 코딩에서 가장 자주 생략되고,
가장 자주 후회하는 단계다.
세 가지를 확인한다.
| 확인 | 왜 |
|---|---|
| 의도한 파일만 바뀌었나 | 곁가지 수정이 섞였을 수 있다 |
| 테스트가 약해지지 않았나 | 단정문 삭제, @Disabled 추가 |
| 관계없는 정리가 없나 | import 정렬, 포맷 변경이 섞이면 리뷰가 어려워진다 |
세 번째는 사소해 보이지만
Diff 200줄 중 190줄이 포맷 변경이면 리뷰는 사실상 불가능해진다.
발견하면 되돌린다.
포맷 변경은 되돌리고 로직 수정만 남겨줘.
7️⃣ 커밋은 작게
git add -p
git commit -m "fix: 주문 취소 시 포인트 이중 환급 수정
이벤트 기반 환급(a3f9c21) 도입 시 제거되지 않은
Facade의 직접 호출을 제거.
재현 테스트 추가."
커밋 메시지에 원인 커밋을 남겨두면
다음 사람이 이 판단을 다시 하지 않는다.
19장에서 말하는 외부화의 가장 값싼 형태다.
잘못됐을 때 되돌리는 법
세 가지 층이 있다.
| 상황 | 방법 |
|---|---|
| 작업 중 방향이 틀렸다 | Esc 로 중단 |
| 수정이 마음에 안 든다 | git checkout -- <파일> |
| 여러 파일이 엉켰다 | git stash 또는 브랜치 폐기 |
작업 전 git status 가 깨끗했다면
세 방법 모두 즉시 통한다.
그래서 6장에서 그 조건을 강조했다.
세션을 멈춰야 하는 신호
첫 작업에서 이런 상황이 오면 이어가지 않는다.
⚠️ 같은 수정을 두 번째로 반복한다
⚠️ 테스트를 통과시키려 검증을 약화시킨다
⚠️ 설명이 처음 설명과 달라진다
세 신호 모두 같은 뜻이다.
Context가 오염됐다.
/clear 로 새 Session을 시작하고,
방금 알아낸 사실만 정리해서 다시 넘긴다.
18장에서 이 판단 기준을 다룬다.
이 장의 핵심
- 첫 작업은 재현 가능하고, 범위가 좁고, 되돌리기 쉬운 것으로 고른다
- 성능 개선과 컨벤션 정리는 첫 작업으로 최악이다 — 판정 기준이 없다
- 계획 모드에서 시작해 원인과 계획을 먼저 받는다
- Agent는 코드를 보고, 사람은
git log를 본다 — 이력이 판단을 바꾼다 - 실패하지 않는 재현 테스트는 아무것도 증명하지 않는다
- Diff를 읽지 않고 커밋하는 것은 에이전틱 코딩이 아니다
- 커밋 메시지에 원인 커밋을 남기면 판단이 재사용된다
- 같은 수정 반복 · 검증 약화 · 설명 변경은 Session을 끊으라는 신호다