AI › Claude Code

프롬프트 대신 계약서를 쓰기 시작했다 — Claude Code 작업 계약·좁은 수정·검증 3종 세트

Skills랑 Auto Memory 세팅하고 나서 신났었는데, 정작 그 커맨드들로 실제 작업 요청을 넣을 때마다 여전히 사고가 났다. “이거 좀 고쳐줘”라고 짧게 던지면 십중팔구 뭔가 하나씩 더 바뀌어 있었다. 이번엔 그 요청 자체를 어떻게 써야 하는지 제대로 정리해봤다.


“로그인 버그 고쳐줘”가 항상 사고로 이어졌던 이유

블로그 커맨드 만들면서 제일 많이 겪은 패턴이 이거였다. “이 부분 이상해, 고쳐줘”라고만 말하면 Claude는 뭘 고쳐야 하는지, 어디까지 고쳐도 되는지 아무 정보가 없다. 그러면 나름대로 가장 그럴듯한 해석을 골라서 움직인다.

문제는 그 “그럴듯한 해석”이 내 생각이랑 다를 때다. 한 번은 /deploy 커맨드의 PR 생성 부분이 이상하다고만 말했더니, PR 생성 로직뿐 아니라 커밋 메시지 추천 로직까지 같이 손봐놨다. 나는 PR 부분만 생각하고 있었는데, Claude 입장에선 “이상하다”는 범위를 PR 관련 코드 전체로 넓게 해석한 거였다.

실제로 뭐가 문제였나

이 상황을 8단계 작동 모델로 다시 보면 원인이 명확해진다. 03. Plan 단계에서 “다음에 뭘 할지” 후보를 고르는데, 참고할 정보가 부실하면 통계적으로 가장 흔한 선택지를 고른다.

1
2
3
4
5
6
7
8
9
10
"PR 생성 부분이 이상해, 고쳐줘"
        │
        ▼
02. Context — "PR" 관련 코드를 다 훑어봄 (범위 제한 없음)
        │
        ▼
03. Plan — "이상하다"의 기준을 스스로 세움
        │   PR 생성 로직뿐 아니라 커밋 메시지 추천 로직도 "관련 있어 보여서" 후보에 포함
        ▼
결과: 요청 안 한 부분까지 같이 수정됨

이게 우연이 아니라 구조적인 문제라는 걸 알고 나서, “요청을 어떻게 써야 하는가”를 진지하게 찾아봤다. 답은 계약서였다.

작업 계약 5요소가 빈칸 추측을 막는 구조


계약서 쓰듯 요청하기 — 작업 계약 5요소

집 계약서에 뭐가 들어가는지 생각해보면 답이 나온다. 목표(어떤 집), 현재상태(지금 어떤 상태), 유지조건(가구는 그대로), 완료기준(언제까지), 검증방법(문제 생기면 어떻게 확인)이 다 적혀 있다. 이 중 하나라도 빠지면 나중에 “그건 말 안 했잖아요”로 분쟁이 생긴다.

Claude Code 요청도 똑같다. 다섯 항목을 다 채우면 Claude가 추측할 빈칸이 없어진다.

5요소가 실제로 하는 일

각 항목이 8단계 루프의 정확히 다른 지점에 꽂힌다는 걸 알고 나서야 왜 하필 이 5개인지 이해가 됐다.

계약 요소 루프 단계 역할
목표 02. Context “무엇을 위한 작업인가”의 기준점
현재상태 02. Context 탐색 범위를 좁혀서 관련 없는 파일 안 읽게 함
유지조건 03. Plan 후보 행동 중 조건 위반하는 것 사전 제거
완료기준 07. Result 언제 루프를 멈출지 종료 조건
검증방법 07. Result “완료”가 주장이 아니라 근거가 되게 함

특히 유지조건이 03. Plan에 꽂힌다는 게 핵심이었다. Plan 단계는 후보 행동을 여러 개 놓고 그중 하나를 고르는 단계인데, 유지조건이 있으면 조건 위반하는 옵션 자체가 후보군에서 애초에 빠진다. 내가 겪은 PR 사고는, 유지조건이 없어서 “커밋 메시지 로직도 손대기”가 유효한 후보로 남아있다가 선택된 거였다.

deploy 커맨드에 적용해본 예시

같은 상황을 계약 형식으로 다시 써보면 이렇게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
[처음에 썼던 요청 — 사고 유발]
/deploy 커맨드 PR 생성 부분이 이상해. 고쳐줘.


[작업 계약으로 다시 쓴 요청]
.claude/commands/deploy.md 4단계 "자동 PR 생성"에서
PR 제목에 특수문자가 있으면 gh pr create 명령이 깨지는 문제를 고쳐줘.

