코딩 복습용 블로그
안녕하세여! 코딩 복습용 블로그입니다.
전체 글 (420)
[LLM-응용-1] Claude Code 스킬로 "이슈 → 커밋 → PR → 리뷰 → 리뷰 반영" 자동화해 보기
728x90

Claude Code 스킬로 "이슈 → 커밋 → PR → 리뷰 → 리뷰 반영" 자동화해 보기

버그 하나를 고치는 데 필요한 GitHub 작업을 Claude Code 프로젝트 스킬 여섯 개로 나눠 만들고, 실제 저장소에서 처음부터 끝까지 돌려 봤습니다. 만드는 법, 돌리면서 막힌 곳, 그리고 배운 점을 정리합니다.


들어가며: 왜 스킬인가

Claude Code에 "이슈 만들어 줘", "커밋해 줘", "PR 올려 줘"라고 말하면 대체로 잘 해 줍니다. 문제는 매번 결과가 조금씩 다르다는 점입니다.

  • 이번에는 제목에 [Bug]를 붙였는데 다음에는 빠집니다.
  • PR 본문에 관련 이슈 연결을 잊습니다.
  • 확인도 없이 push해 버립니다.

매번 프롬프트에 규칙을 길게 적을 수도 있지만, 그러면 사람이 같은 말을 반복해야 합니다. 스킬은 이 반복을 파일로 고정하는 방법입니다. 한 번 적어 두면 Claude가 상황에 맞을 때 스스로 꺼내 쓰고, /스킬이름으로 직접 부를 수도 있습니다.

이번 실습의 목표는 간단합니다.

작은 API 서버의 성능 버그 하나를 두고, 이슈 등록부터 리뷰 반영까지 전 과정을 스킬로 처리한다.

실습 대상: task-keeper

task-keeper는 할 일(Task)과 태그(Tag)를 다대다로 관리하는 작은 FastAPI 앱입니다. 여기에 일부러 심어 둔 버그가 하나 있습니다.

# app/services/task_service.py
def list_tasks(db: Session) -> list[Task]:
    tasks = db.query(Task).all()
    return tasks
# app/models.py
tags = relationship("Tag", secondary=task_tags, lazy="select")

GET /tasks는 할 일 목록을 한 번에 가져오지만, 응답을 JSON으로 만들 때 할 일마다 task.tags에 접근합니다. 관계가 lazy 로딩이라서 접근할 때마다 태그 조회 쿼리가 하나씩 더 나갑니다. 할 일이 N개면 쿼리는 1 + N번입니다. 전형적인 N+1 문제입니다.


1. 스킬의 기본 구조

스킬은 프로젝트의 .claude/skills/<스킬이름>/SKILL.md 파일 하나로 시작합니다.

.claude/skills/
└── create-issue/
    ├── SKILL.md                     # 무엇을, 어떤 순서로 할지
    └── resources/
        └── issue_templates.md       # 결과물의 모양 (템플릿)

SKILL.md의 맨 위에는 frontmatter가 들어갑니다.

---
name: create-issue
description: 깃헙 이슈를 생성합니다. 사용자가 버그 리포트, 기능 요청, 개선 사항 등을
  이슈로 등록하고 싶을 때 사용합니다. "이슈 만들어줘", "버그 리포트 작성해줘",
  "기능 요청 이슈 올려줘" 등의 요청에 반응합니다.
---

여기서 가장 중요한 것은 description 입니다. Claude는 이 설명을 보고 지금 이 스킬을 쓸지 판단합니다. 그래서 "무엇을 하는지"와 함께 "어떤 말을 들으면 반응하는지" 를 예시로 적어 두는 게 좋습니다.

본문에는 보통 아래 세 가지를 적습니다.

섹션 내용
Goal 이 스킬이 끝나면 무엇이 만들어지는가
Instructions 어떤 명령을 어떤 순서로 실행하는가
Constraints 하면 안 되는 것, 반드시 사람에게 물어야 하는 지점

이번에 만든 스킬 여섯 개

스킬 하는 일 템플릿
create-issue 요청을 분석해 [Bug]/[Feature]/[Improvement] 이슈 생성 issue_templates.md
git-commit <type>(<scope>): <subject> 형식의 한국어 커밋 —
create-pr 브랜치의 모든 커밋을 분석해 PR 생성, 이슈 연결 pr_template.md
review-pr 체크리스트로 PR 리뷰 (품질·보안·성능·테스트) review_checklist.md
apply-pr-review 리뷰 코멘트를 분류해 반영하고 코멘트마다 답글 reply_templates.md
commit-and-pr git-commit과 create-pr을 차례로 호출 두 스킬 것을 사용

create-issue와 git-commit은 미리 준비된 예제이고, 나머지 네 개는 이번에 직접 만들었습니다.


2. 스킬을 만들 때 정한 원칙 네 가지

만들다 보니 스킬마다 반복해서 고민하게 되는 지점이 있었습니다. 다음 네 가지로 정리했습니다.

① 절차와 형식을 분리한다

SKILL.md에는 절차만 적고, 결과물의 모양은 resources/의 템플릿으로 뺐습니다.

4. **템플릿 참조**: `resources/pr_template.md`에서 제목 형식과 본문 템플릿을 읽습니다.

PR 본문에 섹션을 하나 추가하고 싶으면 템플릿 파일만 고치면 됩니다. 절차는 건드리지 않아도 됩니다.

② 스킬끼리는 규칙을 복사하지 말고 호출한다

commit-and-pr을 만들 때, 커밋 규칙과 PR 규칙을 다시 적고 싶은 유혹이 있었습니다. 하지만 그러면 규칙이 두 곳에 생기고, 한쪽만 고치는 순간 어긋납니다. 그래서 이 스킬은 두 스킬을 부르기만 하도록 만들었습니다.

4. **커밋**: `git-commit` 스킬을 호출하여 그 절차대로 커밋합니다.
5. **PR 생성**: 이어서 `create-pr` 스킬을 호출하여 그 절차대로 push하고 PR을 만듭니다.

③ 되돌리기 어려운 일 앞에서 멈춘다

이슈 생성, PR 생성, GitHub에 리뷰 게시, push는 지워도 알림과 기록이 남습니다. 이런 단계 앞에는 반드시 사람의 승인을 받도록 적었습니다.

## Constraints
- 사용자 승인 없이 PR을 생성하지 않습니다.
- 기본 브랜치(`main`)에 직접 push하지 않습니다.
- `git push --force`는 사용자가 명시적으로 요청한 경우에만 사용합니다.

반대로 로컬 파일을 읽거나 분석하는 단계에는 승인을 두지 않았습니다. 모든 단계에서 물어보면 흐름이 끊기기 때문입니다.

④ 확인하지 못한 것을 적을 자리를 만든다

에이전트가 가장 위험할 때는 확인하지 않은 것을 확인한 것처럼 말할 때입니다. 그래서 템플릿 곳곳에 "모르면 모른다고 쓰는 자리"를 만들었습니다.

  • 이슈 템플릿: 모르는 정보는 TODO: 추가 정보 필요
  • PR 템플릿: 테스트 항목은 체크박스. 실행하지 않았으면 체크하지 않음
  • 리뷰: 추측과 확인한 사실을 구분하고, 확인 못 한 것은 "확인 필요"
  • 리뷰 반영: "실행하지 않은 테스트를 통과했다고 쓰지 않는다"

이 원칙은 실습 중에 실제로 크게 도움이 되었습니다. 뒤에서 다시 나옵니다.


3. 실제로 돌려 보기

Step 1. /create-issue: 한 줄짜리 요청을 이슈로

/create-issue 할 일 목록 조회가 느림. 할 일이 많을 때 GET /tasks 응답이 오래 걸림

흥미로웠던 점은, Claude가 이 문장만으로 이슈를 쓰지 않았다는 것입니다. 먼저 GET /tasks 코드를 따라가 읽고, list_tasks()와 lazy="select"에서 N+1 원인을 찾은 뒤 이슈를 썼습니다.

생성된 이슈는 다음과 같습니다.

  • 제목: [Bug] 할 일이 많을 때 GET /tasks 응답이 느림 (태그 N+1 쿼리)
  • 본문: 원인, 재현 방법(SQLALCHEMY_ECHO=1로 쿼리 로그 보기), 기대 동작과 실제 동작, 수정 방향
  • 측정값처럼 모르는 정보는 TODO: 추가 정보 필요로 남김

막힌 곳

문제 해결
needs-triage 라벨이 저장소에 없음 스킬에 적힌 대로 gh label create로 먼저 생성
the repository has disabled issues 저장소 설정을 바꾸는 일이라 Claude가 멈추고 물어봄 → 승인 후 gh repo edit --enable-issues

두 번째 경우는 스킬에 적혀 있지 않은 상황이었습니다. 그런데도 "되돌리기 어려운 일은 묻는다"는 원칙대로 멈춘 것이 좋았습니다.

Step 2. /git-commit: 성격별로 커밋 나누기

스킬 파일과 문서를 커밋할 때, Claude는 이를 chore(skills)와 docs(wiki) 두 커밋으로 나눴습니다.

여기서 Author identity unknown 오류가 났습니다. 새 PC라 git 사용자 정보가 설정되어 있지 않았던 것입니다. 전역 설정을 함부로 바꾸지 않고, 해당 커밋에만 작성자를 지정했습니다.

git -c user.name=<이름> -c user.email=<이메일> commit -m "..."

Step 3. /create-pr: 요청과 실제 상태가 다를 때

/create-pr 방금 커밋한 n+1 수정을 pr로 올려라

이번 실습에서 가장 인상적인 장면이었습니다. create-pr 스킬에는 이런 단계가 있습니다.

2. **변경사항 분석**: base 브랜치 대비 이 브랜치에 들어간 내용을 모두 읽습니다.
   - 최신 커밋 하나만 보지 말고, 브랜치의 **모든 커밋**을 기준으로 작성합니다.

이 단계를 실행해 보니, 브랜치에 N+1 수정 커밋이 아예 없었습니다. 앞에서 커밋한 것은 스킬 파일과 문서뿐이었습니다. 요청을 그대로 따랐다면 버그 수정이 없는 "fix" PR이 올라갔을 것입니다.

Claude는 이 사실을 먼저 알리고, 수정을 만든 뒤 PR을 올렸습니다.

from sqlalchemy.orm import Session, selectinload

def list_tasks(db: Session) -> list[Task]:
    # 태그를 IN 쿼리 한 번으로 미리 불러와 N+1을 막습니다.
    return db.query(Task).options(selectinload(Task.tags)).all()

selectinload는 할 일 목록을 가져온 뒤, 그 할 일들의 태그를 WHERE task_id IN (...) 쿼리 한 번으로 몰아서 가져옵니다. 할 일이 몇 개든 쿼리는 2번으로 고정됩니다.

회귀 테스트도 하나 추가했습니다. SQLAlchemy 이벤트로 실제 실행된 쿼리 수를 세는 방식입니다.

def test_list_tasks_query_count_is_constant(client, db_session):
    for i in range(5):
        client.post("/tasks", json={"title": f"할 일 {i}", "tags": [f"태그{i}"]})

    queries = []
    engine = db_session.get_bind()
    listener = lambda *args: queries.append(args[2])
    event.listen(engine, "before_cursor_execute", listener)
    try:
        res = client.get("/tasks")
    finally:
        event.remove(engine, "before_cursor_execute", listener)

    assert len(queries) == 2  # 할 일 1회 + 태그 1회

그런데 테스트가 안 돌았습니다

uv run pytest를 실행하자마자 pydantic 내부에서 AssertionError가 났습니다. 원인은 로컬 Python이 3.14.0rc2, 즉 정식 출시 전 버전이었기 때문입니다. pydantic이 쓰는 typing 내부 API가 rc 버전과 맞지 않았습니다. 수정 전 코드에서도 똑같이 실패했으니 이번 변경과는 관계가 없었습니다.

정식판 Python을 받으려 했지만 실습 환경에서 다운로드가 막혀 있었습니다. 그래서 pydantic을 거치지 않고 SQLAlchemy만으로 쿼리 수를 직접 세 봤습니다.

할 일 50개일 때 쿼리 수
수정 전 51회
수정 후 2회

그리고 pytest를 돌리지 못했다는 사실은 PR 본문의 테스트 항목에 체크하지 않은 채로 남겼습니다. 원칙 ④가 쓰인 순간입니다.

## 테스트
- [x] SQLAlchemy로 쿼리 수 측정: 할 일 50개 기준 51회 → 2회
- [ ] `uv run pytest` — 로컬 Python 3.14.0rc2에서 pydantic 임포트 실패로 미실행

PR 본문에 Closes #1을 넣어 이슈와 연결했습니다. 이렇게 하면 PR이 머지될 때 이슈가 자동으로 닫힙니다.

