나는 하나의 설정 변경으로 50개의 PR을 망쳤다. 이것이 내가 이를 방지하기 위해 타임머신을 만든 방법이다.

발행: (2026년 3월 9일 PM 12:10 GMT+9)
12 분 소요
원문: Dev.to

Source: Dev.to

번역을 진행하려면 번역하고자 하는 전체 텍스트를 제공해 주시겠어요?
현재는 링크만 포함되어 있어 번역할 내용이 없습니다. 텍스트를 알려 주시면 바로 한국어로 번역해 드리겠습니다.

우리 모두 겪어봤죠

코드 품질을 개선할 때가 되었다고 결심합니다. “프로덕션 코드에 console.log는 더 이상 사용하지 않겠다”고 선언합니다. 간단한 ESLint 규칙을 추가하고, 설정을 푸시한 뒤 머지합니다.

10분 후, 슬랙이 폭발합니다.

  • “왜 내 PR에서 빌드가 실패하나요?”
  • “핫픽스를 배포할 수 없어요!”
  • “누가 재미 경찰을 켰나요?”

당신은 위반이 얼마나 널리 퍼져 있는지 몰라서 50개의 열린 풀 리퀘스트를 깨버렸습니다. 변경을 되돌리고 사과하지만 코드베이스는 여전히 지저분합니다.

새로운 규칙을 적용하면서 발생하는 혼란, 즉 **“정책 충격”**에 대한 두려움 때문에 많은 팀이 거버넌스를 강화하는 것을 꺼립니다.

하지만 시간 여행을 할 수 있다면 어떨까요? 머지하기 전에 레포의 최근 100개의 PR에 대해 새로운 규칙을 테스트할 수 있다면 어떨까요?

우리가 바로 그것을 만들었습니다. 아래는 GitHub용 정책 영향 시뮬레이터를 어떻게 만들었는지에 대한 기술적인 깊이 있는 탐구입니다.

문제: 거버넌스는 추측 게임

대부분의 CI/CD 파이프라인은 이진 형태입니다: 통과 또는 실패. 새로운 검사를 도입하면 즉시 모든 것에 적용됩니다. “구매 전에 시도해 보기” 같은 것은 없습니다.

우리는 다음을 할 수 있는 시스템이 필요했습니다:

  1. Draft a policy (e.g., “Max PR size: 20 files”). → 정책을 초안 작성하기 (예: “최대 PR 파일 수: 20개”).
  2. Fetch historical data (snapshots of past PRs). → 과거 데이터 가져오기 (과거 PR의 스냅샷).
  3. Replay the draft policy against that history. → 그 기록에 대해 초안 정책을 재생하기.
  4. Visualize the Blast Radius—how many legit PRs would have been blocked? → Blast Radius시각화하기—얼마나 많은 정상 PR이 차단되었을지?

Source:

Architecture

우리는 Node.js 백엔드(Express)와 React 프런트엔드를 사용해 이 시스템을 구축했습니다. 핵심 로직은 우리의 타임머신 역할을 하는 PolicySimulationService에 있습니다.

1. The Snapshot Engine

첫 번째 과제는 데이터를 얻는 것입니다. 레포를 복제하고 npm install을 100번 실행하는 것은 너무 느립니다. 대신 GitHub API를 통해 가벼운 메타데이터 스냅샷을 가져옵니다.

우리는 PR을 다음과 같은 사실들의 모음으로 취급합니다:

  • files_count
  • 사용된 extensions (.ts, .js, .py 등)
  • test_coverage 비율
  • Diff 통계(추가 / 삭제)
// backend/src/services/policySimulation.service.js

async function collectSnapshots(repo, daysBack) {
  // 1. Fetch merged PRs from the last N days
  const prs = await github.fetchHistoricalPRs(repo, daysBack);

  // 2. Extract lightweight "Fact Snapshots"
  return prs.map(pr => ({
    id: pr.number,
    files_count: pr.changed_files,
    has_tests: pr.files.some(f => f.filename.includes('.test.')),
    extensions: [...new Set(pr.files.map(f => path.extname(f.filename)))],
    // ... other metadata
  }));
}

코드를 메타데이터 facts 로 추상화함으로써 파일 시스템에 접근하지 않고도 수천 개의 시뮬레이션을 몇 초 안에 실행할 수 있습니다.

2. The Simulation Loop (The “Judge”)

스냅샷을 확보하면 이를 평가 엔진에 전달합니다. 바로 여기서 마법이 일어나며, 이 컴포넌트를 The Judge 라고 부릅니다.

Judge는 Draft Policy(JSON 로직)와 Snapshot을 받아 PASS 또는 BLOCK이라는 평결을 반환합니다.

// The core simulation loop
async function executeSimulation(draftRules, snapshots) {
  const results = {
    blocked: 0,
    passed: 0,
    impacted_prs: []
  };

  for (const snapshot of snapshots) {
    // The Judge evaluates the rule
    const verdict = evaluate(draftRules, snapshot);

    if (verdict === 'BLOCK') {
      results.blocked++;
      results.impacted_prs.push({
        pr: snapshot.id,
        reason: `Violated rule: ${draftRules.type} (Limit: ${draftRules.value})`
      });
    } else {
      results.passed++;
    }
  }

  return results;
}