- 목표: 특수문자 포함 제목이어도 PR 생성 명령이 안 깨지게
- 현재상태: 4단계 섹션의 gh pr create 호출부부터 확인해줘
- 유지조건: 2단계 "커밋 메시지 추천" 로직은 절대 건드리지 마
- 완료기준: 특수문자 포함 제목으로 실제 PR 생성까지 성공
- 검증방법: 수정 후 특수문자 포함 제목으로 dry-run 결과 보여줘

바뀐 게 딱 하나다. “유지조건” 줄. 근데 이 한 줄이 있고 없고가 실제로 결과를 갈랐다. 유지조건 줄을 넣고 나서는 커밋 메시지 로직이 단 한 번도 같이 수정된 적이 없었다.


계약 하나로는 안 됐다 — 유지조건이 빠졌던 사고

계약 형식을 알고 나서도 한동안은 유지조건 줄을 자주 빼먹었다. “당연히 다른 덴 안 건드리겠지” 하는 안일함 때문이었다. 그러다 실제로 사고가 한 번 더 났다.

/new-post 커맨드의 이미지 프롬프트 작성 규칙 부분만 손보려고 했는데, 다시 보니 이미지 개수 최소 기준 문구까지 같이 바뀌어 있었다. 두 규칙이 같은 섹션 안에 붙어 있어서 Claude 입장에선 “같이 다듬는 게 자연스럽다”고 판단한 거다.

추정 — 요청 자체가 모호했나 다시 읽어봤다. “이미지 프롬프트 규칙만 고쳐줘”라고는 했었다.

소거 — 목표는 명확했다 (프롬프트 규칙). 현재상태도 명확했다 (해당 섹션 지목함). 이 둘은 원인에서 제외.

검증git diff로 실제 변경 범위를 확인해보니 최소 이미지 개수 문구까지 같이 바뀌어 있었다. 유지조건 줄이 아예 없었다는 걸 그제야 깨달았다.

1
2
git diff --stat        # 예상보다 변경된 줄 수가 많은지 먼저 확인
git diff [파일명]       # 실제로 어느 부분이 왜 바뀌었는지 확인

재발 방지책은 단순했다. 요청을 쓰기 전에 “이 요청에서 절대 건드리면 안 되는 게 뭐지?”를 스스로 한 번 물어보고, 그 답을 유지조건 줄에 그대로 옮겨 적는 습관을 들였다. 이 질문 자체가 유지조건을 채우는 트리거가 됐다.

유지조건 누락으로 인접 규칙까지 함께 변경된 git diff 사고 재현


수정할 땐 계약이 아니라 “좁은 수정”이 필요했다

계약 5요소를 익히고 나서도 뭔가 아쉬웠다. 새 기능을 만들 때는 5요소가 딱 맞았는데, 이미 있는 코드를 살짝 손보는 상황에선 매번 목표·현재상태까지 풀어 쓰는 게 과했다. 코드가 이미 있으니 “무엇을 위한 코드인지”는 대부분 자명한데, 그걸 다시 설명하는 느낌이었다.

그러다 알게 된 게 좁은 수정 기법이다. 계약 5요소 중 유지조건과 완료기준·검증방법 세 개를 극단적으로 구체화한, 기존 코드 수정 전용 압축판이었다.

1
2
3
4
5
6
7
5요소 계약 (새 기능)              좁은 수정 (기존 코드)
─────────────────                ─────────────────
목표          ─┐                  (대부분 생략 가능 —
현재상태       │                    이미 있는 코드라 자명)
유지조건      ─┼─→ 압축         →   바꿀 지점
완료기준       │                    바꾸지 않을 지점
검증방법      ─┘                    확인 시나리오

미용실에서 “앞머리 눈썹 위 1cm까지만, 옆뒷머리는 손대지 말고, 자르고 나서 거울로 확인해달라”고 말하는 것과 같은 원리다. 미용사가 헷갈릴 여지가 없다.

작업 계약 5요소가 좁은 수정 3요소로 압축되는 구조


좁은 수정 3요소 실전 적용

세 요소는 바꿀 지점, 바꾸지 않을 지점, 확인 시나리오다. 이걸 실제로 써본 사례가 앞서 나온 커밋 메시지 어투 수정 건이다.

1
2
3
4
5
6
7
8
.claude/commands/deploy.md의 "2단계 — 커밋 메시지 추천" 섹션에서
AskUserQuestion에 넘기는 옵션 텍스트의 어투만 바꿔줘.

- 바꿀 지점: 옵션 label에 들어가는 커밋 메시지 문구 자체
- 바꾸지 않을 지점: 커밋 타입 분류 기준표(post/feat/fix/chore 등),
  3~4단계의 git add/commit/push/PR 로직은 절대 건드리지 마