Step 4. /review-pr: 체크리스트로 리뷰하기

review-pr 스킬은 gh pr view --json으로 메타데이터를, gh pr diff로 변경 내용을 읽은 뒤 체크리스트를 하나씩 적용합니다. 체크리스트 일부를 소개하면 다음과 같습니다.

## 3. 성능
- [ ] N+1 쿼리가 없는가 (관계 접근 시 `selectinload`, `joinedload` 사용)
- [ ] 반복문 안에서 DB 쿼리나 네트워크 호출을 하지 않는가
- [ ] 목록 조회에 페이지네이션이나 개수 제한이 필요하지 않은가

## 4. 테스트
- [ ] 버그 수정이라면, 수정 전 코드에서 실패하는 테스트인가

지적 사항은 심각도로 나눕니다.

  • 🔴 Blocker: 머지 전에 반드시 수정 (버그, 보안 취약점)
  • 🟡 Major: 강하게 권장 (성능, 테스트 누락)
  • 🟢 Minor: 선택 (가독성, 스타일)

방금 만든 PR을 리뷰해 보니, Blocker는 없었지만 Major가 세 개 나왔습니다.

심각도 지적
🟡 새 테스트를 한 번도 실행하지 못했다
🟡 테스트가 같은 DB 세션을 공유해서, 수정 전 코드로 돌려도 1+N이 아니라 1+(N-1)로 셀 수 있다 → 측정 직전에 db_session.expire_all() 필요
🟡 PR에 버그 수정과 관계없는 커밋(스킬, 문서)이 섞여 있다

두 번째 지적이 특히 좋았습니다. 테스트가 통과하는지만 볼 게 아니라, "이 테스트가 정말 버그를 잡아내는가" 를 따진 것입니다. 앞선 POST 요청에서 마지막 할 일의 태그가 세션에 이미 올라와 있으므로, 측정값이 버그 모양과 정확히 맞지 않는다는 분석입니다.

참고로 자기 PR에는 GitHub에서 Approve나 Request changes를 할 수 없습니다. 그래서 스킬에 --comment로 남기라고 적어 두었습니다.

Step 5. /apply-pr-review: 리뷰를 무조건 따르지 않기

리뷰를 반영하는 스킬에서 가장 신경 쓴 부분은 이것입니다.

리뷰어 의견을 무조건 따르지 않습니다. 지적이 맞는지 코드를 직접 읽고 확인합니다.

지적마다 네 가지 중 하나로 분류합니다.

분류 의미 답글
✅ 반영 타당하고 이 PR 범위 안 "반영했습니다. (커밋 해시)"
💬 질문 답변 코드 변경 없이 설명 필요 답변
🙅 반영 안 함 근거가 틀렸거나 범위 밖 이유와 대안
⏸️ 보류 사람이 결정해야 함 결정 후 다시 답변

GitHub의 리뷰 코멘트는 종류가 세 가지라서 가져오는 방법도 다릅니다. 이 부분은 미리 알아 두면 좋습니다.

종류 가져오기 답하기
코드 줄에 단 인라인 코멘트 GraphQL reviewThreads (해결 여부 포함) gh api .../pulls/<n>/comments/<id>/replies
리뷰 본문 gh pr view --json reviews 답글 기능 없음 → gh pr comment로 종합
일반 PR 코멘트 gh pr view --json comments 답글 기능 없음 → gh pr comment로 종합

실제로 돌려 보니 PR에 코멘트가 하나도 없었습니다. 앞 단계의 리뷰가 로컬 파일로만 있었기 때문입니다. 스킬은 그대로 끝내지 않고 "리뷰를 먼저 게시할까요?"라고 물었고, 게시한 뒤 지적 7건을 분류했습니다.

  • ✅ 반영 4건: expire_all() 추가, import 위치 정리, README와 주석 갱신
  • 💬 답변 1건: 테스트 미실행. 이 환경에서는 해결할 수 없음
  • 🙅 반영 안 함 2건: PR 분리(force push 필요), 페이지네이션(이번 PR 범위 밖)

PR 분리를 "반영 안 함"으로 둔 이유가 좋았습니다. 리뷰 지적은 맞지만, 고치려면 브랜치 이력을 다시 쓰고 force push를 해야 합니다. 스킬의 제약("force push는 명시적 요청이 있을 때만")과 부딪히므로, 사람에게 판단을 넘긴 것입니다.


4. 번외: Claude Code가 아예 안 켜졌던 이야기

사실 이 실습은 Claude Code가 실행되지 않는 문제로 시작했습니다. Windows에서 npm으로 Claude Code를 쓴다면 겪을 수 있는 일이라 함께 적어 둡니다.

$ claude
/c/Users/admin/.npm-global/claude: line 12: .../claude-code/bin/claude.exe: No such file or directory

npm uninstall -g, npm install -g로 다시 설치해도 그대로였습니다.

원인: claude가 두 군데 설치되어 있었다

위치 버전 상태
~/.npm-global (예전 npm 설치 위치) 2.1.291 망가짐. 셸이 PATH에서 이걸 먼저 찾음
C:\Program Files\nodejs (nvm, 현재 npm 설치 위치) 2.1.319 정상

망가지는 과정은 이랬습니다.

  1. ~/.npm-global의 claude가 자동 업데이트를 시작합니다.
  2. Windows는 실행 중인 exe를 덮어쓸 수 없어서, 업데이터가 기존 파일 이름을 claude.exe.old.*로 바꿔 둡니다.
  3. 새 버전은 npm install -g로 설치되는데, 이 명령은 현재 npm 설정의 위치(nvm 쪽) 에 설치합니다.
  4. 결국 ~/.npm-global에는 .old 파일만 남고, 셸은 계속 거기서 claude를 찾다가 실패합니다.

사용자가 직접 한 재설치도 모두 nvm 쪽에만 설치되었기 때문에, 망가진 쪽은 한 번도 고쳐지지 않았던 것입니다.

헛짚었던 것들

  • 처음에는 npm의 ignore-scripts 설정 때문에 설치 스크립트가 안 돈 줄 알았습니다. 확인해 보니 처음부터 false였습니다.
  • exe를 수동으로 다시 만드는 임시 복구를 했지만, 16분 뒤 자동 업데이트가 돌면서 똑같이 다시 망가졌습니다.

로그를 끝까지 읽고 나서야 "설치 위치가 두 군데"라는 근본 원인이 보였습니다.

해결

오래된 설치본을 지우는 대신 백업 폴더로 옮겼습니다. 혹시 문제가 생기면 되돌릴 수 있게 하기 위해서입니다.

mkdir -p ~/.npm-global/_claude-backup
mv ~/.npm-global/node_modules/@anthropic-ai/claude-code \
   ~/.npm-global/claude ~/.npm-global/claude.cmd ~/.npm-global/claude.ps1 \
   ~/.npm-global/_claude-backup/
hash -r
$ which -a claude
/c/Program Files/nodejs/claude
$ claude --version
2.1.319 (Claude Code)

💡 hash -r은 Git Bash(bash)용 명령입니다. bash는 한 번 찾은 실행 파일의 경로를 기억해 두기 때문에, 파일을 옮긴 뒤에는 이 명령으로 기억을 지워야 합니다. PowerShell에는 hash 명령이 없고 경로를 기억해 두지도 않으므로 필요 없습니다. 창을 새로 열어도 됩니다.

비슷한 증상을 겪는다면 which -a claude(bash) 또는 where.exe claude(PowerShell)로 claude가 몇 군데 있는지부터 확인해 보세요.


5. 배운 점

스킬은 "요청"보다 "실제 상태"를 먼저 확인하게 만들어야 한다

이번 실습에서 요청에 담긴 전제가 틀린 경우가 세 번 있었습니다.

  • "방금 커밋한 수정" → 브랜치에 수정 커밋이 없었음
  • "리뷰 반영해 줘" → PR에 리뷰 코멘트가 없었음
  • "재설치했는데 안 됨" → 재설치가 엉뚱한 위치에 되고 있었음

세 경우 모두 상태를 먼저 확인하는 단계 덕분에 잡아냈습니다. 스킬의 첫 단계는 거의 항상 "현재 상태 확인"이어야 한다고 느꼈습니다.

스킬이 모든 예외를 다룰 수는 없다. 대신 "멈추고 알린다"를 기본으로

Issues 기능 꺼짐, git 사용자 정보 없음, rc 버전 Python은 모두 스킬에 적혀 있지 않은 상황이었습니다. 이런 일을 하나하나 스킬에 적을 수는 없습니다. 대신 "되돌리기 어려운 일 앞에서는 멈춘다" 는 원칙을 Constraints에 넣어 두니, 처음 보는 상황에서도 안전하게 행동했습니다.

검증하지 못한 것을 기록할 자리가 신뢰를 만든다

pytest를 돌리지 못한 사실이 PR 본문, 리뷰, 리뷰 반영 계획에 일관되게 남았습니다. 템플릿에 체크박스와 "확인 필요" 자리를 만들어 둔 덕분입니다. 이게 없었다면 테스트가 통과한 것처럼 보였을 것입니다.

에이전트에게 다른 저장소에 올리라고 할 때는 경로를 명확히

실습 결과를 별도 저장소에 올리는 과정에서, "올려 줘"라고만 했더니 Claude가 폴더 위치를 추측해서 두 번이나 엉뚱한 곳에 올렸습니다. 사람에게는 당연한 "기존 폴더들과 같은 위치"가 에이전트에게는 당연하지 않았습니다. 대상 경로는 처음부터 명시하거나, 올리기 전에 확인하게 하는 게 좋습니다.

스킬 조합은 복사가 아니라 호출로

commit-and-pr을 두 스킬의 호출로만 만드니, 커밋 규칙을 바꿀 때 git-commit 하나만 고치면 됩니다. 코드에서 함수를 재사용하는 것과 같은 원리입니다.


마치며

단계 결과
이슈 task-keeper#1
수정 selectinload 한 줄, 쿼리 51회 → 2회
PR task-keeper#2, 이슈 자동 연결
리뷰 Blocker 0 / Major 3 / Minor 4
리뷰 반영 7건 분류 (반영 4, 답변 1, 반영 안 함 2)

스킬을 만들기 전에는 "Claude에게 일을 시킨다"고 생각했다면, 만들고 나서는 "Claude가 일하는 방식을 설계한다" 는 느낌에 가까웠습니다. 무엇을 할지보다 어디서 멈출지, 무엇을 기록할지 를 정하는 데 더 많은 시간을 썼고, 실제로 그 부분이 가장 큰 차이를 만들었습니다.

아직 남은 것

  • 정식판 Python에서 pytest 실행해 보기
  • 리뷰 반영을 끝까지 실행하고, 인라인 코멘트 답글·스레드 해결 경로 검증하기
  • commit-and-pr 실제로 돌려 보기

전체 스킬 파일은 Harness_Engineering/skills-example/task-keeper에서 볼 수 있습니다.

728x90
  Comments,     Trackbacks
[TIL]폐쇄망 K8S 이미지 패치 정리-③ 이미지 교체·재배포 검증 및 Rollback
728x90

개요

앞선 글에서 아래 과정을 정리했습니다.

글 내용
정리-1 1. 보안 원격접속(SSH) 및 작업 노드 사전 점검 · 2. SFTP 기반 이미지 아티팩트 반입
정리-2 3. Deployment 매니페스트 백업 · 4. 이미지 적재·재태깅

이 글은 마지막 두 과정을 정리합니다.

  1. 이미지 교체 및 Rolling 재배포 검증
  2. 매니페스트 기반 Rollback

아래 이름은 모두 예시입니다. 실제 값은 승인된 작업 계획서에서 확인하세요.

항목 예시 값
Namespace cmp
Deployment sample-api, sample-web
새 이미지 (정리-2에서 push) registry.example.local:50000/myproject/<이미지>:<새 tag>
백업 파일 (정리-2에서 생성) sample-api-bak.yaml, sample-web-bak.yaml

5. 이미지 교체 및 Rolling 재배포 검증

개념

Deployment의 spec.template.spec.containers[].image 값을 바꾸면 그 자체로 Rolling Update가 시작됩니다. K8S가 새 이미지로 Pod를 하나씩 띄우고, 새 Pod가 Ready가 되면 기존 Pod를 하나씩 내립니다. 그래서 서비스를 멈추지 않고 이미지를 바꿀 수 있습니다.

kubectl rollout restart는 이미지 변경 없이 Pod만 새로 띄울 때 쓰는 명령입니다. 이미지를 바꾼 뒤에는 따로 실행하지 않아도 됩니다. 실행하면 재배포가 한 번 더 일어날 뿐 해는 없지만, Rollback 리비전이 하나 늘어 6번에서 헷갈릴 수 있습니다.

