AIDive

Shopify 4개 게이트를 Claude Code로 재현, 테스트만 통했다

AIDive · 게시

코딩 에이전트자동화와 워크플로

인트로: 끝났다는 말을 거부하는 게이트

Shopify가 말하는 게이트는 AI가 쓴 변경이 다음 단계로 넘어가기 전에 반드시 통과해야 하는 검사예요. Claude Code로 다시 만들어 보니 첫 번째 게이트인 인수 테스트만 효과가 있었어요. Shopify는 게이트 네 개를 공개했지만 숫자는 내놓지 않았어요. 그래서 이 실험은 실제 저장소 위에 게이트를 직접 만들고, 게이트를 하나씩 끈 채로 요청 쉰 개를 실행해서 각 게이트가 무엇을 잡았고 얼마가 들었는지 세어 봤어요.

Helix는 2026년 9월 21일에 공개됐어요. 모든 체크포인트가 테스트, 설계 리뷰, 리뷰어 둘, 엔지니어 순서로 네 개의 게이트를 지나요. 글에는 합격률이 없어요. 많은 사람이 따라 하는 부분은 리뷰어인데, 이 쉰 번의 실행에서 합격률은 테스트가 루프에 있을 때만 움직였어요. 리뷰어는 비용이 약 세 배인데도 보탠 것이 없었어요.

같은 벤치에서 두 가지가 더 나왔어요. 무시할 수 없다던 훅의 공식 문서에 출구가 세 개나 있어요. 그리고 테스트와 리뷰어의 판단이 엇갈리자 에이전트는 아무도 예상하지 못한 행동을 했어요. 세기는 게이트가 전혀 없는 경우부터 시작해요.

게이트 네 개, 숫자는 0, 벤치 하나

벤치 저장소는 실제 문서 데이터베이스인 tinydb이고, 사용한 커밋에서는 테스트 226개가 모두 통과해요.

Helix는 Shopify가 모바일 앱을 네이티브 코드로 옮기려고 쓰는 내부 도구예요. 글은 규칙을 직설적으로 적어요. 각 체크포인트는 네 개의 게이트를 순서대로 지나요.

게이트 Shopify 글에서 하는 일
동작 관련된 모든 케이스가 통과해야 하는 테스트를 돌린다
설계 두 번째 모델에게 완벽주의자 설계 리뷰어 역할을 시킨다
리뷰 서로 컨텍스트가 격리된 독립 리뷰어 에이전트 둘. 둘 다 승인할 때까지 루프를 반복한다
엔지니어 사람이 루프를 마무리한다

이 설계를 팔아 주는 문장은 "시도는 틀려도 된다. 하지만 틀리지 않게 될 때까지는 배포될 수 없다."예요. 라이브러리에서는 설계 게이트가 빠지고, 글에 따르면 로직만 바뀌는 변경은 이 게이트를 그냥 건너뛰어요.

글에 없는 것이 숫자예요. Shop 앱은 12주 만에 다시 만들어졌고 Shopify 앱에는 화면이 300개가 넘는다는 것이 장부의 전부예요. 합격률도, 비용도, 놓친 결함의 수도 없어요.

벤치는 개발자가 평소 치는 식으로 쓴 요청 다섯 개예요. push와 pull 연산, between 쿼리, 테이블 이름 변경, 정렬 검색, 쓰기 캐시 크기예요. 각 요청 뒤에는 에이전트가 절대 보지 못하는 숨은 요구사항이 다섯 개씩 있어요. 신중한 시니어 엔지니어라면 기대할 만한 것들이에요. 모두 합쳐 요구사항이 25개예요.

워커는 헤드리스로 돌린 Sonnet 5.5예요. 구성 다섯 개에 작업 다섯 개, 반복 두 번이면 50번 실행이에요. 한 가지 단서가 있어요. 이 요청들을 명시적으로 쓰면 한 번에 통과하기 때문에, 프롬프트를 사람들이 실제로 치는 수준으로 줄였어요.

게이트가 아예 없으면 요구사항 50개 중 35개가 통과해요. 70%이고, 완전히 깨끗한 실행은 열 번 중 세 번이에요.

정렬 작업이 놓침이 어떤 모습인지 보여 줘요. 에이전트는 모든 문서를 정렬 필드로 색인했고, 그 필드가 없는 문서가 하나라도 있으면 검색 전체가 죽어요. 요구사항 다섯 개, 통과 0개, 누락된 키 하나예요.