이 결정적 루프를 통해 임계값을 조정하면—예를 들어 max file count를 20에서 50으로 바꾸면—영향 그래프가 즉시 업데이트되는 것을 확인할 수 있습니다.

3. Front‑end Visualization

프런트엔드에서는 React를 사용해 데이터를 실용적으로 보여줍니다. PolicySimulation 컴포넌트는 사용자가 다음을 할 수 있게 합니다:

  • 대상 레포 선택
  • 초안 정책 구성(예: “리뷰어 2명 요구”)
  • Simulate 버튼 클릭

결과는 Recharts를 이용해 Blast Radius를 시각화합니다.

// frontend/src/components/governance/PolicySimulation.tsx
export const PolicySimulation = () => {
  const [result, setResult] = useState(null);

  // ...setup logic...

  return (
    <div>
      <h2>Simulation Configuration</h2>
      <label>
        Max PR Size
        <input type="number" />
      </label>
      <label>
        Test Coverage
        <input type="number" />
      </label>

      <button onClick={/* simulate */}>Simulate Impact</button>

      {result && (
        <Alert type={result.blast_radius > 50 ? "destructive" : "default"}>
          Blast Radius Alert
          <p>
            This policy would have blocked {result.total_blocked} out of {result.total_scanned} PRs.
            {result.blast_radius > 50
              ? " This is too disruptive!"
              : " Safe to merge."}
          </p>
        </Alert>
      )}
      {/* Charts go here */}
    </div>
  );
};

우리는 의도적으로 “Friction Index” 를 계산합니다. 정책이 과거 PR의 > 20 %를 차단하면 High Friction 으로 표시합니다. 이 간단한 휴리스틱 덕분에 과도하게 공격적인 규칙이 병합되는 일을 수없이 방지할 수 있었습니다.

Lessons Learned

이 도구를 만들면서 개발자 경험(DX)에 대한 세 가지 핵심 교훈을 배웠습니다:

  1. Metadata > Source Code – 고수준 거버넌스 결정을 내리기 위해 전체 AST가 필요할 일은 거의 없습니다. 메타데이터(파일 유형, 크기, 작성자)는 약 80 %의 사용 사례를 커버하며 처리 속도가 약 100× 빠릅니다.
  2. Feedback Loops Matter – 규칙의 영향을 즉시 수 있을 때, 더 나은 규칙을 작성하게 됩니다. 거버넌스는 처벌적인 장벽이 아니라 협업 대화가 됩니다.
  3. Safety‑First Defaults – 기본적으로 우리는 넓은 과거 기간을 시뮬레이션하고 “높은 마찰” 경고를 표시하여 팀이 정책을 실제 적용하기 전에 반복하도록 장려합니다.

TL;DR

Policy Shock은(는) 팀을 마비시킬 필요가 없습니다. 과거 PR을 스냅샷하고, 초안 정책을 재생 테스트하며, 영향을 시각화함으로써 자신 있게 거버넌스 변경을 배포할 수 있습니다. Policy Impact Simulator는 일상적인 개발 흐름을 방해하지 않으면서 표준을 강화할 수 있는 위험 없는 샌드박스를 제공합니다.

Ratic “Gate”를 디자인 문제로 접근하기

  • JSON 스키마는 강력합니다: 정책을 JSON으로 정의하면(하드코딩된 함수 대신) 버전 관리, 차이점 확인, 그리고 가장 중요한 배포 없이 시뮬레이션이 가능합니다.

### Future Work: AI Analysis

우리의 다음 단계는 LLM을 통합해 *왜* 정책이 실패했는지를 설명하도록 하는 것입니다. 단순히 “Blocked”라고 표시하는 대신, 시스템이 PR 설명을 보고 “결제 게이트웨이에 영향을 주지만 ‘Security’ 라벨이 없어서 차단되었습니다.”라고 말하도록 하고 싶습니다.

우리는 `translate-natural-language` 엔드포인트를 사용해 일반 영어(예: “Block PRs with no tests”)를 우리의 JSON 스키마로 변환하는 프로토타입을 실행 중입니다.

```js
// Transforming English to Policy Config
const result = await api.post('/v1/policies/translate-natural-language', {
  description: "Block huge PRs"
});
// Output: { type: "pr_size", max_files: 50 }

Try It Yourself

이 시뮬레이터는 거버넌스를 눈에 띄지 않게, 그리고 도움이 되도록 만드는 더 큰 이니셔티브의 일부입니다.

새로운 린트 규칙이 반란을 일으킬지 고민하는 데 지쳤다면, CI용 간단한 “dry‑run” 스크립트를 만드는 것을 강력히 권장합니다. 최근 50개의 PR을 grep으로 검사하는 기본 스크립트라도 큰 고통을 예방할 수 있습니다.

개발 프로세스를 테스트할 때 어떤 도구를 사용하시나요? 댓글로 알려 주세요—다른 사람들이 “Policy Shock” 문제를 어떻게 해결하고 있는지 보고 싶습니다.

읽어 주셔서 감사합니다! 이 기술적 분석이 도움이 되었다면 별표를 남기거나 아래에 댓글을 달아 주세요.

0 조회
Back to Blog

관련 글

더 보기 »