실제 작업 흐름

순서 행동 확인할 결과
1 Deployment의 image를 새 tag로 변경 edited 또는 image updated
2 롤아웃 완료 대기 successfully rolled out
3 Pod 상태·이미지 확인 새 Pod Running, READY 가득 참, image가 새 tag
4 애플리케이션 로그·서비스 확인 오류 로그 없음, 기능 정상
5 이상 시 6번 진행 -

목적: 대상 Deployment를 새 이미지로 교체하고 정상 기동을 확인합니다.
완료 기준: 롤아웃이 완료되고, 새 Pod가 모두 Ready이며 재시작이 늘지 않고, 서비스 확인까지 끝나면 패치 완료입니다.

5.1 이미지 교체

방법 A — kubectl edit

kubectl edit -n cmp deployment.apps/sample-api
kubectl edit -n cmp deployment.apps/sample-web

편집기에서 image: 줄의 태그만 <새 tag>로 바꾸고 저장·종료합니다. 다른 줄은 건드리지 않습니다.

방법 B — kubectl set image (오타 위험이 적음)

# 컨테이너 이름은 백업 파일의 containers[].name 값
kubectl set image -n cmp deployment/sample-api \
  <컨테이너명>=registry.example.local:50000/myproject/sample-api:<새 tag>
kubectl set image -n cmp deployment/sample-web \
  <컨테이너명>=registry.example.local:50000/myproject/sample-web:<새 tag>

(선택) 이미지 변경 없이 재기동만 필요할 때:

kubectl rollout restart -n cmp deployment.apps/sample-api
kubectl rollout restart -n cmp deployment.apps/sample-web

5.2 롤아웃 검증

kubectl rollout status -n cmp deployment/sample-api --timeout=300s
kubectl rollout status -n cmp deployment/sample-web --timeout=300s
kubectl get pod -n cmp -o wide -w          # Ctrl+C로 종료
kubectl get deploy -n cmp sample-api sample-web \
  -o custom-columns=NAME:.metadata.name,IMAGE:.spec.template.spec.containers[*].image
kubectl logs -n cmp deploy/sample-api --tail=100
kubectl logs -n cmp deploy/sample-web --tail=100

watch -n 0.1 kubectl get pod ...처럼 0.1초 간격으로 조회하면 API 서버에 부담이 됩니다. -w(변경 시에만 출력)나 watch -n 2를 씁니다.

5.3 상태별 판단

Pod 상태 의미 조치
ContainerCreating → Running 정상 진행 대기
ImagePullBackOff / ErrImagePull 레지스트리 주소·태그 오류 또는 push 누락 kubectl describe pod의 Events 확인 → 정리-2의 4번 재확인
CrashLoopBackOff 새 이미지가 기동 직후 종료 kubectl logs --previous로 원인 확인 → 6번
Running인데 READY 0/1 readiness probe 실패 로그·설정 확인, 지속되면 6번
롤아웃이 --timeout 안에 안 끝남 새 Pod가 Ready가 되지 못함 위 상태 중 무엇인지 확인 → 6번

Rolling Update 중에는 기존 Pod가 남아 있으므로, 새 Pod가 실패해도 서비스는 기존 Pod로 계속 응답합니다. 서두르지 말고 원인을 확인한 뒤 되돌립니다.


6. 매니페스트 기반 Rollback

개념

새 이미지에 문제가 있으면 기존 이미지로 되돌립니다. 방법은 두 가지입니다.

방법 장점 주의
A. 백업 YAML 적용 정리-2의 3번에서 저장한 정의 그대로 복구 백업의 resourceVersion 때문에 충돌로 거부될 수 있음 → 6.1
B. kubectl rollout undo 명령 한 줄로 직전 ReplicaSet 복구 5번에서 edit와 restart를 둘 다 했다면 "직전"이 restart 시점일 수 있음 → rollout history로 리비전 지정

실제 작업 흐름

순서 행동 확인할 결과
1 Rollback 결정 (담당자 승인) -
2 백업 YAML 적용 또는 rollout undo configured / rolled back
3 롤아웃 완료 대기 successfully rolled out
4 Pod 상태·이미지 확인 image가 <기존 tag>, Pod 정상

목적: 패치 실패 시 정리-2에서 기록한 정상 상태로 되돌립니다.
완료 기준: Deployment 이미지가 기존 태그로 돌아오고, Pod 상태가 패치 전 기록과 같아지면 복구 완료입니다.

6.1 방법 A — 백업 YAML 적용

kubectl apply -n cmp -f sample-api-bak.yaml
kubectl apply -n cmp -f sample-web-bak.yaml

the object has been modified; please apply your changes to the latest version 오류가 나면 백업 파일의 resourceVersion 줄을 지우고 다시 적용합니다.

sed -i '/^  resourceVersion:/d' sample-api-bak.yaml sample-web-bak.yaml
kubectl apply -n cmp -f sample-api-bak.yaml
kubectl apply -n cmp -f sample-web-bak.yaml

6.2 방법 B — rollout undo

kubectl rollout history -n cmp deployment/sample-api
kubectl rollout undo -n cmp deployment/sample-api                       # 직전으로
kubectl rollout undo -n cmp deployment/sample-api --to-revision=<번호>   # 지정 리비전으로
# sample-web도 동일

rollout history --revision=<번호>로 각 리비전의 이미지를 확인한 뒤 고르면 안전합니다.

6.3 복구 확인

kubectl rollout status -n cmp deployment/sample-api --timeout=300s
kubectl rollout status -n cmp deployment/sample-web --timeout=300s
kubectl get pod -n cmp -o wide
kubectl get deploy -n cmp sample-api sample-web \
  -o custom-columns=NAME:.metadata.name,IMAGE:.spec.template.spec.containers[*].image

image가 정리-2에서 기록한 <기존 tag>이고, Pod 수·상태가 패치 전과 같으면 복구 완료입니다.


전체 작업 체크리스트 (정리-1 ~ 3)

# 과정 확인
1 보안 원격접속(SSH) 및 작업 노드 사전 점검 SSH 접속, 점검 10항목 정상
2 SFTP 기반 이미지 아티팩트 반입 업로드, 서버 해시 = 원본 해시
3 Deployment 매니페스트 백업 상태 기록, 백업 YAML의 image = 기존 tag
4 이미지 적재·재태깅 load → tag → push → pull 오류 없음
5 이미지 교체 및 Rolling 재배포 검증 rollout 완료, Pod Ready, 로그·서비스 정상
6 (실패 시) 매니페스트 기반 Rollback image = 기존 tag, Pod 상태 원복

참고

728x90
  Comments,     Trackbacks
[TIL]폐쇄망 K8S 이미지 패치 정리-② 매니페스트 백업 및 이미지 적재·재태깅
728x90

개요

정리-1에서 아래 두 과정을 정리했습니다.

  1. 보안 원격접속(SSH) 및 작업 노드 사전 점검
  2. SFTP 기반 이미지 아티팩트 반입

이 글은 이어지는 두 과정을 정리합니다.

  1. Deployment 매니페스트 백업
  2. 이미지 적재·재태깅

아래 이름은 모두 예시입니다. 실제 값은 승인된 작업 계획서에서 확인하세요.

항목 예시 값
Namespace cmp
Deployment sample-api, sample-web
반입한 tar sample-api.tar, sample-web.tar
tar 안의 이미지 이름 registry.example.local:55000/myproject/<이미지>:<기존 tag>
클러스터가 pull하는 레지스트리 registry.example.local:50000/myproject/<이미지>:<새 tag>

3. Deployment 매니페스트 백업

개념

Deployment는 "이 이미지로 Pod를 N개 유지하라"는 선언입니다. 이미지를 바꾸면 K8S가 새 Pod를 하나씩 띄우고 기존 Pod를 하나씩 내리는 Rolling Update를 수행합니다.

이미지를 바꾸기 전에 두 가지를 남깁니다.

  • 현재 상태 기록: Pod 수, 상태, 재시작 횟수, 배치 노드. 패치 전부터 비정상이었다면 패치 탓과 구분할 수 없으므로 먼저 확인합니다.
  • 매니페스트 백업: 현재 Deployment 정의를 YAML 파일로 저장합니다. 새 이미지에 문제가 생겼을 때 되돌릴 원본입니다.

실제 작업 흐름

순서 행동 확인할 결과
1 Namespace 전체 Pod 상태 확인 대상 Pod 모두 Running, READY 가득 참
2 대상 Deployment·ReplicaSet·Pod 필터링 대상 리소스 확인
3 현재 이미지 태그 기록 기존 tag 확보 (4번에서 사용)
4 Deployment YAML 백업 *-bak.yaml 파일 생성
5 백업 파일 확인 image:에 기존 태그 표시

목적: 패치 전 정상 상태를 기록하고 원복할 수 있는 매니페스트를 확보합니다.
완료 기준: 대상 Pod가 정상이고, 백업 파일 두 개의 image:가 기존 태그와 같으면 4번으로 넘어갑니다.

3.1 현재 상태 확인

kubectl get pod -n cmp -o wide
kubectl get all -n cmp | grep -iE 'sample-api|sample-web'
  • STATUS가 Running, READY가 1/1처럼 가득 차 있는지 봅니다.
  • 이미 CrashLoopBackOff이거나 RESTARTS가 늘고 있다면 진행하지 말고 담당자에게 확인합니다.

3.2 현재 이미지 태그 기록

kubectl get deploy -n cmp sample-api sample-web \
  -o custom-columns=NAME:.metadata.name,IMAGE:.spec.template.spec.containers[*].image

출력된 <기존 tag>와 레지스트리 주소를 작업 기록에 적어 둡니다. 4.2 재태깅에서 대상 주소를 맞출 때 씁니다.

3.3 매니페스트 백업

kubectl get -n cmp deployment.apps/sample-api -o yaml > sample-api-bak.yaml
kubectl get -n cmp deployment.apps/sample-web -o yaml > sample-web-bak.yaml

3.4 백업 확인

ls -l *-bak.yaml
grep 'image:' *-bak.yaml

파일이 비어 있지 않고 image:에 3.2에서 기록한 태그가 보이면 백업 완료입니다.

3.5 백업 파일을 쓸 때 주의

나중에 kubectl apply -n cmp -f sample-api-bak.yaml로 되돌릴 때 the object has been modified 오류가 날 수 있습니다. kubectl get -o yaml로 받은 파일에는 저장 당시의 resourceVersion이 들어 있어, 그사이 Deployment가 바뀌면 충돌로 거부되기 때문입니다. 이때는 그 줄을 지우고 다시 적용합니다.

sed -i '/^  resourceVersion:/d' sample-api-bak.yaml sample-web-bak.yaml

4. 이미지 적재·재태깅

개념

폐쇄망 클러스터의 노드는 사내 레지스트리에서 이미지를 pull합니다. 작업 노드에 tar만 올려 두면 다른 노드는 그 이미지를 볼 수 없으므로, 적재 후 레지스트리에 올려야 합니다.

명령 하는 일
nerdctl load tar를 풀어 작업 노드의 로컬 이미지로 등록
nerdctl tag 같은 이미지에 클러스터가 pull할 주소·새 태그로 이름을 하나 더 붙임 (복사 아님)
nerdctl push 그 이름으로 레지스트리에 업로드
nerdctl pull 레지스트리에서 다시 받아 업로드가 정상인지 확인

왜 재태깅하는가: tar 안에는 빌드할 때 붙인 이름(예: :55000/...)이 그대로 들어 있습니다. 클러스터는 다른 주소(예: :50000/...)에서 이미지를 받으므로, 그 주소로 이름을 다시 붙여 올려야 합니다.

왜 새 태그인가: 기존 태그를 덮어쓰면 노드에 남은 캐시 때문에 일부 Pod가 옛 이미지로 뜰 수 있고, 되돌릴 때 어느 것이 옛 이미지인지 구분할 수 없습니다.

실제 작업 흐름

순서 행동 확인할 결과
1 tar 적재 Loaded image: ... 출력
2 적재된 이미지 이름·태그 확인 :55000/...:<기존 tag> 표시
3 레지스트리 주소·새 태그로 tag :50000/...:<새 tag> 추가 표시
4 대상 주소 대조 3.2에서 기록한 Deployment 주소와 일치
5 레지스트리에 push 오류 없이 종료
6 레지스트리에서 pull 오류 없이 종료

목적: 반입한 이미지를 클러스터 전 노드가 받을 수 있는 레지스트리에 새 태그로 올립니다.
완료 기준: push·pull이 오류 없이 끝나면 이미지 교체(재배포)를 진행할 준비가 된 것입니다.

4.1 이미지 적재