끝났다는 말을 거부하는 훅과 세 가지 출구

Stop hook은 에이전트가 턴을 끝내려 할 때마다 Claude Code가 실행하는 스크립트예요. Anthropic의 가이드가 이 문제를 직접 짚어요. Claude는 작업이 끝난 것처럼 보이면 멈춰요. 돌려 볼 검사가 없으면 "끝난 것처럼 보인다"가 유일한 신호이고, 검증 루프는 여러분 자신이 돼요.

훅은 프로젝트 설정 파일의 hooks, Stop 아래에 타임아웃이 달린 명령 하나로 들어가요. 여기서는 게이트 스크립트를 부르는 한 줄짜리 래퍼예요. 계약은 종료 코드예요. 0은 턴을 끝내게 하고, 2는 막고, 스크립트가 표준 오류로 출력한 내용이 이유로 에이전트에게 돌아가요.

실제로는 이렇게 돌아가요. 에이전트가 변경을 요약하고 멈춰요. 훅이 인수 테스트를 돌리고, 하나가 실패하면 턴은 끝나지 않아요. 에이전트는 실패를 훅 피드백으로 읽어요. 이번에는 타입 오류를 기대했는데 나지 않은 테스트였어요. 코드를 고치고 다시 멈추면, 이번에는 게이트가 조용해요.

프롬프트에 적은 규칙은 조언이에요. 훅은 그렇지 않아요. 그런데 같은 문서 페이지가 빠져나가는 길을 세 가지나 적어 놓았어요.

출구 문서의 설명
상한 stop hook이 연속으로 턴을 8번 이어 가게 하면, Claude Code는 다음 차단을 무시하고 턴을 끝낸다. 에이전트가 도구를 부를 때마다 횟수가 초기화되고, 환경 변수로 올릴 수 있다.
타임아웃 명령 훅의 기본값은 600초이고, 타임아웃된 훅은 판단을 내리지 않는다. 느린 테스트 스위트는 게이트를 저절로 열어 버린다.
플래그 훅 입력에 stop_hook_active가 들어 있고, 세션이 이미 stop hook 때문에 이어지는 중이면 true다. 풀 수 없는 조건 때문에 계속 막지 않도록 확인하라고 문서가 말한다.

여기서 만든 게이트는 이 플래그를 기록만 하고 무시해요. 대신 차단 6번 뒤에 스스로 포기하게 해서 루프를 셀 수 있게 했어요.

문서에 없는 출구가 하나 더 있어요. 한 번 막히자 에이전트는 테스트를 찾아 나섰어요. 훅 스크립트에 cat을 돌렸고, 디스크 전체에서 테스트 파일을 검색했어요. 이걸 막은 건 허용 목록뿐이었어요. 그래서 인수 테스트는 저장소 트리 밖에 두고, 설정에서 훅과 숨은 테스트의 읽기를 거부해요. 에이전트는 실패는 보지만 파일은 끝내 보지 못해요.

게이트 1: 인수 테스트

인수 테스트 게이트는 구현자와 독립적으로 작성된 테스트 묶음이고, "끝남"이 무엇인지를 정의해요. Shopify의 첫 번째 게이트는 에이전트 자신의 테스트가 아니에요. 서브에이전트가 체크포인트마다 통합 테스트 스타일의 케이스를 만들고, 해피 패스를 넘어 엣지 케이스까지 밀어붙이며, 그 테스트가 무엇이 증명된 것인지를 정해요.

그래서 여기서 작성자는 별도의 헤드리스 Sonnet이에요. 변경 전의 요청과 저장소를 읽기 전용으로만 보고, 코드를 실행할 수 없으며, 신중한 시니어가 기대할 동작마다 테스트를 하나씩 쓰라는 지시를 받아요. 누락된 필드, 잘못된 타입, 경계값, 영속성, 캐시, 기존 동작 유지가 그 대상이에요. 테스트는 23개에서 53개, 평균 39개를 약 40초 만에 써요. 파일은 저장소 밖에 놓이기 때문에 테스트가 실패하면 에이전트는 단언문을 읽을 뿐 테스트를 보지는 못해요.

테스트만으로 합격률이 70%에서 84%로 올라요. 요구사항 50개 중 42개이고, 완전히 깨끗한 실행은 세 번에서 여섯 번으로 늘어요.

