ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 기존 pre-push 훅을 깨뜨리지 않고 끼워 넣기 — chain mode와 fail-soft 정책
    IT 2026. 6. 2. 21:00
    기존 pre-push 훅을 깨뜨리지 않고 끼워 넣기 — chain mode와 fail-soft 정책

    새 도구를 만들고 그 도구가 git 훅에 자기를 끼워야 할 때, 가장 흔한 실수가 있다. 여기서 git 훅이란 "git이 특정 시점(commit 전, push 전 등)에 자동 실행해 주는 스크립트".git/hooks/ 폴더 안의 실행 파일들이다. 이름이 pre-push인 파일이 있으면 git push 직전에 자동으로 한 번 호출된다. 가장 흔한 실수는 "기존 .git/hooks/pre-push 파일을 그냥 덮어쓴다"는 것이다. 설치 직후엔 잘 동작하는 것 같지만, 사실 사용자가 이전에 쓰고 있던 다른 도구의 훅(예: repo 정책 lint, 로컬 자동화)을 조용히 삭제해 버린 상태다. 사용자가 그걸 깨닫는 건 보통 며칠 뒤, 또 다른 PR이 깨졌을 때다.

    deep-wiki는 22개 자신의 repo에 pre-push 훅을 설치해야 했다. 코드 변경 시 영향 분석을 자동 트리거하기 위해서다. 처음부터 한 가지 정책을 잠갔다 — "기존 훅이 있으면 백업하고, 마지막에 그 백업을 호출한다. 그리고 우리 훅은 절대로 push를 막지 않는다." 두 핵심 키워드의 의미를 풀면 이렇다.

    • chain mode — 기존 훅을 옆 파일로 백업해 두고, 우리 훅 마지막에 그 백업을 호출해 두 훅이 연쇄(chain)로 동작하게 하는 방식이다. 한 자리에 우리만 들어앉지 않고, 기존 사용자를 옆에 동행시킨다.
    • fail-soft — 보안 분야의 fail-secure(실패 시 잠금) 반대 개념. "우리가 실패해도 사용자 작업은 진행되게 한다"는 정책. 우리 훅이 push를 막을 수 있는 경로 자체를 만들지 않는다.

    이 글은 그 정책이 80줄짜리 셸 스크립트 한 파일에 어떻게 녹았는지의 기록이다.

    1. 배경 — 훅 덮어쓰기가 만드는 보이지 않는 손상

    diagram

    두 정책의 차이는 명확하다. 덮어쓰기는 빠르지만 사용자에게 부채를 만든다. 설치 직후엔 보이지 않지만, repo 정책 lint를 우회한 채로 보안 사고 PR이 올라가거나, 로컬 자동화가 침묵하게 된다. 사용자가 며칠 뒤 "왜 이게 안 도는 거지?"라며 발견하는 손상이다. 반면 chain mode는 한 줄 더 복잡하지만 기존 인프라와 평화롭게 공존한다.

    fail-soft도 같은 맥락이다. 우리 훅이 어떤 이유로 실패하더라도 (MCP gateway 다운, 네트워크 오류 등) push 자체를 막아서는 안 된다. deep-wiki 영향 분석이 작동 안 된다는 이유로 사용자 작업이 중단되는 건 받아들이기 어려운 친화성이다. 우리 훅은 push 위에 얹는 정보 layer지, push의 게이트가 아니다.

    2. 핵심 아키텍처 — 80줄 셸 스크립트

    전체 구현은 scripts/install_pr_hook.sh 한 파일이다. 80줄 셸로 끝난다. 한 가지 흥미로운 점은 이 스크립트가 두 가지 일을 동시에 한다는 것이다 — installer 자신, 그리고 설치되는 hook 본체가 같은 파일 안에 있다.

    # scripts/install_pr_hook.sh — D1.4 chain mode + fail-soft
    
    set -euo pipefail
    
    repo="$HOME/projects/${1:?usage: install_pr_hook.sh }"
    hooks_dir="$repo/.git/hooks"
    target="$hooks_dir/pre-push"
    backup="$hooks_dir/pre-push.deep-wiki-prev"
    mkdir -p "$hooks_dir"
    
    # 1. 기존 훅 백업 — 단, 이미 우리 훅이면 skip (idempotent)
    if [ -f "$target" ] && [ ! -f "$backup" ] && \
       ! grep -q "deep-wiki hook v1" "$target" 2>/dev/null; then
        mv "$target" "$backup"
        chmod +x "$backup"
        echo "backed up existing pre-push -> pre-push.deep-wiki-prev"
    fi
    
    # 2. 우리 훅 파일을 새로 쓴다
    cat > "$target" <<'HOOK'
    #!/usr/bin/env bash
    # deep-wiki hook v1 — pre-push (chain mode)
    
    set +e    # 어떤 명령도 우리 스크립트를 중단시키지 않게
    
    remote="$1"; url="$2"
    
    # 변경된 파일 목록
    changed=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || true)
    
    if [ -n "$changed" ]; then
        # MCP gateway 응답이 없으면 그냥 skip — push는 계속
        resp=$(curl --max-time 3 -fsS \
            "http://127.0.0.1:8190/voice/health" 2>/dev/null || echo "")
        if [ -z "$resp" ]; then
            echo "[deep-wiki] gateway not reachable; skipping impact analysis"
        else
            echo "[deep-wiki] changed files:"
            printf '  %s\n' $changed
            echo "[deep-wiki] (simulate_change_impact will wire MCP call)"
        fi
    fi
    
    # 3. 백업 훅 호출 — 있으면 chain, 없으면 skip
    hook_dir="$(dirname "$0")"
    if [ -x "$hook_dir/pre-push.deep-wiki-prev" ]; then
        "$hook_dir/pre-push.deep-wiki-prev" "$@"
        prev_rc=$?
        if [ "$prev_rc" -ne 0 ]; then
            # 백업 훅이 실패해도 우리는 push를 막지 않음
            echo "[deep-wiki] previous pre-push returned $prev_rc (non-blocking)"
        fi
    fi
    
    # 4. 항상 성공으로 종료 — fail-soft 원칙
    exit 0
    HOOK
    
    chmod +x "$target"
    echo "installed deep-wiki pre-push hook at $target"
    

    이 셸 스크립트의 정책이 4단계에 박혔다. (1) 기존 훅이 있고 우리 훅이 아니면 백업한다, (2) 우리 훅 본체를 쓴다, (3) 우리 검증 후 백업 훅을 호출한다, (4) 마지막엔 무조건 exit 0. 각 단계마다 안전장치가 박혀 있다.

    3. 멱등성 — 두 번 설치해도 부서지지 않게

    훅 설치 스크립트의 가장 흔한 운영 부담은 "두 번 실행하면 백업이 자기 자신이 된다"는 함정이다. 멱등성이란 같은 작업을 몇 번 반복해도 결과가 같다는 성질 — 설치 스크립트는 멱등해야 한다. 그렇지 않으면 다음 시나리오가 발생한다. 첫 실행에서 기존 훅이 백업되고 우리 훅이 들어간다. 두 번째 실행에서 다시 "기존 훅"(이미 우리 것)을 백업으로 옮기면, 백업 자리에 우리 훅이 들어가고 우리 훅 자리에는 또 우리 훅이 새로 쓰인다. 백업 ↔ 우리 훅 자리가 뒤바뀌고, 백업 훅 호출이 무한 재귀로 변할 수 있다.

    이 함정을 피하는 한 줄이 grep -q "deep-wiki hook v1" "$target"이다. 우리 훅에는 deep-wiki hook v1이라는 표지 문자열이 첫 줄 주석으로 박혀 있다. 그 표지가 발견되면 "이미 우리 훅이 들어가 있다"를 의미하므로 백업을 skip한다. 추가로 [ ! -f "$backup" ] 검사도 함께 둬서 백업이 이미 있을 때도 skip한다. 두 조건이 함께 멱등성을 만든다.

    4. fail-soft가 정말 fail-soft인가

    diagram

    네 시나리오 모두 push가 허용되는 게 핵심이다. 두 가지 셸 기법이 이 보장을 만든다. set +e는 셸의 기본 동작 — "한 명령이라도 실패하면 스크립트 즉시 종료" — 를 끄는 한 줄이다. 보통 안전을 위해 set -e를 쓰지만, fail-soft 정책에서는 반대로 켜 둬야 한다. 그리고 마지막 exit 0은 무엇이 일어났든 종료 코드를 0(성공)으로 강제 — git은 pre-push 훅이 0이 아닌 값을 반환하면 push를 차단하는데, 이 한 줄로 그 가능성을 닫는다. 우리 훅이 누군가의 push를 막을 수 있는 경로는 존재하지 않는다. 이는 사용자에게 명확한 계약이다 — deep-wiki를 깔아도 당신의 push는 deep-wiki 때문에 막히지 않는다.

    5. 운영 결과 — 9개 repo에 끼워 넣고 충돌 0회

    테스트 삼아서 9개 repo에 일괄 설치했다. 한 줄 명령 — for r in $(cat repos.txt); do scripts/install_pr_hook.sh "$r"; done. 9개 모두 정상 설치하고, 그 중 3개에는 기존 훅(개인 lint, repo 코드 정책)이 있었는데 백업으로 정상 이동했다. 첫 push에서 백업 훅이 정상 호출되는 걸 확인했다.

    한 달 동안 운영해 보니 chain mode가 실제로 가치를 보인 사건이 두 차례 있었다. 한 번은 MCP gateway가 잠시 다운된 상태에서 push했을 때 — "gateway not reachable; skipping impact analysis" 메시지와 함께 push가 정상 진행됐다. 또 한 번은 lint가 어떤 의도하지 않은 사유로 exit 1을 반환했는데 — "(non-blocking)" 경고가 출력되며 push가 계속됐다. 두 경우 모두 fail-soft 정책 덕에 작업이 중단되지 않았다.

    한 가지 부수 효과 — 설치 스크립트가 워낙 단순하니까 uninstall도 쉽다. .git/hooks/pre-push.deep-wiki-prevpre-push로 mv하기만 하면 원상복구. 그 단순성이 22 repo 운영에 큰 자유를 준다 — 언제든 빼낼 수 있는 정책이 깨끗하다.

    마무리 — 끼어드는 도구의 첫 번째 의무

    사용자의 인프라에 끼어드는 도구를 만들 때 가장 먼저 잠가야 할 두 가지 정책이 있다. "기존 것을 잃지 않게 하라." "내가 실패해도 사용자 흐름을 막지 마라." 이 두 가지가 chain mode + fail-soft다. 80줄 셸이면 충분하다.

    1인 운영에서도 그렇지만, 다인 팀에서는 더 결정적이다. 다른 사람의 작업 흐름에 침투하는 도구는 그 흐름의 안전성을 1순위로 둬야 한다. "내 도구가 더 중요해"가 아니라 "네 작업이 멈추지 않게 내가 양보한다"가 신뢰의 시작이다. 이 한 줄 정책을 잠그면, 도구가 사용자에게 부담이 아니라 선택지가 된다.


    이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.

Designed by Tistory.