nerdctl load -i sample-api.tar
nerdctl load -i sample-web.tar
nerdctl images | grep -iE 'sample-api|sample-web'

Loaded image: 줄에 나온 이름이 다음 tag 명령의 원본 이름입니다. 작업 계획서와 다르면 출력된 이름을 씁니다.

4.2 재태깅

nerdctl tag registry.example.local:55000/myproject/sample-api:<기존 tag> \
            registry.example.local:50000/myproject/sample-api:<새 tag>
nerdctl tag registry.example.local:55000/myproject/sample-web:<기존 tag> \
            registry.example.local:50000/myproject/sample-web:<새 tag>
nerdctl images | grep -iE 'sample-api|sample-web'

⚠️ 대상 주소는 3.2에서 기록한 Deployment의 현재 image: 레지스트리 주소와 같아야 합니다. 다르면 이후 재배포 때 Pod가 ImagePullBackOff에 빠집니다.

4.3 레지스트리 업로드 및 확인

nerdctl push registry.example.local:50000/myproject/sample-api:<새 tag>
nerdctl push registry.example.local:50000/myproject/sample-web:<새 tag>

nerdctl pull registry.example.local:50000/myproject/sample-api:<새 tag>
nerdctl pull registry.example.local:50000/myproject/sample-web:<새 tag>

4.4 오류가 날 때

증상 확인할 것
server gave HTTP response to HTTPS client HTTP 레지스트리입니다. 운영 절차에 따라 --insecure-registry 사용 여부를 확인합니다.
x509: certificate signed by unknown authority 사내 CA 인증서가 노드에 등록되어 있는지 확인합니다. 임의로 검증을 끄지 않습니다.
unauthorized / denied nerdctl login 상태와 push 권한을 확인합니다.
no space left on device df -h로 확인합니다. 정리-1의 "tar 크기 2배" 기준을 다시 봅니다.

다음 글

적재·재태깅까지 끝나면 Deployment의 이미지를 새 태그로 바꾸고(kubectl edit / kubectl set image), kubectl rollout status로 재배포를 검증합니다. 실패하면 3번에서 만든 백업 YAML로 되돌립니다. 이 과정은 다음 글에서 정리합니다.

참고

728x90
  Comments,     Trackbacks
인프라 개념 정리: 보안과 자동화 정리
728x90

[TIL] 보안과 자동화 정리

인프라 보안과 IaC를 정리하며

개발하다 보면 서버가 느려지거나, 외부에서 비정상적인 요청이 들어오거나, 설정을 바꾼 뒤 서비스가 달라지는 상황을 마주한다. 이때 인프라 보안과 운영 자동화의 기본 개념을 알고 있으면 문제의 원인을 더 차분하게 좁힐 수 있다.

이번 글에서는 인프라에서 자주 만나는 보안 사고와 취약점 대응 방법을 먼저 정리하고, 이어서 인프라를 코드로 관리하는 IaC의 계층과 운영 원칙을 살펴본다.

1. 보안 사고를 보는 기본 관점

보안 사고는 한 가지 기술 결함만으로 발생하지 않는다. 취약한 소프트웨어, 잘못된 설정, 탈취된 계정, 감시 공백이 겹치면서 사고 가능성과 피해가 커진다.

위협, 취약점, 위험

  • 위협: 피해를 일으킬 수 있는 행위자나 사건
  • 취약점: 위협이 악용할 수 있는 약점
  • 위험: 취약점이 악용될 가능성과 예상 피해를 함께 고려한 판단

예를 들어 외부에 공개된 오래된 서비스는 취약점이 될 수 있다. 이를 노리는 공격은 위협이고, 서비스 중단이나 정보 유출이 일어날 가능성은 위험으로 본다.

사고가 반복되는 원인

  • 패치 누락, 공개 범위 오설정, 과도한 권한처럼 사람과 절차에서 생기는 실수
  • 이미 공개된 취약점을 방치하는 문제
  • 단일 장애 지점이나 과도한 신뢰 관계처럼 설계가 피해를 키우는 경우
  • 담당자의 기억과 수동 확인에만 의존해 운영의 일관성이 무너지는 경우

제로데이 취약점은 수정 패치가 제공되기 전에 알려졌거나 악용되는 취약점을 말한다. 패치 적용을 놓친 알려진 취약점과는 구분해야 한다. 제로데이 대응은 패치만 기다리는 데 그치지 않고, 노출 범위를 줄이고 접근을 제한하며 탐지를 강화하는 방식으로 위험을 낮춘다.

2. UDP와 서비스 거부 공격

서비스 거부(DoS) 공격은 네트워크나 시스템 자원을 소진시켜 서비스를 사용할 수 없게 만드는 공격이다. 여러 감염 기기가 동시에 공격하면 분산 서비스 거부(DDoS)라고 한다.

UDP는 연결 설정 절차가 없는 데이터그램 프로토콜이다. 이 특성은 출발지 주소를 위조한 요청과 서버 응답을 이용하는 반사 공격에 악용될 수 있다. UDP 자체가 공격을 뜻하는 것은 아니며, 서버의 역할과 설정에 따라 실제 동작은 달라진다.

반사와 증폭 공격 흐름

  1. 공격자는 요청의 출발지 주소를 피해자 주소로 위조해 여러 UDP 서버에 보낸다.
  2. 요청을 받은 서버가 응답하면 그 응답이 피해자에게 전달된다. 이를 반사 공격이라고 한다.
  3. 요청보다 응답 데이터가 더 큰 경우 피해자는 공격자가 보낸 양보다 많은 트래픽을 받는다. 이를 증폭 공격이라고 한다.

DNS, NTP, SSDP 등 UDP를 사용하는 서비스는 잘못 공개되거나 설정된 경우 반사·증폭 공격에 악용될 수 있다.

DNS 서버 역할 구분

  • 권한 DNS 서버: 특정 도메인 영역의 레코드를 권위 있게 제공한다.
  • 재귀 리졸버: 클라이언트를 대신해 다른 DNS 서버를 조회하고 결과를 캐시한다.
  • 오픈 리졸버: 인터넷의 임의 사용자에게 재귀 질의를 제공하도록 노출된 DNS 리졸버를 말한다.

권한 DNS 서버라고 해서 재귀 질의를 절대 하지 않는다고 단정할 수는 없다. 두 역할은 다르지만 실제 동작은 서버 설정을 확인해야 한다.

운영에서 적용할 방어

  • DNS와 UDP 기반 서비스는 필요한 기능과 대상에만 공개한다.
  • 재귀 DNS 질의의 접근 범위를 제한해 공개 리졸버가 되지 않도록 한다.
  • 출발지 주소 위조 트래픽을 줄이는 네트워크 필터링과 클라우드 사업자의 DDoS 완화 기능을 활용한다.
  • 평소 트래픽 기준선을 기록하고 요청량, 대역폭, 오류율의 급격한 변화를 감시한다.
  • 장애 시 차단·우회·사업자 연락 절차를 미리 정하고 점검한다.

3. 웹 공격과 표적형 공격

드라이브 바이 다운로드

드라이브 바이 다운로드는 사용자가 악성 파일을 직접 실행하지 않았더라도 취약한 브라우저나 플러그인, 잘못 구성된 웹 환경을 통해 악성 코드가 내려오거나 실행되는 공격을 가리킨다.

웹 서버와 브라우저를 최신 상태로 유지하고 불필요한 플러그인을 제거하는 것이 기본이다. 여기에 엔드포인트 보호, 웹 서버 변경 감시, 의심스러운 파일 격리를 더해 피해 가능성을 낮출 수 있다.

SQL 인젝션, 세션 공격, CSRF

SQL 인젝션은 외부 입력이 SQL 구문으로 해석되어 실행되는 취약점이다. 값과 질의 구조를 분리하는 매개변수화 질의를 사용하고, 데이터베이스 계정의 권한을 필요한 범위로 제한해야 한다.

세션 탈취와 인증 우회 위험을 낮추려면 쿠키 보호 설정, 안전한 세션 수명, 다중 요소 인증, 요청마다의 권한 확인을 적용한다.

CSRF(사이트 간 요청 위조)는 사용자가 로그인한 상태를 악용해 의도하지 않은 요청을 보내게 하는 공격이다. 상태를 변경하는 요청에서 CSRF 토큰을 확인하고, 쿠키의 SameSite 설정도 함께 검토한다.

스피어 피싱과 APT

  • 스피어 피싱: 특정 사람이나 조직에 맞춘 메시지로 링크 클릭이나 계정 정보 입력을 유도한다.
  • 소셜 엔지니어링: 사람의 신뢰나 업무 관행을 악용한다.
  • APT(Advanced Persistent Threat): 고도화되고 지속적인 위협을 뜻하며, 장기간 은밀하게 접근을 유지하는 공격 캠페인을 설명할 때 사용한다.

APT는 특정 악성코드나 도구 하나를 뜻하지 않는다. 메일·웹 필터링, 다중 요소 인증, 최소 권한, 계정과 단말 로그 수집, 백업 복구 점검을 함께 적용해야 한다. 계정 탈취가 의심되면 세션과 자격 증명을 폐기하고 영향 범위를 확인한다.

4. 클라우드 보안과 책임 범위

클라우드 보안은 제공자와 고객이 나누어 맡는다. 제공자는 데이터센터와 물리 장비, 가상화 기반처럼 클라우드 자체의 보안을 담당한다. 고객은 선택한 서비스에 따라 데이터, 계정과 권한, 네트워크 규칙, 애플리케이션과 설정을 관리한다.

가상 서버를 직접 운영할 때와 완전 관리형 서비스를 사용할 때 고객이 맡는 범위는 달라진다. 따라서 서비스별 책임 경계를 확인해야 한다.

관련 표준과 관리체계

  • ISO/IEC 27017: 클라우드 서비스의 정보보호 통제를 다루는 지침이다. 클라우드 제공자와 고객의 책임을 포함한다.
  • ISMS: 정보보호 관리체계(Information Security Management System)를 뜻한다. 위험을 식별하고 정책과 통제를 적용한 뒤 점검과 개선을 반복하는 관리 방식이다.

표준과 인증 기준은 개정될 수 있으므로 실제 계약이나 감사에 적용할 때는 해당 시점의 공식 기준을 확인한다.

클라우드 기본 점검

  • 계정과 역할에 최소 권한을 적용하고 관리자 접근에는 강한 인증을 사용한다.
  • 공개 저장소와 네트워크 규칙을 검토한다.
  • 암호화 키와 토큰 같은 비밀정보를 코드 저장소에 두지 않는다.
  • 로그와 백업의 보존 기간, 접근 권한, 복구 책임자를 정한다.
  • 서비스별 제공자 책임과 고객 책임을 문서화한다.

5. 운영 중 취약점과 이상 징후 확인

운영 보안은 자산을 파악하고, 이상을 탐지하고, 영향도를 판단한 뒤, 수정과 재확인을 반복하는 과정이다. 도구 이름만 외우기보다 어떤 질문에 답하기 위해 사용하는지 이해하는 것이 중요하다.

모니터링과 점검 도구

  • SNMP: 네트워크 장비와 서버의 상태 정보를 수집하는 관리 프로토콜이다.
  • Cacti, OpenNMS: 네트워크와 시스템 모니터링에 사용할 수 있는 도구의 예다.
  • syslog, Windows Event Log: 운영체제와 서비스에서 발생한 로그를 수집하는 데 사용한다.
  • ICMP ping: 호스트의 기본 연결성을 확인하지만 애플리케이션이 정상인지까지 보장하지는 않는다. 실제 서비스는 TCP 연결이나 HTTP 요청도 확인해야 한다.
  • Nmap: 네트워크 호스트와 포트를 확인하는 도구다. 소유하거나 명시적으로 허가받은 범위에서만 실행한다.
  • OWASP ZAP: 웹 애플리케이션의 동적 보안 시험에 활용할 수 있다. 테스트 환경과 실행 범위를 먼저 정한다.
  • Greenbone/OpenVAS 계열: 취약점 점검과 결과 관리를 지원한다. 발견 결과는 자산과 버전, 노출 경로를 대조해 확인한다.

취약점 대응 순서

  1. 자산과 외부 노출 범위를 확인하고 취약점 식별자, 영향을 받는 제품과 버전을 기록한다.
  2. CVSS 점수와 벡터를 참고해 기술적 심각도를 파악한다.
  3. 인터넷 노출 여부, 실제 악용 정황, 자산 중요도, 보완 통제와 패치 가능성을 함께 고려해 우선순위를 정한다.
  4. 패치나 설정 변경을 검증 환경에서 확인하고 변경 및 롤백 계획을 세운다.
  5. 적용 후 재검사와 로그 확인으로 해결 여부를 검증한다.
  6. 예외를 둘 경우 책임자, 만료 기한, 보완 통제를 기록한다.