작업 테스트 없음 테스트 있음
정렬 검색 10 중 5 10 중 10
쓰기 캐시 크기 10 중 8 10 중 10
push와 pull 10 중 4 10 중 4

정렬 작업에서 필드가 없을 때 죽는 문제는 작성자가 던질 법한 바로 그 종류의 질문이에요. 캐시 작업에서는 작성자가 캐시 크기가 1보다 작으면 값 오류를 내라고 요구했어요.

push와 pull 작업은 움직이지 않아요. 요구사항 세 개를 놓치는데, 그중 둘은 필드가 없는 문서에 관한 것이고 요청에는 그에 대한 말이 없었어요. 작성자는 프롬프트가 침묵하는 바로 그 지점에서 얼버무려요. 테스트가 예외도 조용한 무동작도 모두 받아들이기 때문에, 누구도 말하지 않은 요구사항은 게이트가 되살릴 수 없어요.

테스트가 있는 게이트 실행 30번 동안, 게이트는 실제 요구사항이 실패한 채로 에이전트를 28번 되돌려 보냈고, 요구사항이 모두 통과한 상태에서도 5번 되돌려 보냈어요. 그 5번 중 하나가 이름 변경 작업이었어요. 작성자가 문자열이 아닌 이름을 거부하라고 요구했는데 아무도 그런 요청을 하지 않았고, 에이전트는 이 때문에 한 라운드를 잃었어요.

이 게이트의 첫 버전은 구현자 자신의 테스트를 다시 돌렸어요. 숨은 요구사항이 실패하는데도 모든 실행을 첫 멈춤에서 통과시켰어요. 에이전트의 테스트는 자기가 만든 것을 테스트하기 때문이에요.

게이트의 값은 한 번 실행에 $0.085가 아니라 $0.20이고, 33초가 아니라 60초예요.

게이트 3: Shopify가 쓴 대로의 리뷰어 둘

리뷰 게이트는 각자 새 컨텍스트에서 일하는 리뷰어 에이전트 한 쌍이에요. Shopify의 표현은 이래요. 독립적이고 컨텍스트가 격리된 리뷰어 에이전트 둘이 새 코드를 검사하고, 모든 발견 사항은 고쳐야 하며, 두 리뷰어가 모두 승인할 때까지 루프를 반복해요. Anthropic의 가이드는 격리가 중요한 이유를 설명해요. 새 컨텍스트의 리뷰어는 변경을 만든 추론이 아니라 diff와 여러분이 준 기준만 봐요.

여기서 각 리뷰어는 읽기 전용 도구를 쓰고 턴이 최대 15번인 헤드리스 Sonnet이에요. 판정은 구조화된 JSON으로 돌려줘요. approve 플래그와 발견 사항이고, 각 발견 사항의 심각도는 blocking 또는 minor예요. 리뷰어 둘이 요청과 전체 diff를 보고 병렬로 돌고, 발견 사항을 합치며, 한쪽이라도 blocking을 찾으면 게이트가 막아요. 한 쌍은 멈춤 한 번에 3센트에서 6센트가 들어요.

구성 통과한 요구사항 합격률 차단
게이트 없음 50 중 35 70% 없음
리뷰어만 50 중 34 68% 열 번 중 1번
테스트만 50 중 42 84% 열 번 중 6번 (정당 3, 잘못 3)
테스트 뒤 리뷰어 50 중 41 82% 열 번 중 2번, 둘 다 정당

리뷰어만 쓰면 게이트가 아예 없을 때보다 2점 낮고, 테스트 뒤에 리뷰어를 두면 테스트만 쓸 때보다 2점 낮아요.

수치를 설명해 주는 것이 정렬 작업이에요. 한 번의 실행에서는 리뷰어 둘이 모두 막았어요. 정렬은 문서를 색인하고, 그 필드가 없는 문서가 하나라도 있으면 키 오류가 나서 검색 전체가 실패해요. 에이전트는 이를 고쳤고 다섯 개 중 다섯 개를 통과했어요.

다른 두 번의 실행에서는 리뷰어 둘이 똑같은 크래시를 보고도 minor로 평가했어요. 정렬 필드가 없는 문서는 그냥 키 오류를 내고, 독스트링이 이를 문서화해 두었다는 이유였어요. 리뷰어 둘이 모두 승인했고, 두 실행 모두 다섯 개 중 0개로 끝났어요. 같은 모델, 같은 diff, 같은 버그인데 심각도 판정이 둘로 갈렸어요. 리뷰어는 막았을 때 틀린 적이 없어요. 문제는 그들이 통과시키는 것이에요.