- 확인 시나리오: 이 파일을 수정한 뒤 diff만 보여줘.
  실제 커밋 메시지를 생성하는 로직 자체는 안 바뀌었는지 확인해줘

“바꿀 지점”을 함수·섹션 단위로 물리적으로 못 박고, “바꾸지 않을 지점”에 인접해서 같이 바뀔 위험이 있는 부분을 콕 집는 게 핵심이었다. 이 요청 이후로는 diff가 딱 예상한 범위 안에서만 나왔다.

헬퍼 함수까지 번진 사고

근데 좁은 수정도 완벽하진 않았다. “이 함수만 고쳐줘”라고 했는데도 그 함수가 호출하는 헬퍼 함수까지 같이 수정된 적이 있다.

원인을 파고들어 보니, 그 함수의 버그가 사실 자기 코드가 아니라 자신이 호출하는 헬퍼 함수 쪽 로직 때문이었다. Claude 입장에선 “이 함수를 고치려면 원인이 있는 헬퍼 함수도 고쳐야 한다”고 판단해서 범위를 넓힌 거였다.

1
2
3
# 함수 하나만 고쳤는데 diff가 예상보다 넓으면 먼저 확인
git diff --stat          # 몇 개 파일/함수가 바뀌었는지 개수부터 확인
git diff [파일명]         # 호출하는 다른 함수까지 번졌는지 확인

이 경험 이후로는 좁은 수정을 요청하기 전에 “이 함수가 다른 함수를 호출하나?”를 먼저 확인하는 습관이 붙었다. 호출 관계가 있으면 그 헬퍼 함수 이름까지 “바꾸지 않을 지점”에 명시적으로 추가한다.

헬퍼 함수 호출 관계 때문에 수정 범위가 예상보다 번진 상황


“완료했습니다”를 못 믿게 된 계기

계약이랑 좁은 수정으로 요청 자체는 꽤 정리됐는데, 여전히 남는 문제가 있었다. Claude가 “완료했습니다”라고 보고하면 일단 믿고 다음 작업으로 넘어갔는데, 가끔 그게 사실이 아니었다.

지난 6월 첫 포스트에서도 이 얘기를 살짝 썼었다. 에러 처리 고쳐달라고 했더니 성공 케이스 응답 형식까지 바뀌어서, 30분 되돌리느라 고생했다고. 그때는 “완료했다는데 왜 안 돼?” 정도로만 생각했는데, 이번에 제대로 파보니 “완료했다”라는 말 자체가 늘 부실한 근거 위에 있었다.

파일이 수정된 것과 그게 실제로 동작하는 건 다른 주장인데, 이 둘을 같은 말처럼 쓰고 있었던 거다.


5계층 검증 쌓아보기

건물 준공 검사가 배관, 전기, 소방, 구조, 마감을 각각 따로 점검하듯, “완료”도 다섯 개 항목으로 쪼개서 확인하기로 했다.

계층별로 뭘 잡아내나

각 계층은 서로 다른 종류의 실패를 잡는다. 앞 계층을 통과 못 하면 뒤 계층은 의미가 없어서, 반드시 이 순서로 쌓아야 한다.

계층 확인 문구 무엇을 잡아내나
① 실행 “실제로 실행한 결과를 보여줘” 문법 오류, 임포트 누락 등 애초에 안 돌아가는 코드
② 시나리오 “정상 케이스와 에러 케이스 각각 실행해줘” 특정 조건에서만 터지는 버그
③ 회귀 “이 수정이 기존 기능에 영향을 주는지 확인해줘” 의도치 않게 망가진 다른 기능
④ 범위 “diff를 보여주고 의도 밖 변경이 있는지 확인해줘” 넓은 수정 문제 (앞서 겪은 사고들)
⑤ 불확실성 공개 “확인 못 한 부분이 있으면 명시해줘” Claude가 자신 없는 부분을 숨기는 것

⑤번이 나머지 넷과 성격이 다르다는 걸 나중에야 알았다. ①~④는 “확인해서 문제없음을 증명”하는 계층이고, ⑤는 “확인 못 한 게 있으면 그 사실 자체를 숨기지 말라”는 계층이다. 실제 서비스 환경에서만 재현되는 버그처럼 로컬에서 도저히 다 확인할 수 없는 경우가 있는데, 그럴 때 “이 부분은 로컬에서 확인 못 했다”는 정직한 보고가 있어야 내가 그 부분만 따로 챙길 수 있었다.

5계층 검증이 순서대로 쌓이는 구조

검증 템플릿 실전 적용