CVSS는 취약점 특성에 따른 심각도 척도다. 조직의 실제 위험도나 조치 순서를 단독으로 결정하는 값은 아니다.

6. 코드로 관리하는 인프라

IaC(Infrastructure as Code)는 서버와 네트워크 같은 인프라의 원하는 상태를 코드나 설정으로 표현하고, 검토·배포·재현하는 방식이다. 변경 이력을 남기고 반복 가능한 절차를 만들면 수작업으로 생기는 누락과 환경 차이를 줄일 수 있다.

다만 잘못된 설정도 빠르게 복제될 수 있다. 코드 검토와 권한 통제를 함께 운영해야 한다.

부트스트래핑 계층

운영체제나 실행 기반을 준비하는 단계다. 데이터센터에서는 OS 설치 자동화가 해당하고, 클라우드에서는 이미지 선택이나 인스턴스 초기화가 이에 해당한다.

Kickstart와 Cobbler는 OS 설치 자동화의 사례다. 클라우드 이미지와 초기화 스크립트도 부트스트래핑의 일부를 맡는다.

구성 관리 계층

OS가 준비된 뒤 패키지, 미들웨어, 사용자, 파일, 서비스 설정을 원하는 상태로 맞춘다. Ansible, Puppet, Chef 등이 구성 관리 도구의 예다.

구성 관리 도구에서 자주 쓰는 개념은 다음과 같다.

  • 선언적: 원하는 최종 상태를 표현하고, 실행 절차 일부를 도구에 맡긴다.
  • 추상화: 개별 명령보다 서비스나 패키지 같은 관리 대상을 중심으로 기술한다.
  • 멱등성: 같은 설정을 여러 번 적용해도 결과가 의도한 상태로 유지되도록 한다.
  • 수렴성: 현재 상태와 원하는 상태의 차이를 줄여 목표 상태에 도달하도록 한다.

오케스트레이션 계층

여러 서버와 서비스의 배포와 연결, 확장을 조율한다. 애플리케이션 배포, 클러스터 구성원 변경, 로드밸런서 대상 등록, 모니터링 대상 등록 등이 여기에 포함된다.

서버를 직접 지정하는 방법은 변경 범위가 분명하지만 반복 작업이 생긴다. 서비스 디스커버리나 클러스터 관리 도구를 통한 자동 등록은 규모 확장에 유리하지만 자동화 규칙과 상태 감시가 필요하다.

Capistrano와 Fabric은 원격 작업이나 배포 자동화에 사용할 수 있다. Serf와 Consul은 클러스터 구성원 정보와 서비스 디스커버리 같은 분산 환경 기능의 사례다. 도구별 기능과 지원 상태는 바뀔 수 있으므로 도입 전 공식 문서를 확인한다.

IaC를 안전하게 운영하기

  • 코드를 버전 관리에 저장하고 동료 검토와 자동 검증을 거쳐 적용한다.
  • 비밀번호와 토큰, 개인 키를 코드나 상태 파일에 평문으로 저장하지 않는다.
  • 개발·검증·운영 환경의 차이를 명시적으로 관리한다.
  • 실제 인프라와 코드 사이의 설정 편차(drift)를 주기적으로 확인한다.
  • 변경 권한을 최소화하고 실패 시 되돌릴 방법을 준비한다.
  • 큰 변경은 일부 자원에 먼저 적용해 영향을 확인한다.

후기

이번 내용을 정리하면서 인프라 보안은 보안 도구를 설치하는 것만으로 끝나는 일이 아니라는 점을 알게 됐다. 안전한 설정과 최소 권한을 바탕으로 로그와 지표를 계속 확인하고, 취약점이 실제 서비스에 미치는 영향까지 살펴야 대응 우선순위를 제대로 정할 수 있다.

또 IaC를 사용하면 인프라 변경을 반복하고 재현하기 쉬워지지만, 잘못된 설정도 여러 환경에 빠르게 퍼질 수 있다는 점을 배웠다. 그래서 코드를 검토하고 비밀정보를 보호하며 실제 설정과 코드가 일치하는지 확인하는 과정이 자동화만큼 중요하다. 앞으로 인프라 문제를 만나면 도구부터 찾기보다 어떤 자산이 영향을 받는지, 책임 범위와 변경 이력이 무엇인지 먼저 확인해야겠다.

핵심만 다시 정리

  • 사고 원인은 기술 결함뿐 아니라 설정 실수, 계정 문제, 변경 절차, 감시 공백에서도 생긴다.
  • UDP 반사·증폭 공격은 주소 위조와 공개 서비스의 설정 문제를 악용할 수 있다.
  • 클라우드 책임 범위는 사용하는 서비스 형태에 따라 달라진다.
  • 취약점 점수와 스캐너 결과는 판단을 돕는 자료이며 실제 위험을 함께 따져야 한다.
  • IaC를 안전하게 쓰려면 변경 검토, 비밀정보 보호, 편차 확인이 필요하다.

참고 자료

728x90
  Comments,     Trackbacks
하네스 엔지니어링이란? - (end)
728x90

Claude Code 하네스로 디버깅 / RCA 팀 꾸리기 — "로컬에서는 잘 되는데요"

리뷰 팀은 있는 코드를 평가했고, 구현 팀은 없는 기능을 만들었고, 마이그레이션 팀은 같은 변환을 수천 번 반복했다. 이번엔 원인을 모르는 문제를 다룬다. 디버깅은 추측이 가장 쉽게 끼어드는 일이라, 이번 하네스의 목표는 하나다. 추측을 증거로 바꾸는 순서를 강제하는 것.
참고 저장소: https://github.com/revfactory/harness-engineering-with-cc (ex-14-10-application-guide)


1. 디버깅이 왜 하네스가 필요한 일인가

혼자 디버깅할 때를 떠올려 보면 이렇다. 버그 보고를 받고, 코드를 훑고, "아마 이거겠지" 하고 고치고, 테스트가 통과하면 끝. 재현도, 가설 검증도, 같은 실수가 다른 곳에 있는지 확인도 안 한다. 운 좋게 맞으면 다행이고, 틀리면 버그는 다른 모양으로 돌아온다.

Claude에게 "이 버그 고쳐 줘"라고 시켜도 똑같다. 오히려 더 빠르게 추측하고 더 자신 있게 고친다. 그래서 이번 하네스는 네 가지 장치로 그 지름길을 막는다.

장치 막는 것
단계별 담당 분리 재현·분석·가설·수정이 한 머릿속에서 뒤섞이는 것
결정론 게이트 재현도 안 하고 가설로 넘어가는 것
하이브리드 구조 탐색은 느려지고, 수정 루프는 끝없이 도는 것
피시본 분석 떠오르는 원인 하나에 꽂혀 나머지를 안 보는 것

2. 저장소에서 가져온 것, 직접 설계한 것

저장소의 디버깅 예제에는 응용 가이드 폴더에 프로덕션 장애용 reviewer.md 하나만 있었다. 핵심 4인 에이전트, 오케스트레이터, 샘플 버그는 없었다. 대신 reviewer.md가 검토하는 산출물 이름(reproduction.sh, hypotheses.md, fix.patch, completion-checklist.md)이 나와 있어서, 그 이름과 5단계 워크플로에 맞춰 나머지를 직접 설계했다.


3. 시나리오: 로컬에서는 통과, CI에서는 실패

실습용 버그는 타임존 버그로 만들었다.

// src/settlement.js — 정산 마감일 계산
export function businessDate(isoString) {
  const d = new Date(isoString);
  const y = d.getFullYear();
  const m = String(d.getMonth() + 1).padStart(2, '0');
  const day = String(d.getDate()).padStart(2, '0');
  return `${y}-${m}-${day}`;
}

getDate()는 코드를 실행하는 컴퓨터의 시간대 기준이다. 한국 시간 11월 1일 새벽 0시 30분은 UTC로는 10월 31일 15시 30분. 그래서 한국 PC에서는 2026-11-01, UTC로 도는 CI 서버에서는 2026-10-31이 나온다. 코드만 봐서는 잘못이 안 보이고, 개발자 PC에서는 테스트가 전부 통과한다. "로컬에서는 잘 되는데요"의 교과서 같은 사례다.

재료는 세 가지를 뒀다.

  • 버그 보고서 reports/BUG-001.md: 증상, "로컬에서는 잘 되는데요"라는 개발자 코멘트, 발생·정상 환경
  • CI 로그 reports/ci.log: 실패 메시지와 함께 Node 버전, LANG=C.UTF-8, TZ=(빈 값). 진짜 단서와 헷갈리게 하는 단서가 섞여 있다
  • 같은 패턴의 두 번째 파일 src/invoice.js: 청구서 발행일도 같은 방식으로 날짜를 뽑는다. 마지막 단계의 "유사 패턴 검색"이 이걸 찾아내는지 보기 위한 함정이다

하네스를 돌리기 전에 정답부터 손으로 확인해 뒀다.

npm test                 # 통과 (로컬 한국 시간)
TZ=UTC npm test          # 실패 (CI와 같은 조건)

4. 팀 구성: 4명

에이전트 맡은 일 model 핵심 권한
reproducer 재현 스크립트, 3회 연속 재현 게이트, 수정 후 재검증 sonnet Bash, Write / Edit 없음
log-analyzer 정상·실패 환경 비교, 타임라인 sonnet Bash, Write / Edit 없음
hypothesis-analyst 피시본 분석, 가설 5개, 반증 실험 설계 opus Write / Bash 없음
fixer 최소 수정, 회귀 테스트, fix.patch sonnet Edit, Bash

지난 실습들에서 배운 대로 역할 경계는 tools 한 줄로 정했다.

코드를 고칠 수 있는 건 fixer뿐이다. 재현 담당이 재현하다가 "이거 고치면 되겠네" 하고 손대는 순간, 재현이 증거에서 추측으로 바뀐다.

별도 verifier가 없다. 수정 후 검증은 reproducer가 맡는다. 처음에 버그를 잡아낸 그 재현 스크립트로 "이제 안 일어난다"를 보여 준다. 같은 자로 재야 전후 비교가 된다.

가설 담당은 실험을 못 한다. Bash가 없어서 반증 실험을 설계만 하고 실행은 reproducer가 한다. 가설을 세운 사람이 자기 가설을 검증하면 확증 편향이 끼어든다. 구현 팀에서 "만든 사람이 검증하면 자기 확증"이라고 정리했던 원리가 여기서는 "가설 세운 사람이 실험하면 자기 확증"으로 나타난다.

opus는 가설 담당 하나다. 여러 증거를 동시에 놓고 범주별로 원인을 따지는, 판단이 가장 무거운 자리라서다.


5. 워크플로: 5단계와 두 개의 게이트

단계 하는 일 모드 다음으로 가는 문
1. 준비 보고서 파싱, _workspace/bug-001/ 생성 메인 세션 report.md 정리
2. 관찰 재현 + 로그 분석 병렬 팀 게이트 1: 재현 3회 연속
3. 가설 피시본 → 가설 5개 → 우선순위 → 반증 실험 서브 1순위 가설이 실험으로 지지됨
4. 수정-검증 수정 → 재현 스크립트 확인, 최대 3회 서브 게이트 2: 수정 후 3회 연속 통과
5. 정리 포스트모템 + 유사 패턴 검색 메인 세션 완료 체크리스트

6. 결정론 게이트: "3회 연속"의 의미

reproducer의 정의 파일에서 가장 중요한 줄은 이것이다.

3회 중 결과가 갈리면 "비결정적"으로 보고하고 게이트를 통과시키지 않는다.

한 번 재현은 우연일 수 있다. 두 번 실패, 한 번 성공이면 그건 재현이 아니라 운이다. 그 상태로 가설 단계에 가면 가설을 확인할 방법이 없다. 실험 결과가 바뀌어도 그게 가설 때문인지 운 때문인지 모르니까.

그리고 재현 스크립트에는 대조군이 같이 들어간다. "UTC에서 실패"만으로는 부족하고, 같은 스크립트 안에서 "한국 시간에서는 통과"까지 보여야 한다. 그래야 시간대가 원인의 경계선이라는 게 증명된다.

reproduction.sh 3회 실행
  1회: TZ=UTC 실패 / TZ=Asia/Seoul 통과
  2회: TZ=UTC 실패 / TZ=Asia/Seoul 통과
  3회: TZ=UTC 실패 / TZ=Asia/Seoul 통과
→ 게이트 1 통과

같은 스크립트가 수정 후에는 반대 방향의 게이트가 된다. 3회 모두 양쪽 다 통과해야 수정이 인정된다. 게이트를 사람의 느낌이 아니라 반복 실행 결과로 정한다는 것, 이게 이번 실습에서 배운 첫 번째 지식이다.