8월의 논문이 공개 벤치마크에서 같은 모양을 측정했어요. 제로샷은 77%, 리뷰어 하나는 77%, 독립 리뷰어 둘은 75%로, 모두 리뷰가 전혀 없는 경우와 오차 범위 안이에요.

LiveCodeBench 설정 점수
제로샷 77%
리뷰어 하나 77%
독립 리뷰어 둘 75%
리뷰어와 크리틱 (적대적 리뷰) 87%

리뷰어와 크리틱, 끝나지 않는 루프

적대적 리뷰는 리뷰어와 크리틱을 짝지어, 메인 에이전트가 수정하기 전에 구조화된 반박으로 크리틱이 리뷰를 검증하게 하는 방식이에요. 같은 논문은 효과가 있는 건 이 모양이라고 밝혔어요. 리뷰어 둘이 75%였던 곳에서 87%를 기록했고, 가장 강한 에이전트 다섯 개 기준선의 82%도 넘었어요.

벤치마크 제로샷 적대적 리뷰 적용 비용
SWE-bench 71.6 75.2 토큰 약 4.5배

그래서 크리틱을 게이트에 넣었어요. 리뷰어 A가 리뷰해요. 크리틱 B는 발견 사항과 diff를 받아서 각 항목에 인정 또는 기각 도장을 찍어요. 인정된 blocking 발견 사항만 게이트를 닫아요.

이 저장소에서 결과는 50 중 39, 즉 78%였어요. 크리틱 쌍은 열 번의 실행에서 한 번도 막지 않았어요. 검증 중에는 리뷰어 둘 구성이었다면 막았을 거짓 발견 사항을 실제로 기각했어요.

크리틱이 고치지 못한 것은 루프예요. 세 번의 실행이 같은 인수 테스트에서 차단 6번에 도달했고, 게이트는 실제 요구사항이 멈출 때마다 계속 실패한 채로 포기했어요. 그중 두 번은 막은 테스트가 작성자 자신의 실수였고, 에이전트도 그렇게 말했어요. 읽을 수 없는 테스트 때문에 코드를 고치지는 않겠다고요.

세 번째 경우를 기억해 둘 만해요. 인수 테스트는 실패한 update가 모든 문서를 건드리지 않은 채로 남기기를 원했고, 그래서 에이전트는 테이블의 update 메서드를 다시 썼어요. 리뷰어가 이를 막았어요. update 메서드 변경은 요청에 없었고 작업은 새 함수 두 개였기 때문이에요. 에이전트는 되돌렸어요. 마지막 메시지는 인수 테스트와 코드 리뷰가 서로 반대되는 것을 요구해서 아무것도 바꾸지 않고 멈췄다는 내용이었어요. 차단 6번 뒤에 카운터가 에이전트를 내보내 줬어요.

여기서 포기 지점은 6번이에요. Claude Code 자체의 상한은 연속 차단 8번이고 도구를 부를 때마다 초기화되기 때문에, 쉰 번의 실행에서 한 번도 작동하지 않았어요.

게이트 4는 여러분: 사람에게 닿은 것, 청구서, 키트

네 번째 게이트는 엔지니어이고, Shopify의 글은 지켜보는 사람이 없을 때도 게이트가 느슨해지지 않는다고 약속해요. 엔지니어에게 닿는 것은 다른 게이트가 통과시킨 모든 것이에요. 아래 장부는 어느 게이트가 제값을 했는지에 답해요.

구성 50 중 통과 합격률 실행당 비용 사람에게 남은 것
게이트 없음 35 70% $0.085 15
인수 테스트 42 84% $0.20 8
리뷰어만 34 68% $0.15 16
테스트와 리뷰어 41 82% $0.28 9
테스트와 리뷰어, 크리틱 39 78% $0.28 11

게이트가 있는 줄은 모두 실행당 54초에서 86초가 걸려요. 게이트가 없으면 33초예요.

쉰 번의 실행 전체에서 차단은 이렇게 나뉘어요. 테스트는 에이전트를 정당하게 28번, 잘못 5번 돌려보냈어요. 리뷰어는 3번 막았고 잘못 막은 적은 없으며, 루프 세 개가 포기했어요. 전체 매트릭스는 CLI가 보고하는 정가 기준 API 비용으로 $9.95, 워커 시간 합계로는 약 3,136초, 즉 52분이 들었어요.