앞서 나온 커밋 메시지 어투 수정 요청 끝에 검증 템플릿을 붙여서 완성해보면 이렇다.

1
2
3
4
5
6
완료로 보고하기 전에 아래를 확인해줘:
1. [실행] 수정한 deploy.md 파일이 마크다운 문법 오류 없이 파싱되는지
2. [시나리오] 커밋 타입이 post/feat/fix/chore 4가지일 때 각각 옵션 문구가 자연스러운지
3. [회귀] 3~4단계(git 로직) 텍스트가 그대로인지
4. [범위] 이 파일 diff에 "2단계" 섹션 밖의 변경이 없는지
5. [불확실성] 실제 AskUserQuestion 렌더링 결과는 직접 못 보니, 문구 검토만 했다는 걸 명시해줘

5번이 특히 실용적이었다. “렌더링 화면은 텍스트만으로 확인 불가능하다”는 한계를 Claude가 스스로 인정하니까, 내가 그 부분만 직접 눈으로 확인하면 된다는 걸 바로 알 수 있었다.

검증 템플릿을 실제 커밋 메시지 수정 요청에 적용한 코드


테스트가 통과했는데도 터진 사고

5계층을 다 거치고도 문제가 생긴 적이 한 번 있다. “기존 테스트가 다 통과했으니 회귀 없음”이라고 보고받았는데, 실제로 확인해보니 문제가 있었다.

추정 — 5계층 중 어디가 부실했나 다시 확인했다.

소거 — 실행·시나리오·범위 계층은 로그로 봤을 때 제대로 확인됐다. 이 셋은 원인에서 제외.

검증 — 회귀 계층에서 “기존 테스트 통과”라고만 했는데, 그 테스트 자체가 이번에 문제가 된 케이스를 애초에 다루고 있지 않았다. “테스트가 통과했다”와 “문제가 없다”는 다른 말인데 같은 걸로 취급한 게 원인이었다.

1
2
3
4
5
❌ "기존 테스트가 다 통과했으니 회귀 없음"
   → 테스트 커버리지 밖의 케이스는 애초에 확인 불가

✅ "기존 테스트 통과 + 이번 문제와 관련된 케이스가
   테스트에 실제로 포함돼 있는지도 같이 확인"

이후로는 회귀 계층을 확인할 때 “테스트가 통과했다”에서 멈추지 않고 “그 테스트가 이번 변경과 관련 있는 부분을 실제로 커버하는가”까지 한 번 더 묻는다. 5계층 중 어느 것도 완벽하지 않을 수 있다는 걸 인정하는 게, 결국 5번 계층(불확실성 공개)과 같은 태도라는 걸 알았다.

기존 테스트 통과에도 커버리지 밖 케이스로 발생한 회귀 사고


세 가지를 하나로 합쳐보니

계약, 좁은 수정, 검증 세 개를 따로 배웠지만 실제로는 하나로 이어서 쓰는 게 맞았다. 새 작업이면 계약 5요소로 시작하고, 이미 있는 코드 수정이면 좁은 수정 3요소로 시작하고, 끝에는 항상 5계층 검증 템플릿을 붙인다.

이 셋을 실무 문화랑 비교해보니 이미 다들 쓰고 있던 원칙이었다. 구글 엔지니어링 가이드의 “한 CL은 한 가지만” 원칙이 좁은 수정이고, 지라 티켓의 Acceptance Criteria가 계약의 완료기준이고, CI 파이프라인의 빌드→테스트→통합테스트 게이트가 5계층 검증이다. 사람 간 협업에서 이미 검증된 형식을 AI 요청에 그대로 옮긴 것뿐이었다.

세팅 전후로 체감한 걸 정리해봤다.

지표 세팅 전 세팅 후
“완료했다는데 안 됨” 재요청 비율 요청 3번 중 1번 이상 10번 중 1번 미만
의도 밖 변경(diff 예상 못한 파일) 발생 절반 이상의 요청에서 발생 유지조건 명시 후 거의 0
요청 작성에 걸리는 시간 5초 (근데 재작업 30분) 1분 (재작업 거의 없음)

요청 작성 시간은 늘었는데 총 소요 시간은 확실히 줄었다. 지난 포스트에서 “에러 처리 고쳤다가 성공 케이스까지 바뀌어서 30분 되돌렸다”고 썼던 그 사고가, 유지조건 한 줄만 있었어도 안 생겼을 일이었다.

세 가지 요청 기법 적용 전후 재작업 비율 변화

파트 2(요청 아키텍처)는 여기서 끝이다. 다음은 파트 3, Hooks랑 Sub-agent 차례다. 이번엔 진짜 미루지 않고 이어서 써볼 생각이다.


참고 자료