7. 하이브리드 구조: 왜 관찰은 팀, 수정은 서브에이전트인가

지금까지 세 팀은 모드가 하나였다. 리뷰 팀과 마이그레이션 팀은 서브에이전트, 구현 팀은 팀 모드. 디버깅 팀은 한 워크플로 안에서 둘을 섞는다.

관찰은 탐색이다. log-analyzer가 CI 로그에서 TZ=가 비어 있는 걸 발견하는 순간, reproducer가 바로 TZ=UTC로 돌려 봐야 빠르다. 메인 세션을 거쳐 전달하면 느리고 맥락이 끊긴다. 그래서 둘을 한 팀으로 묶어 직접 대화하게 둔다.

수정-검증은 통제다. 고치고, 재고, 실패하면 다시. 이 루프의 횟수를 메인 세션이 정확히 세고 있어야 3회 상한이 지켜진다. fixer와 reproducer를 한 팀으로 묶어 직접 대화하게 두면 저희끼리 몇 번이고 주고받으며 상한이 무너진다. 그래서 메인 세션이 한 명씩 부른다.

탐색 구간은 팀으로, 통제 구간은 서브에이전트로.

모드를 "이 하네스 전체에 하나"로 고르는 게 아니라 구간마다 일의 성격을 보고 고른다. 구현 팀에서 패턴을 Phase마다 조합했던 것처럼, 이번엔 실행 모드를 단계마다 조합한 셈이다.

관찰 단계에 팀 모드가 들어가니 이번엔 환경 변수가 필요하다.

export CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1
claude

8. 피시본: 가설을 떠올리지 말고 펼친다

가설 담당에게는 원인 범주 여섯 개를 줬다.

코드 / 환경 / 의존성 / 데이터 / 테스트 / 프로세스

규칙은 범주마다 최소 하나씩 요인을 적고, 해당 없으면 "해당 없음 + 이유"를 적는 것. 이렇게 하면 처음 눈에 띈 원인에 꽂혀 나머지를 건너뛰는 일이 막힌다. 이번 버그로 펼치면 대략 이런 모양이 나와야 한다.

범주 요인
코드 실행 환경 시간대에 의존하는 날짜 getter
환경 CI의 TZ 미설정(UTC), 로컬은 한국 시간
의존성 Node 버전 차이 (로컬 vs CI v20)
데이터 자정 직후 타임스탬프 (시간대 경계)
테스트 로컬 시간대에서만 실행, 시간대 고정 없음
프로세스 CI에 시간대 매트릭스 없음

여기서 가설 5개를 고르고, 가설마다 반증 실험을 붙인다.

  • 시간대 가설이 맞다면 → TZ=Asia/Seoul로 돌리면 통과해야 한다
  • Node 버전 가설이 맞다면 → 시간대는 그대로 두고 Node 버전만 바꿔도 결과가 바뀌어야 한다

반증 실험은 reproducer가 돌린다. 결과가 나오면 가설끼리 말싸움할 필요가 없다. 틀린 가설은 실험으로 지워진다. 이게 "체계적으로 좁힌다"의 실제 모습이었다.

한 가지 더. 가설 담당에게 "단일 원인으로 환원하지 않는다, 직접 원인과 기여 요인을 구분한다"를 줬다. 이 버그의 직접 원인은 코드지만, 기여 요인은 테스트(한국 시간에서만 돎)와 프로세스(CI에 시간대 고정 없음)다. 코드만 고치면 다음 시간대 버그도 또 "로컬에서는 잘 되는" 채로 들어온다.


9. 범위를 지키는 두 줄

이번 하네스에서 의외로 중요했던 건 "하지 않는 일"을 정한 두 줄이다.

fixer: "기여 요인은 고치지 않고 포스트모템 권고로 넘긴다."
fixer가 신나서 CI 설정까지 바꾸기 시작하면 수정 범위가 커지고, 검증이 실패했을 때 무엇 때문인지 가려낼 수 없다. 코드 수정은 최소로, 구조 개선은 포스트모템으로 분리한다.

5단계: "유사 패턴은 목록화만 하고 고치지 않는다."
invoice.js에도 같은 버그가 있다. 하지만 여기서 같이 고치면 이번 수정의 검증 범위가 흐려진다. 유사 패턴 목록은 다음 사고 후보 목록이고, 고치는 건 별도 버그로 다룬다.


10. 하네스 구조

debug_rca/
├── CLAUDE.md                          원칙: 재현 없이 가설 없음, 3회 게이트, blameless
├── reports/
│   ├── BUG-001.md                     버그 보고서
│   └── ci.log                         CI 실패 로그
├── src/settlement.js, invoice.js      버그가 숨은 코드
├── tests/settlement.test.js           로컬에서만 통과하는 테스트
├── .claude/
│   ├── agents/
│   │   ├── reproducer.md
│   │   ├── log-analyzer.md
│   │   ├── hypothesis-analyst.md
│   │   └── fixer.md
│   └── skills/debug-rca/SKILL.md      5단계 오케스트레이터
└── _workspace/bug-001/                실행 시 생성
    ├── report.md
    ├── observations/repro.md, logs.md
    ├── reproduction.sh
    ├── hypotheses.md
    ├── fix.patch
    ├── verification.md
    ├── postmortem.md
    ├── similar-patterns.md
    └── completion-checklist.md

실행은 한 줄이다.

BUG-001 디버깅해 줘

11. 응용: 같은 뼈대, 바꾸는 곳만 다르다

재현 → 가설 → 수정-검증 → 정리라는 뼈대는 그대로 두고, 상황에 따라 게이트의 의미와 담당자의 초점만 바꾼다.

응용 게이트 1 (재현) 바뀌는 담당 게이트 2 (수정 확인)
프로덕션 장애 프로덕션 말고 스테이징·로컬에 재현 환경 구축 log-analyzer → 알람·배포 타임라인 중심. 저장소의 reviewer 추가 핫픽스는 사람이 결정, 하네스는 근본 수정만
플래키 테스트 3회 연속 대신 N회(예: 50회) 실패율 측정 피시본 범주 교체: 실행 순서, 타이밍, 공유 상태, 난수, 네트워크 같은 N회에서 실패 0회
성능 저하 3회 측정 모두 기준선 대비 임계값 이상 저하 + 편차 작음 log-analyzer → 프로파일러 역할 3회 측정 모두 기준선 ± 허용 오차
1인 개발자 그대로 3회 팀 모드 없이 reproducer + fixer 둘만 남김. 로그·가설은 메인 세션이 겸함 그대로 3회, 루프 3회 상한 유지

몇 가지는 짚어 둘 만하다.

프로덕션 장애의 reviewer는 저장소에 있던 유일한 파일인데, 도구가 Read 하나뿐이다. 포스트모템 검토자는 아무것도 실행하거나 고치지 못하고 읽기만 한다. 그리고 "누구 잘못인가" 대신 "시스템이 왜 그걸 허용했나"를 묻고, 권고마다 자동화된 가드(린터, 테스트, 알람 임계값)를 붙이게 되어 있다. 장애 회고가 사람 탓으로 흐르는 걸 구조로 막는다. 이 blameless 원칙은 기본 하네스의 CLAUDE.md에도 헌법으로 넣어 뒀다.

플래키 테스트는 reproducer의 "3회 중 결과가 갈리면 통과시키지 않는다"에서 갈라지는 경로다. 결정론 게이트를 통과하지 못하는 버그는 버리는 게 아니라 통계 게이트로 갈아탄다.

1인 개발자 버전에서 아무리 줄여도 reproducer와 fixer는 반드시 분리한다. 둘이 합쳐지면 "고친 사람이 고쳐졌다고 판정"하게 되어 하네스의 존재 이유가 사라진다. 비용을 줄일 때 지켜야 할 최소 단위가 이 둘이다.


12. 네 팀을 이어 보면 (최종 정리)

리뷰 팀 구현 팀 마이그레이션 팀 디버깅 팀
다루는 일 있는 코드 평가 없는 기능 만들기 같은 변환 반복 모르는 원인 찾기
주 패턴 팬아웃 Phase별 조합 감독자 게이트 + 하이브리드
넘어가는 기준 없음 Phase 산출물 검토 배치 검증 재현 3회 연속
실행 모드 서브 팀 서브 팀 + 서브 혼합
검증 기준 정의 파일 기준 설계 문서 기준선 테스트 재현 스크립트
마지막 산출물 리뷰 보고서 통합 보고서 상태 파일 포스트모템

디버깅 팀에서 새로 얻은 건 두 가지다.

  1. "넘어가도 되는가"를 사람의 느낌이 아니라 반복 실행 결과로 정하는 게이트. 그리고 그 게이트가 통과되지 않을 때 갈아탈 경로(플래키, 성능)까지 미리 설계해 두는 것.
  2. 한 워크플로 안에서도 탐색 구간은 팀으로, 통제 구간은 서브에이전트로 섞는 설계. 패턴을 조합했듯 모드도 조합한다.
728x90
  Comments,     Trackbacks
하네스 엔지니어링이란? - (3)
728x90

레거시 파일 수천 개를 옮기는 하네스 직접 쌓아 보기 — 감독자 패턴 실습기

리뷰 팀은 "동시에 보고 끝내는" 팀이었고, 구현 팀은 "Phase마다 팀을 갈아끼우는" 팀이었다. 세 번째로 만든 건 같은 작은 일이 수천 번 반복되는 일을 맡는 팀이다. 레거시 .jsx 파일들을 TypeScript로 옮기는 마이그레이션. 이번엔 감독자 패턴을 처음으로 직접 짜 봤다.

항목 내용
실습일 2026-11-01
실습 위치 ~/Desktop/side/harness/legacy_migration
만든 것 에이전트 4명 + 감독자 스킬 1개 + CLAUDE.md + 상태 파일 구조
직접 설계한 것 baseline-tester, migration-worker, migration-verifier, legacy-migration 스킬
가져다 쓴 것 dependency-mapper 정의, 샘플 레거시 코드 17개
진행 상황 하네스 구축·검사 완료, 본 실행 결과는 다음 글

1. 시작하며: 이번엔 일의 모양이 다르다

앞의 두 팀을 만들면서 "일의 모양이 패턴을 정한다"는 걸 배웠다. 그래서 이번에도 먼저 일의 모양부터 따져 봤다. 레거시 3,500개 파일을 바꾼다고 치면 성질이 네 가지다.

  1. 반복: 파일 하나 바꾸는 일 자체는 작고 비슷하다. 그게 수천 번.
  2. 순서: UserCard → useAuth → api → http. 아래부터 바꿔야 위가 안 깨진다.
  3. 불균형: 같은 개수여도 어떤 묶음은 쉽고 어떤 묶음은 어렵다.
  4. 중단: 이 정도 양이면 중간에 반드시 끊긴다.

1-1. 처음 떠올린 두 방법이 왜 안 되는가

Claude 하나에게 다 시키기. 제일 먼저 떠오르지만 바로 접었다.

  • 맥락이 넘쳐서 100번째와 3,000번째 파일의 변환 방식이 달라진다.
  • 어디까지 했는지가 대화 속에만 있어서 끊기면 처음부터다.
  • 한 파일이 깨져도 다음으로 넘어가 버린다.

리뷰 팀처럼 처음에 똑같이 나누기(고정 팬아웃). 리뷰 팀에서 잘 썼으니 이번에도 될까 싶었는데 아니었다.

  • 워커 A가 UserCard를 바꾸는 동안 그게 쓰는 http는 워커 C가 아직 손도 안 댔을 수 있다. 순서가 찢어진다.
  • 쉬운 묶음 받은 워커는 놀고, 어려운 묶음 받은 워커는 한참 걸린다.
  • 이미 다 나눠 준 뒤라 실패한 일을 다시 나눌 수가 없다.

1-2. 그래서 감독자

정리하고 보니 필요한 건 대기열을 쥔 사람이었다. 주방장이 주문서를 들고 있다가, 요리사 손이 비면 지금 바로 만들 수 있는 주문 하나를 건넨다. 소스가 아직 안 끓었으면 그 소스가 들어가는 요리는 내주지 않는다. 일을 미는 게 아니라 워커가 가져가는 구조다.

  혼자 다 하기 처음에 나누기 감독자
분배 시점 없음 처음 한 번 워커가 빌 때마다
의존 순서 머릿속 찢어짐 대기열이 지킴
일의 불균형 — 노는 워커 자동 흡수
실패 처리 묻힘 못 옮김 재배정, 상한 넘으면 사람
중단 후 재개 처음부터 어려움 상태 파일에서 이어서

2. 팀 설계: 4단계, 4명 + 감독자