이런 저장소에 대한 결론은 이래요. 테스트 게이트는 남기되, 구현자가 아닌 작성자를 쓰고 에이전트가 수정할 수 없는 테스트를 쓰세요. 여섯 번 중 한 번꼴로 잘못된 차단이 나온다고 예상하고, 루프에는 카운터를 달아 두세요. Shopify가 쓴 대로의 리뷰어 둘 게이트는 어느 모양으로도 여기서 합격률을 움직이지 못했어요. 남겨 둔다면 장벽이 아니라 피드백 속 조언으로 두세요.

가는 길의 함정이 하나 있어요. 문서는 스크립트 호출에서 훅, 스킬, 플러그인, 메모리를 건너뛰는 맨 플래그를 설명해요. 구독 로그인에서는 이 플래그가 API 키만 읽고 오류를 돌려줘요. 그래서 각 리뷰어는 대신 빈 설정 소스와 비활성화된 훅으로 격리했어요.

한계는 분명해요. 저장소 하나, 워커 모델 하나, 작업 다섯 개, 칸당 반복 두 번이에요. 줄 사이의 2점 차이는 잡음이에요. 게이트 없음과 테스트 게이트 사이의 14점은 잡음이 아니에요. 설계 게이트는 만들지 않았고, 엔지니어의 시간은 재지 않았어요.

Shopify의 네 게이트 중 이 저장소에서 제값을 한 건 테스트뿐이었어요. 키트 전체는 무료 팩에 있어요. 훅과 거부 목록이 든 설정 파일, stop 스크립트, 게이트, 스키마가 붙은 작성자와 리뷰어 명령, 그리고 장부 템플릿이에요.

무료 자료

출처

자주 묻는 질문

Shopify의 AI 게이트 네 개 중 Claude Code로 다시 만들 가치가 있는 건 무엇인가요?
인수 테스트 게이트예요. tinydb에서 50번 실행하는 동안 합격률을 70%에서 84%로 올렸고, 리뷰어 둘 게이트는 합격률을 움직이지 못했어요(단독 68%, 테스트 뒤 82%).
테스트가 통과하기 전에 Claude Code가 끝났다고 말하지 못하게 하려면 어떻게 하나요?
프로젝트 설정에 인수 테스트를 돌리는 Stop hook을 추가하고, 실패하면 종료 코드 2로 끝내세요. Claude Code는 턴 종료를 막고, 스크립트가 표준 오류로 쓴 내용을 이유로 에이전트에게 돌려줘요.
Stop hook으로 Claude Code가 계속 일하도록 완전히 강제할 수 있나요?
완전하지는 않아요. hooks 문서에는 도구를 부를 때마다 초기화되는 연속 8번 차단 상한, 판단을 내지 않는 기본 600초 타임아웃, 확인하라고 안내하는 stop_hook_active 플래그가 적혀 있어요. 루프가 예측 가능하게 끝나도록 직접 차단 카운터를 추가하세요.
독립적인 AI 코드 리뷰어 둘이 에이전트의 결과물을 개선하나요?
이 테스트에서는 아니었어요. Sonnet 리뷰어 둘은 단독으로 68%, 테스트 뒤에 82%로, 각 기준선보다 2점 낮았어요. 두 번의 실행에서 실제 크래시를 minor로 평가했기 때문이에요. 한 논문은 LiveCodeBench에서 같은 모양을 측정했어요. 제로샷 77%, 리뷰어 둘 75%, 리뷰어와 크리틱 87%예요.
Claude Code에서 게이트마다 실행당 비용은 얼마인가요?
실행당 정가 기준 API 비용은 게이트 없음 $0.085, 인수 테스트 $0.20, 리뷰어만 $0.15, 테스트와 리뷰어 $0.28이었어요. 게이트가 있는 실행은 33초가 아니라 54초에서 86초가 걸렸어요.
에이전트가 숨은 인수 테스트를 읽지 못하게 하려면 어떻게 하나요?
테스트를 저장소 트리 밖에 두고, 설정에서 훅 스크립트와 테스트 파일의 읽기를 거부하세요. 그러면 에이전트는 실패한 단언문은 보지만 파일은 보지 못해요.

관련 영상