예제 저장소를 열어 보니 이번 폴더엔 dependency-mapper 에이전트 하나와 샘플 코드 17개뿐이었다. README에 나오는 mock-mapper.sh, verify.sh도 없었다. 리뷰 팀 때 "저장소는 단계별 스냅샷"이라는 걸 한 번 겪어서 이번엔 덜 당황했다. 나머지 팀원은 직접 설계하기로 했다.

먼저 워크플로를 4단계로 잡았다.

단계 담당 하는 일 패턴
1. 의존 매핑 dependency-mapper import 그래프 → 배치와 순서 —
2. 기준선 테스트 baseline-tester 변환 전 동작을 테스트로 박제 —
3. 점진 변환 감독자 + migration-worker 여럿 시작 가능한 배치를 하나씩 가져감 감독자
4. 검증 migration-verifier 배치마다 PASS/FAIL 판정 생성-검증

구현 팀에서 배운 "패턴의 층"이 여기서도 그대로 보였다. 바깥 뼈대는 파이프라인(1→2→3·4), 안쪽 엔진은 감독자와 생성-검증. 다만 이번엔 3단계와 4단계가 배치 단위로 맞물려 계속 돈다.

[1. 의존 매핑] ──▶ [2. 기준선 테스트] ──▶ ┌──────── 3·4단계 루프 (배치 단위) ────────┐
 import 그래프       변환 전 동작 박제       │ 감독자(메인 세션) ── 대기열(batches.json) │
 → 배치·순서         전부 통과 못 하면 정지  │   │ 시작 가능 배치 출고 (최대 3 동시)      │
                                          │   ▼                                    │
                                          │ migration-worker ×N ──▶ migration-verifier│
                                          │        ▲   FAIL(사유)       │ PASS        │
                                          │        └── pending 복귀 ◀───┘ → verified  │
                                          │  3회째 FAIL → 되돌림 + failed + 위층 차단  │
                                          └──────────────────────────────────────────┘

감독자는 에이전트로 만들지 않았다. 구현 팀의 PM처럼 스킬을 읽은 메인 세션이 감독자다. 워커끼리 대화할 일도 없어서 에이전트 팀 모드도 안 켰다. 배치 사이의 정보는 "이미 변환된 파일의 타입을 읽는다"로 파일을 통해 오간다. 일반 서브에이전트로 충분하다.

변환 대상은 샘플이 .jsx라서 JavaScript → TypeScript로 정했다.

2-1. 권한 설계: tools 칸이 곧 직무 분리

담당 모델 tools 일부러 뺀 것
dependency-mapper (원본 유지) Read, Grep, Bash Write — 저장은 감독자가
baseline-tester sonnet Read, Grep, Write, Bash Edit — src는 읽기만
migration-worker sonnet Read, Grep, Write, Edit, Bash (본문에서 tests/·남의 배치·commit 금지)
migration-verifier sonnet Read, Grep, Bash Edit, Write — 판정만

표를 채우면서 의도한 건 세 가지다.

  • 테스트를 쓰는 사람은 코드를 못 고친다 → 테스트 통과시키려고 코드를 바꾸는 일이 불가능.
  • 판정하는 사람은 아무것도 못 고친다 → 보다가 편한 쪽을 슬쩍 고쳐 버리는 일이 불가능. 구현 팀 boundary-verifier에서 배운 그대로다.
  • 아무도 커밋하지 않는다 → 커밋은 내가 한다.

2-2. 모델 설계: 이번엔 전원 sonnet

구현 팀 때 boundary-verifier는 opus였다. 두 파일을 동시에 놓고 "구현 탓이냐 설계 탓이냐"를 추론해야 했으니까. 이번 verifier는 다르다. tsc, vitest, git diff가 내놓는 결과로 판정이 거의 다 정해지고, 판정도 PASS/FAIL 둘뿐이다. 판단이 도구 출력에 있으면 모델은 한 단계 낮춰도 된다고 봤다. 워커도 규칙이 명확하고 배치가 작으니 sonnet. 반복 횟수가 많아서 비용 면에서도 이게 맞다.


3. 판 깔기

mkdir -p ~/Desktop/side/harness/legacy_migration
cd ~/Desktop/side/harness/legacy_migration

git clone --depth 1 https://github.com/revfactory/harness-engineering-with-cc.git /tmp/hewcc
cp -r /tmp/hewcc/ex-13-01-dependency-mapper/sample-src ./src
find src -type f | sort

샘플 17개를 훑어보다가 일부러 심어 둔 함정 두 개를 발견했다.

파일 함정 하네스가 해야 할 일
cyclic-a.jsx, cyclic-b.jsx 서로를 import하는 순환 의존 한 배치로 묶고 cyclic 경고
truncated.jsx 중간에 잘린 파일 (파싱 실패) 배치에서 빼고 manual-queue로

나머지 15개로 의존 층을 직접 그려 봤다. 실행했을 때 맞는지 비교할 정답지다.

1층 (의존 없음)   utils/http, utils/format, components/Footer, standalone
2층              utils/api
3층              hooks/useAuth, hooks/useFetch, components/Header
4층              context/SessionContext, components/UserCard
5층              components/Profile, components/UserList
6층              pages/Dashboard, pages/Settings
따로             cyclic-a + cyclic-b (한 배치, cyclic 경고)
제외             truncated (manual-queue)

되돌리기를 git으로 할 거라 레거시 상태를 먼저 커밋해 두고, TypeScript와 vitest를 깔았다.

git init
git add src
git -c user.name=practice -c user.email=practice@local commit -m "legacy baseline"

npm init -y
npm install -D typescript vitest
npm pkg set scripts.test="vitest run" scripts.typecheck="tsc --noEmit"

cat > tsconfig.json << 'EOF'
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "jsx": "preserve",
    "strict": true,
    "allowJs": true,
    "checkJs": false,
    "noEmit": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}
EOF

allowJs: true가 중요했다. 마이그레이션 도중엔 .tsx와 .jsx가 섞여 있다. 이 상태에서도 타입 체크가 돌아야 배치마다 검증할 수 있다.


4. CLAUDE.md: 원칙 네 줄

cat > CLAUDE.md << 'EOF'
# legacy_migration — 레거시 마이그레이션 하네스

## 개요
- 진입점: legacy-migration 스킬 ("src를 TypeScript로 마이그레이션해 줘")
- 메인 세션이 감독자. 워커는 서브에이전트
- 상태 파일: _workspace/migration/batches.json (진행 상태의 유일한 기준)

## 워크플로
1. 의존 매핑 → 2. 기준선 테스트 → 3. 점진 변환 → 4. 배치별 검증

## 원칙
- 마이그레이션은 동작을 바꾸지 않는다. 버그도 그대로 옮긴다. 고치는 건 별도 작업
- 기준선 테스트는 변환 중 절대 수정하지 않는다
- 배치 실패가 상한을 넘으면 그 배치 파일을 git으로 되돌리고 manual-queue에 기록
- 누구도 git commit 하지 않는다. 커밋은 사람이 한다
EOF

"버그도 그대로 옮긴다"를 넣을지 한참 고민했다. 바꾸는 김에 고치면 좋지 않나? 그런데 그러면 테스트가 깨졌을 때 변환이 잘못된 건지 일부러 고친 건지 구분이 안 된다. 기준이 흔들리면 검증 자체가 의미가 없어진다. 그래서 고치는 건 별도 작업으로 뺐다.


5. 팀원 명부

5-1. dependency-mapper: 가져와서 배치 크기만 줄였다

mkdir -p .claude/agents
cp /tmp/hewcc/ex-13-01-dependency-mapper/.claude/agents/dependency-mapper.md .claude/agents/
sed -i 's/10~50/2~4/' .claude/agents/dependency-mapper.md

정의를 열어 보니 tools: Read, Grep, Bash로 Write가 없고, 복잡도 공식과 배치 필드 8개가 들어 있었다.

필드 의미
id 배치 식별자
files 배치에 속한 파일
depends_on 먼저 verified 돼야 하는 배치
complexity 복잡도 점수 (감독자의 우선순위)
pattern 변환 유형
status pending → in_progress → converted → verified | failed
attempts / max_attempts 시도 횟수 / 상한

배치 크기는 원래 10

50이다. 그런데 파일이 17개뿐이라 그대로 두면 배치가 한두 개밖에 안 나오고, 감독자가 일을 나눠 주는 모습을 볼 수가 없다. **분배가 눈에 보이도록 2

4로 줄였다.**

5-2. baseline-tester: "올바른 동작"이 아니라 "지금의 동작"

cat > .claude/agents/baseline-tester.md << 'EOF'
---
name: baseline-tester
description: 마이그레이션 전에 레거시 코드의 현재 동작을 테스트로 박제한다. 변환 전 코드에서 전부 통과해야 한다. 기준선, 박제, 회귀 테스트 키워드에서 사용.
model: sonnet
tools: Read, Grep, Write, Bash
---

## 역할
- src의 export마다 현재 동작을 그대로 기록하는 테스트를 쓴다.
- 기대값은 "올바른 동작"이 아니라 "지금의 동작"이다. 버그도 그대로 박제한다.

## 입력
- src/ 전체
- batches.json (manual-queue에 들어간 파일은 제외)

## 절차
1. 파일마다 export 목록을 뽑는다.
2. 외부 호출(fetch 등)은 vi.fn으로 막고, 무엇이 어떤 인자로 호출되는지까지 기록한다.
3. tests/baseline/에 테스트 작성. import 경로에 확장자를 쓰지 않는다 (.jsx → .tsx 후에도 그대로 동작하도록).
4. npm test 실행. 레거시 코드에서 전부 통과해야 한다.

## 출력
- tests/baseline/*.test.js
- 통과 결과 요약 (파일별 테스트 수)

## 에러 핸들링
- src 파일은 읽기만 한다. Edit이 없다.
- 실행하면 문제가 생기는 동작(예: 무한 재귀)은 "그 문제가 일어난다"를 기대값으로 박제한다.
- 레거시에서 통과하지 않는 테스트는 기준선이 아니다. 통과할 때까지 테스트를 고친다.
EOF

이 에이전트를 쓰면서 마이그레이션 검증의 핵심이 뭔지 정리됐다. "동작이 안 바뀌었다"를 확인하려면, 바꾸기 전 동작을 먼저 사진 찍어 둬야 한다. 순환 의존 파일이 스택을 터뜨리면 "스택이 터진다"가 정답이다.

작은 요령 하나: import 경로에 확장자를 안 쓰게 했다. 그래야 .jsx가 .tsx로 바뀌어도 테스트를 한 글자도 안 고쳐도 된다.

5-3. migration-worker: 유일하게 손을 가진 팀원

cat > .claude/agents/migration-worker.md << 'EOF'
---
name: migration-worker
description: 감독자에게 배치 하나를 받아 그 배치의 파일만 JavaScript에서 TypeScript로 변환한다. 여러 명이 동시에 일한다. 변환, 마이그레이션 워커 키워드에서 사용.
model: sonnet
tools: Read, Grep, Write, Edit, Bash
---

## 역할
- 받은 배치의 파일만 변환한다. 다른 배치 파일은 건드리지 않는다.
- 동작은 바꾸지 않는다. 버그도 그대로 옮긴다.

## 입력
- 배치 하나 (id, files, pattern)
- 재시도라면 이전 검증 실패 사유

## 변환 규칙
1. .jsx → .tsx (JSX가 없으면 .ts). git mv로 이름을 바꿔 이력을 남긴다.
2. 함수 인자와 반환값에 타입을 붙인다. 모르면 unknown으로 두고 any는 쓰지 않는다.
3. import 경로는 확장자 없이 유지한다.
4. 로직, 문자열, 호출 순서는 그대로 둔다.

## 절차
1. 배치 파일과, 그 파일이 import하는 이미 변환된 파일의 타입을 읽는다.
2. 변환 규칙대로 바꾼다.
3. npx tsc --noEmit으로 이 배치 파일에 오류가 없는지 확인한다.
4. 감독자에게 변환한 파일 목록과 특이사항을 돌려준다.

## 에러 핸들링
- tests/ 아래 파일은 절대 수정하지 않는다.
- 동작을 바꿔야만 타입이 맞는 경우 변환을 멈추고 감독자에게 사유를 보고한다.
- git commit 금지.
EOF

리뷰 팀 refactorer 때처럼, 손을 가진 팀원일수록 제약을 많이 걸었다. 특히 마지막 탈출구 — "동작을 바꿔야만 타입이 맞으면 멈추고 보고" — 를 꼭 넣고 싶었다. 억지로 타입을 맞추다 로직을 건드리는 게 마이그레이션에서 제일 조용히 틀리는 방식이라서다.

5-4. migration-verifier: 판정 순서를 고정했다

cat > .claude/agents/migration-verifier.md << 'EOF'
---
name: migration-verifier
description: 변환이 끝난 배치 하나를 검증해 PASS 또는 FAIL만 판정한다. 직접 고치지 않는다. 검증, 배치 검증 키워드에서 사용.
model: sonnet
tools: Read, Grep, Bash
---

## 역할
- 배치 하나가 변환 규칙을 지켰고 동작이 그대로인지 판정한다.

## 판정 순서 (반드시 이 순서로)
1. npm run typecheck — 이 배치 파일에서 타입 오류가 있으면 FAIL
2. npm test — 기준선 테스트 중 하나라도 실패하면 FAIL (동작이 바뀜)
3. git diff로 이 배치 외 파일이나 tests/가 바뀌었으면 FAIL (경계 침범)
4. any 사용, 로직 변경 흔적이 있으면 FAIL
5. 모두 통과하면 PASS

## 출력
- verdict: PASS | FAIL
- 실패 단계 번호, 파일:라인, 근거 (도구 출력 인용)

## 에러 핸들링
- Edit, Write가 없다. 직접 고치지 않는다.
- 도구 출력 없이 판정하지 않는다.
EOF

boundary-verifier를 고치면서 얻은 교훈 — 판정 에이전트에는 판정의 뜻이 아니라 순서를 준다 — 를 처음부터 적용했다. 그리고 boundary-verifier와 비교해 보니 차이가 분명했다.

  boundary-verifier (구현 팀) migration-verifier (이번)
판정 PASS / FIX / REDO PASS / FAIL
되돌아갈 곳 구현 또는 설계 워커뿐 (설계 문서가 없음)
기준 설계 문서 두 개 기준선 테스트 하나
모델 opus sonnet

REDO가 없는 이유는 간단하다. 되돌아갈 설계가 없다. 기준이 기준선 테스트 하나라 PASS/FAIL이면 충분하다. "도구 출력 없이 판정하지 않는다"는 리뷰 팀에서 haiku 담당이 근거 없이 지적하던 걸 보고 넣은 줄이다.


6. 감독자 스킬: 이번 실습의 핵심

mkdir -p .claude/skills/legacy-migration
cat > .claude/skills/legacy-migration/SKILL.md << 'EOF'
---
name: legacy-migration
description: 레거시 코드 대규모 마이그레이션을 감독자 패턴으로 진행한다. "마이그레이션해 줘", "TypeScript로 바꿔 줘" 요청에서 사용.
---

# 레거시 마이그레이션 감독자

메인 세션이 감독자다. 직접 변환하지 않는다. 대기열을 관리하고 일을 나눠 주고 판정을 받는다.

## 상태 파일
- _workspace/migration/batches.json — 진행 상태의 유일한 기준. 상태가 바뀔 때마다 즉시 저장한다.
- _workspace/migration/manual-queue.md — 사람이 봐야 할 파일과 사유
- 배치 status: pending → in_progress → converted → verified | failed

## 1단계: 의존 매핑
- dependency-mapper에게 target_glob "src/**/*.jsx"를 넘긴다.
- 돌려받은 batches.json 초안을 저장한다. max_attempts가 비어 있으면 3.
- 파싱 실패 파일은 manual-queue.md에 기록한다.
- cyclic 경고가 붙은 배치를 사용자에게 알린다.

## 2단계: 기준선 테스트
- baseline-tester를 부른다.
- npm test가 레거시 코드에서 전부 통과해야 3단계로 간다. 하나라도 실패하면 멈춘다.

## 3단계: 점진 변환 (감독자 루프)
반복한다, pending 배치가 없어질 때까지:
1. 시작 가능 배치 = status가 pending이고 depends_on의 모든 배치가 verified인 것
2. 비어 있는 워커 자리(최대 3)만큼 시작 가능 배치를 꺼낸다. complexity가 높은 것부터.
3. 꺼낸 배치를 in_progress로 바꾸고 저장한 뒤 migration-worker에게 하나씩 넘긴다.
4. 워커가 돌아오면 converted로 바꾸고 4단계 검증으로 보낸다.
5. 워커 자리가 비면 바로 1로 돌아가 다음 배치를 꺼낸다. 다른 워커를 기다리지 않는다.

## 4단계: 배치별 검증 (생성-검증 루프)
- migration-verifier에게 배치를 넘긴다.
- PASS → verified로 저장. 이 배치를 기다리던 배치들이 시작 가능해진다.
- FAIL → attempts + 1
  - attempts < max_attempts: 실패 사유를 붙여 pending으로 되돌린다 (다시 대기열로)
  - attempts = max_attempts: 그 배치 파일을 git checkout으로 되돌리고 failed로 저장, manual-queue.md에 기록.
    이 배치에 의존하는 배치들도 failed로 막고 사유를 남긴다.

## 끝
- verified / failed / manual-queue 건수를 사용자에게 보고한다.
- 커밋은 하지 않는다.
EOF

쓰고 나서 다시 읽어 보니 감독자 패턴을 결정하는 건 결국 세 줄이었다.

  1. "다른 워커를 기다리지 않는다." 이 한 줄이 팬아웃과 감독자를 가른다. 팬아웃은 셋이 다 끝나야 다음 라운드, 감독자는 끝난 자리부터 바로 채운다. 이 줄이 빠지면 그냥 라운드제 팬아웃이 된다.
  2. 시작 조건은 converted가 아니라 verified. 처음엔 converted로 썼다가 고쳤다. 검수 안 된 기초 위에 2층을 올리면, 아래가 나중에 FAIL 났을 때 위까지 다 다시 해야 한다.
  3. FAIL은 재배정, 상한이면 되돌리고 위층까지 막는다. 반쯤 바뀐 파일을 남기지 않고, 깨진 기초 위에 쌓지 않는다.

complexity 높은 것부터 꺼내는 것도 의도가 있다. 오래 걸리는 일을 먼저 출발시켜야 마지막에 한 워커만 혼자 붙잡고 있는 꼬리가 짧아진다.

6-1. 상태 파일 하나로 끝까지

구현 팀 때까지는 진행 상태가 Phase 산출물에 흩어져 있었다. 이번엔 수천 번 반복이라 그러면 안 된다. 모든 진행 상태를 batches.json 하나에 두고, 바뀔 때마다 즉시 저장하게 했다. 세션이 끊기면 새 세션이 이 파일만 읽고 이어 가면 되고, 나도 이 파일 하나만 열면 진행률을 안다.

pending ──▶ in_progress ──▶ converted ──▶ verified
   ▲                                          
   └──── FAIL (3회 미만, 실패 사유 첨부) ───────┘
                    FAIL 3회째 ──▶ failed (되돌림 + manual-queue + 위층 차단)

7. 결재함 만들고 검사하기

mkdir -p _workspace/migration tests/baseline
echo "_workspace/" >> .gitignore
echo "node_modules/" >> .gitignore
find CLAUDE.md .claude tsconfig.json -type f | sort
ls package.json tsconfig.json
claude plugin validate .claude/agents

여기서 하나 당황한 게 있었다. 에이전트가 제대로 등록됐는지 보려고 /agents를 쳤는데 안 먹었다. 이 버전에선 /agents 마법사가 빠졌다. /list-agents는 에이전트가 아니라 세션 목록이었다. 결국 이렇게 확인했다.

  • 세션에서 @mig, @dep, @base를 치면 자동완성에 (agent) 표시가 붙은 항목 4개가 뜨는지
  • 파일 형식은 claude plugin validate .claude/agents로

그리고 리뷰 팀 때 데인 기억이 있어서 이번엔 처음부터 지켰다. 정의 파일이 있는 legacy_migration 폴더에서 새 세션을 연다. 세션 중에 폴더를 옮겼다가 에이전트가 정식 로드되지 않아 tools 제한이 무시됐던 사건 — verifier의 "Edit 없음"이 그냥 부탁이 돼 버리면 이 하네스는 의미가 없다.

cd ~/Desktop/side/harness/legacy_migration
claude

처음 돌릴 땐 shift+tab으로 auto mode를 꺼서 단계마다 눈으로 확인하고, 진행 중엔 /tasks로 워커와 모델을 볼 생각이다.

src를 TypeScript로 마이그레이션해 줘

8. 돌리기 전에 짚어 본 것들

설계를 마치고 다시 훑어보니 걸리는 데가 몇 군데 있었다. 실행하면서 실제로 터지는지 같이 보려고 적어 둔다.

# 걸리는 점 왜 대응 후보
1 병렬 워커 간 오판정 워커 3명이 같은 작업 트리에서 일한다. verifier의 git diff에 다른 워커의 변경이 섞이면 "경계 침범" FAIL이 잘못 나올 수 있다. npm test도 남의 반쯤 바뀐 파일 때문에 깨질 수 있다 diff를 git diff -- <배치 파일>로 한정, 또는 검증만 직렬화
2 truncated.jsx와 타입 체크 allowJs: true라 tsc가 잘린 JS도 파싱한다. 워커가 돌리는 npx tsc --noEmit이 매번 구문 오류를 볼 수 있다 manual-queue 파일을 tsconfig.exclude에 추가
3 git mv 뒤 되돌리기 이름을 바꾼 뒤 git checkout만으로는 원래 .jsx가 안 돌아오고 .tsx가 남을 수 있다 원래 경로 restore + 새 파일 삭제를 절차로 명시
4 컴포넌트 테스트 의존성 깐 건 typescript, vitest뿐. 컴포넌트를 렌더링하는 기준선이라면 react 등이 더 필요할 수 있다 샘플 import 보고 사전 설치
5 차단 범위 "의존 배치도 failed"가 바로 위층만인지 그 위 전부인지 애매하다 그래프 상위 전체를 막는다고 명시

9. 실행하면 확인할 것

단계 확인 항목 결과
1. 의존 매핑 내가 그린 층 구조와 맞는가 / 순환 두 파일이 한 배치 + 경고 / truncated → manual-queue ☐
2. 기준선 레거시에서 전부 통과 / 순환 파일의 "스택 터짐"이 박제되는가 ☐
3. 점진 변환 1층부터 나가는가 / 한 워커가 끝나자마자 다음 배치가 나가는가 / 남의 배치 파일을 안 건드리는가 ☐
4. 검증 FAIL이 사유와 함께 대기열로 돌아가는가 / 3회째면 되돌려지고 위층이 막히는가 ☐
전체 batches.json만 보고 진행 상황을 알 수 있는가 ☐
8장 걸리는 점 1~5 중 실제로 터진 것 ☐

10. 세 팀을 나란히 놓고 보니

  리뷰 팀 구현 팀 마이그레이션 팀
일의 모양 한 번 보고 끝 단계마다 다른 일 같은 일 수천 번
주 패턴 팬아웃 & 팬인 Phase별 조합 감독자 + 생성-검증
분배 처음에 한 번 Phase마다 팀 교체 워커가 빌 때마다
진행 상태 보고서 Phase 산출물 상태 파일 하나
검증 기준 정의 파일의 기준 설계 문서 기준선 테스트
워커 간 대화 없음 필요 (팀 모드) 없음 (서브에이전트)
verifier 모델 — opus sonnet

리뷰 팀이 한 장면을 잘 찍는 법, 구현 팀이 장면을 이어 붙여 영화를 만드는 법이었다면, 이번 팀은 같은 장면을 수천 번 찍어도 흔들리지 않게 촬영장을 운영하는 법이었다.

11. 정리

  1. 일의 모양이 패턴을 정한다. 반복·순서·불균형·중단, 이 네 가지가 감독자를 불렀다.
  2. 감독자의 본질은 대기열 + 당겨 가기 + 상태 파일이다. 처음에 나누지 않고, 빌 때마다 시작 가능한 일을 준다. "다른 워커를 기다리지 않는다" 한 줄이 팬아웃과의 차이다.
  3. 합격 기준을 기계가 판정할 수 있게 만든다. 지금의 동작을 버그까지 기준선 테스트로 박제하니 verifier가 sonnet + PASS/FAIL로 충분해졌다.
  4. 검수 끝난 것 위에만 쌓는다. 시작 조건은 verified.
  5. 실패는 멈추고 기록하고 사람에게. 상한 → 되돌림 → 위층 차단 → manual-queue.
  6. 앞에서 배운 건 그대로 재사용된다. 같은 뼈대의 정의 파일, tools로 권한 분리, 판정 순서 명시, 커밋은 사람, 정의 파일 위치에서 새 세션.

다음엔 실제로 돌려서 batches.json과 manual-queue.md를 들고 9장 표를 채우고, 8장에서 걸렸던 점들이 실제로 터지는지 확인할 생각이다. 일부러 FAIL을 유도해서 3회 차단까지 가는지도 볼 예정이다.


참고

728x90
  Comments,     Trackbacks