내가 짠 건 하네스가 아니라 아웃터 루프였다

내가 짠 건 하네스가 아니라 아웃터 루프였다

타임라인에 "루프 엔지니어링(loop engineering)"이라는 단어가 뜬 걸 봤을 때, 나는 반사적으로 이걸 하네스 얘기의 재탕이라고 생각했다. 프롬프트 엔지니어링, 컨텍스트 엔지니어링, 하네스 엔지니어링까지 순서대로 따라온 사람 입장에선 자연스러운 오해다. 접미사가 똑같으니까. 다음 단어가 하나 더 붙었으니 앞 단어의 변형 버전이겠거니 했다.

그래서 대충 넘기려다, 계보를 한 번 되짚어 봤다. 어디서 온 말인지, 앞 세 단어와 뭐가 다른지. 처음엔 "또 하나 지어낸 신조어겠지" 싶은 마음이 반쯤 있었다. 몇 달에 한 번씩 접미사만 바꾼 유행어가 도는 판이니까. 그런데 읽다 보니 이게 하네스의 변형이 아니었다. 하네스 위에 한 층 더 얹히는 다른 층이었다. 접미사가 같아서 옆으로 나란한 단어인 줄 알았는데, 실은 위아래로 쌓이는 관계였다.

그 렌즈를 손에 쥐고 내 blog-studio repo를 다시 열었다. 그리고 좀 이상한 기분이 들었다. 내가 반년 넘게 이름도 모르고 짜 놨던 것들, 오케스트레이터랑 발행 게이트랑 세션 훅 같은 것들이, 사실 전부 아웃터 루프였다. verifier도 stop rule도 이미 코드로 돌아가고 있었다. 나는 그걸 "자동화 스크립트"라고만 불렀지, 루프라고 생각한 적이 없었다.

그런데 되짚으니 새 질문이 하나 생겼다. 내가 얹은 아웃터 루프와, Claude Code가 이미 돌리고 있던 이너 루프의 경계가 생각보다 흐리다는 것. 어디까지가 내가 엔지니어링한 부분이고, 어디부터가 하네스가 그냥 준 것인가. 이 글은 그 경계를 더듬어 본 기록이다.

계보를 한 줄로 되짚으면

먼저 오해부터 풀고 가자. 프롬프트 → 컨텍스트 → 하네스 → 루프. 이 네 단어는 서로를 대체하지 않는다. 아래 단어가 위 단어를 밀어내는 게 아니라, 위에 한 층씩 쌓인다. 적층이지 교체가 아니다.

프롬프트 엔지니어링은 모델에게 한 번 잘 말하는 기술이었다. 같은 질문도 어떻게 묻느냐에 따라 답이 달라지니, 묻는 문장을 다듬는 데 공을 들였다. 컨텍스트 엔지니어링은 그 한 번의 말에 무엇을 얼마나 넣어 줄지를 설계하는 일로 확장됐다. 관련 파일을 어디까지 넣고, 어떤 배경을 앞에 깔고, 불필요한 건 어떻게 쳐낼지. 여기까지는 결국 "모델에게 무엇을 어떻게 주느냐"의 문제다. 입력을 다듬는 일이라는 점에서 둘은 한 계열이다.

하네스 엔지니어링에서 층위가 바뀐다. 모델을 감싸는 구조 전체를 짜는 일이 된다. 여기서 하네스를 CLAUDE.md 파일 하나로 착각하면 안 된다. 하네스는 에이전트 정의와 스킬, 훅, 그리고 CLAUDE.md를 포함한 규칙 문서 전체다. 에이전트가 무슨 역할을 맡고, 어떤 스킬을 언제 부르고, 어떤 이벤트에 어떤 훅이 걸리고, 어떤 규칙을 항상 지키는지. 이 묶음 전부가 하네스다. 나는 이 개념을 이 시리즈의 하네스가 무엇인가를 다룬 글에서 한 번 정리한 적 있는데, 그때도 파일 하나가 아니라 "모델을 일하게 만드는 장치 전체"라는 점을 강조했었다.

이걸 압축한 공식이 하네스 담론에서 한동안 돌았다. 에이전트는 모델 더하기 하네스라는 것. 모델은 갈아 끼울 수 있는 부품이고, 진짜 자산은 그 모델을 감싸는 하네스 쪽에 쌓인다는 얘기다. GPT를 쓰든 Claude를 쓰든 하네스가 잘 짜여 있으면 성능의 상당 부분이 거기서 나온다. 이 감각은 이 시리즈 1편에서 프롬프트 엔지니어링의 다음을 물었을 때부터 어렴풋이 잡혀 있던 거였다.

그럼 루프는 뭔가. 2026년 여름, Addy Osmani가 "Own the Outer Loop"라는 글에서 이 단어에 구조를 줬다. AI Engineer World's Fair 클로징 키노트의 글 버전이다. 골자는 이렇다. 에이전트는 이미 이너 루프를 돈다. 조사하고, 구현하고, 테스트하고, 보고한다. 사람이 소유해야 하는 건 그 바깥의 루프다. 이 일을 할 가치가 있는지 판단하고, 나온 결과의 증거를 검증하고, 승인하거나 막고, 그 결정의 결과를 책임지는 층.

Osmani가 든 근거 중 하나가 오래 남았다. 이미 커밋되는 코드의 42%가 AI가 짰거나 AI 도움을 크게 받은 것이고(Sonar의 2026 State of Code), 와튼 연구에서는 AI가 틀렸을 때조차 사람의 4분의 3 가까이가 그 답을 그대로 받아들였다는 것. 생성은 싸졌는데 검토와 이해와 유지보수는 여전히 비싸다. 그 격차가 벌어진 자리에 아웃터 루프라는 말이 놓인 거다.

한 문장으로 줄이면 이렇다. 하네스가 "모델을 어떻게 일하게 만드느냐"라면, 루프는 "그 일하는 모델을 어떤 리듬으로 돌리고, 언제 멈추고, 누가 결과를 책임지느냐"다. 층이 다르다. 그래서 변형이 아니라 위층이다.

이너 루프는 내가 짜지 않았다

여기서 내가 한동안 헷갈렸던 지점을 짚고 넘어가야 한다. 루프라는 말을 듣고 처음엔 "그래서 내가 루프를 짜야 하나"라고 생각했다. 지각하고, 추론하고, 행동하고, 관찰하고, 다시 추론하는 그 순환 말이다. 그런데 이건 내가 짜는 게 아니었다. 이미 돌고 있었다.

Claude Code에 "이 파일 고쳐 줘"라고 한마디 던지면, 파일을 읽고, 뭘 바꿀지 판단하고, 편집하고, 결과를 확인하고, 어긋나면 다시 손대는 그 사이클이 알아서 돈다. 나는 그 안쪽 회로에 손을 댄 적이 없다.

한 번 천천히 뜯어보면 이렇다. 편집을 시키면 먼저 대상 파일을 읽어서 현재 상태를 지각한다. 어디를 어떻게 바꿔야 요청에 맞는지 추론한다. 편집이라는 행동을 실행한다. 그리고 결과를 관찰한다. 이때 테스트가 깨졌거나 타입 에러가 났으면 그 관찰이 다시 추론의 입력이 된다. 뭐가 틀렸는지 다시 판단하고, 다시 고치고, 다시 확인한다. 이 지각-추론-행동-관찰-재추론이 내가 개입하지 않아도 몇 바퀴씩 돈다. 도구를 언제 부를지, 관찰 결과를 어떻게 다음 추론에 밀어 넣을지, 이건 전부 하네스가 이미 만들어 놓은 이너 루프다. 이 시리즈 4편에서 Anthropic의 하네스 설계와 Ralph Loop 얘기를 다뤘을 때도 사실 이 이너 루프의 실물을 구경한 거였는데, 그땐 그게 "루프의 안쪽"이라는 이름표를 못 붙였다.

이 사실을 인정하는 게 왜 중요하냐면, 그래야 내 엔지니어링이 어디서 시작되는지 보이기 때문이다. 지각-추론-행동-관찰 회로는 하네스가 준다. 내가 손댈 여지가 별로 없다. 그럼 나한테 남는 건 그 회로를 언제 켜고, 무슨 일감을 물리고, 결과를 어떻게 검증하고, 어느 조건에서 멈출지다. 그게 바깥 루프다.

그러니까 순서가 이렇게 된다. 하네스가 이너 루프를 돌려 준다. 나는 그 이너 루프를 재료 삼아 아웃터 루프를 얹는다. 이 시리즈 5편에서 CLAUDE.md와 훅, 서브에이전트로 실전 적용을 정리했을 때 내가 만졌던 것들이 바로 이 아웃터 루프의 재료였다. 그땐 "하네스 적용법"이라고 불렀지만, 지금 다시 보면 절반은 아웃터 루프 얘기였다.

아웃터 루프 4요소를 내 repo에서 짚었다

여기서부터가 이 글의 진짜 무게중심이다. 아웃터 루프를 추상적으로 "판단하고 검증하는 층"이라고만 두면 손에 안 잡힌다. 그래서 내 나름의 렌즈로 네 요소로 쪼개 봤다. 트리거, 토폴로지, verifier, stop rule.

먼저 짚어 둘 게 있다. 이 네 요소는 내가 내 repo를 해부하려고 만든 분류지, Osmani의 공식 명명이 아니다. Osmani는 아웃터 루프를 Quality, Verdict, Answerability 세 축으로 나눴다. 나는 그 프레임을 그대로 베끼는 대신, 내가 실제로 코드를 짠 자리에 맞는 네 개의 이름을 골랐다. 개념의 출처와 내 해부의 렌즈는 구분해 두는 게 정직하다.

이 네 요소를 blog-studio 실물에 하나씩 대 봤더니 이렇게 맞아떨어졌다.

아웃터 루프 요소 하는 일 blog-studio 실물
트리거 루프를 언제 켜고 무슨 상태를 물리나 .claude/hooks/session-start.sh (SessionStart)
토폴로지 이너 루프들을 어떤 순서로 잇나 blog-orchestrator 스킬
verifier 나온 결과를 무엇으로 검증하나 scripts/deploy_to_blogger.py의 게이트
stop rule 어떤 조건에서 진행을 멈추나 .claude/hooks/pre-commit-check.sh (PreToolUse)

하나씩 풀어 보자.

트리거는 세션이 시작될 때 상태를 먹인다. session-start.sh는 세션이 열리는 순간 자동으로 돈다. 이전 세션에서 어디까지 했는지 적어 둔 active.md, 블로그 정체성 문서, 프로젝트 메모리 인덱스를 읽어서 그 자리에서 컨텍스트 앞에 붙인다. 이게 왜 아웃터 루프의 일부냐면, 이너 루프는 "지금 이 대화" 안에서만 돈다. 세션이 끝나면 그 안쪽 회로의 기억은 날아간다. 세션과 세션 사이를 이어서 "지난번에 여기까지 했으니 이어서 해라"라고 일감을 물려 주는 건 이너 루프 바깥의 일이다. 내가 훅으로 짠 부분이다.

이 감각도 실제로 겪어 보고 잡혔다. 며칠 만에 프로젝트를 다시 열었을 때, 보통이면 "내가 지난번에 뭘 하다 말았지"부터 되짚어야 한다. 그런데 세션을 켜자마자 active.md에 적어 둔 "여기까지 했고 다음은 이것"이 컨텍스트 맨 앞에 딱 붙어서 올라왔다. 나는 기억을 더듬을 필요 없이 바로 다음 작업으로 넘어갔다. 이너 루프한테 "이번 판의 시작점은 여기다"라고 말없이 좌표를 찍어 준 셈이다. 트리거가 없었으면 매 세션이 백지에서 시작했을 거고, 그럼 그건 이어지는 루프가 아니라 매번 리셋되는 단발 실행이었을 거다.

토폴로지는 이너 루프들을 순서대로 잇는다. blog-orchestrator는 글 한 편을 리서치에서 작성, 편집, 게이트, 발행까지 순차로 흘려 보낸다. 각 단계는 그 자체로 하나의 이너 루프가 돈다. 리서치 에이전트가 조사-정리의 사이클을 돌고, 작성이 초안-수정의 사이클을 돌고. 내가 짠 건 그 루프들 사이의 배선이다. 어느 루프의 출력이 어느 루프의 입력이 되는지, 순서가 어떻게 되는지.

순서가 결과를 바꾼다는 건 좀 뻔한 말 같지만 실제로 겪으면 안 뻔하다. 게이트를 발행 뒤에 두면 이미 올라간 글을 내리는 일이 되고, 앞에 두면 애초에 못 올라가게 막는 일이 된다. 같은 검사인데 배선 위치 하나로 "사후 수습"과 "사전 차단"으로 성격이 갈린다. 리서치를 작성 뒤에 붙이면 이미 쓴 글에 근거를 억지로 끼워 맞추게 되고, 앞에 붙이면 근거 위에서 글이 나온다. 이너 루프 자체는 똑같이 도는데, 그것들을 어떤 순서로 이었느냐가 전혀 다른 물건을 만든다. 토폴로지가 아웃터 루프의 요소인 이유가 이거다. 1인 AI 팀을 슬래시 커맨드로 엮은 gstack 사례도 결국 이 토폴로지를 어떻게 그리느냐의 문제였다.

verifier는 코드에 박혀 있어서 건너뛸 수가 없다. 이 요소가 제일 마음에 든다. deploy_to_blogger.py의 360행에서 490행 사이에 발행 게이트가 함수로 들어가 있다. AI 문체 검사 게이트와 품질 게이트 두 개다. 이게 "권고"가 아니라 코드라는 점이 결정적이다.

def check_ai_gate(slug: str) -> tuple[bool, str]:
    rel = f"_workspace/briefs/{slug}-ai-check.md"
    f = WORKSPACE / "briefs" / f"{slug}-ai-check.md"
    if not f.exists():
        return False, f"{rel} 없음 (ai-pattern-check 미실행)"
    ...
    if verdict == "불합격":
        return False, f"AI 게이트 불합격 → 리라이트 필요 ({rel})"
    return True, f"AI 게이트 {verdict}"

검사 파일이 아예 없어도 return False, 판정이 "불합격"이어도 return False. 그리고 발행 함수는 이 게이트를 통과 못 한 slug를 발행 대상에서 그냥 빼 버린다. 전부 걸리면 이렇게 출력하고 멈춘다.

발행 가능한 slug 가 없습니다. --skip-gate 로 우회하거나 게이트를 통과시키세요.

여기서 중요한 건, 게이트를 우회하는 방법이 딱 하나라는 거다. 소스 코드 365행 주석에 그렇게 적혀 있다. "우회는 --skip-gate 로만 가능." 즉 사람이 명령줄에 --skip-gate를 손으로 붙이지 않는 한, 에이전트가 아무리 "이 정도면 됐지, 그냥 발행하자"라고 판단해도 게이트를 넘길 방법이 없다. AI가 검증을 건너뛰자고 판단하는 것 자체가 코드 레벨에서 불가능하다.

이 차이가 왜 큰지는 게이트에 실제로 막혀 보고 나서 알았다. 한번은 글을 다 쓰고 발행하려는데, AI 문체 검사 파일이 아직 안 만들어진 상태였다. 흐름상 검사를 돌린 셈 쳤는데, 파일이 없으니 게이트가 그냥 return False를 뱉었다. 발행 대기 목록에서 그 글이 통째로 빠지고 "발행 가능한 slug 가 없습니다"만 떴다. 순간 좀 짜증이 났다. 다 됐는데 왜. 그런데 곱씹으니 그게 맞았다. 검사를 안 돌렸으면 발행하면 안 되는 거고, 그 판단을 내 기분이 아니라 파일 존재 여부라는 딱딱한 조건에 맡겨 둔 거다. 만약 이게 "가급적 검사하세요" 수준의 권고였다면 나는 그날 그냥 넘겼을 거다.

이게 Osmani가 말한 "증거를 검증하고 승인하거나 막는" 아웃터 루프의 verifier를, 나는 하필 파이썬 함수의 반환값으로 구현해 뒀던 거다. 반년 전엔 그냥 "발행 실수 방지 장치"라고만 생각했는데. 판단의 근거를 사람의 그때그때 기분에서 떼어내 코드로 옮겨 둔 것, 그게 verifier의 핵심이었다는 걸 막히고 나서야 이해했다.

stop rule은 커밋 직전에 손을 붙잡는다. pre-commit-check.sh는 PreToolUse 훅으로, git commit이 실행되기 직전에 낀다. 스테이징된 파일에 OAuth 시크릿이 들었거나, 커밋하면 안 되는 _workspace/ 산출물이 딸려 들어가면 exit 1로 커밋을 막는다.

커밋 차단. 위 항목 정리 후 다시 시도하세요.

이너 루프는 "커밋해 줘"라는 요청을 받으면 그냥 커밋하려 든다. 그게 자기 일이니까. 하지 말아야 할 순간을 아는 건 그 바깥에서 조건을 걸어 둔 쪽이다. verifier가 "결과물이 기준에 맞나"를 본다면, stop rule은 "이 행동을 지금 해도 되나"를 본다. 결이 다르다.

이 훅도 한 번 실제로 나를 붙잡은 적이 있다. 작업하다 무심코 전체를 스테이징하고 커밋을 걸었는데, 거기에 _workspace/ 안의 글 초안이 딸려 들어가 있었다. 이건 커밋 대상이 아니다. 산출물은 레포에 안 들어가는 게 원칙이니까. 평소 같으면 한참 뒤에야 "어 이게 왜 히스토리에 있지" 하고 발견했을 텐데, 훅이 커밋 직전에 exit 1로 막고 뭐가 스테이징됐는지 이유까지 찍어 줬다. 그 자리에서 스테이징을 풀고 다시 했다. 이너 루프는 내가 시킨 커밋을 충실히 실행하려던 것뿐이고, "그 커밋을 지금 하면 안 된다"는 판단은 순전히 바깥에서 걸어 둔 조건이 해 준 거다.

네 요소 말고 하나가 더 있다. 루프와 루프를 잇는 이음새다. stop-reminder.sh는 세션이 끝날 때 도는 Stop 훅인데, active.md가 비어 있으면 "다음 세션을 위해 5~15줄 적어두라"고 안내한다. 이게 왜 필요하냐면, 이번 세션의 마지막이 다음 세션의 트리거로 물려 들어가야 루프가 이어지기 때문이다. 끝에서 상태를 남겨 두면, 시작의 session-start.sh가 그걸 주워서 다음 루프에 먹인다. 트리거와 짝을 이루는 반대쪽 고리다. 이 두 훅이 세션의 경계를 넘어 상태를 릴레이하는 걸 보고 나서야, 아 이게 하나의 루프구나 싶었다.

경계가 흐리다

여기까지 정리하고 나니 처음엔 뿌듯했다. 이름도 모르고 짰던 게 알고 보니 정확히 아웃터 루프였다니. 그런데 뿌듯함은 오래 안 갔다. 되짚을수록 경계가 흐려졌다.

게이트를 예로 들자. 나는 이걸 verifier로 "설계"했다. 함수를 짜고, 판정 규칙을 정하고, 우회 조건을 하나로 좁혔다. 여기까진 분명히 내 아웃터 루프다. 그런데 이 게이트가 발행 흐름 어디에서 언제 호출되는지, 그 실행 타이밍의 상당 부분은 하네스가 준 실행 모델을 탄다. 훅이 걸리는 이벤트도 마찬가지다. pre-commit-check.sh의 코드는 내가 짰지만, "PreToolUse"라는 이벤트가 언제 발화되는지는 런타임이 정한다. 나는 언제 붙잡을지 조건만 걸었지, 붙잡는 순간 자체를 만들지 않았다.

그러니까 내 코드와 하네스가 준 실행 골격이 한 지점에서 겹친다. 훅 파일은 내 것이고 이벤트 타이밍은 하네스 것이다. 게이트 함수는 내 것이고 그걸 호출하는 발행 루프의 골격은 절반쯤 프레임워크가 준 거다. 어디까지가 내가 엔지니어링한 아웃터 루프이고, 어디부터가 하네스가 그냥 깔아 준 바닥인지, 칼로 자르듯 나뉘지 않는다.

한 발 더 들어가면 더 애매해진다. 오케스트레이터의 토폴로지는 분명 내가 그렸다. 그런데 각 단계 안에서 에이전트가 도는 이너 루프는 하네스 것이다. 그럼 "리서치 다음에 작성"이라는 순서는 내 아웃터 루프인데, 그 작성 단계가 실제로 몇 번의 지각-행동 사이클을 돌지는 이너 루프가 알아서 정한다. 내 결정과 하네스의 결정이 한 파이프라인 안에서 번갈아 나온다. 이걸 "내 루프"라고 부르는 게 맞나 싶어진다.

트리거 쪽도 비슷하다. active.md를 다음 세션에 물려 주기로 한 건 내 설계다. 그런데 그 파일이 실제로 컨텍스트 앞에 붙는 순간, 그러니까 SessionStart라는 이벤트가 발화되는 그 타이밍은 내가 만든 게 아니다. 나는 "세션이 시작되면 이걸 읽어라"라고 조건만 걸었다. "세션이 시작된다"는 사건 자체는 런타임이 정의한다. 내 아웃터 루프는 하네스가 열어 준 이벤트 구멍에 손을 넣어서 돌아가는 셈이다. 구멍이 없으면 내 훅도 걸 데가 없다.

이 모호함을 나는 억지로 정리하지 않으려 한다. 사실 이 경계가 흐린 게 문제라기보다는, 아웃터 루프라는 게 원래 하네스 위에 딱 떨어져 얹히는 별도 블록이 아니라 하네스와 맞물려 돌아가는 층이라는 방증에 가깝다. 모델과 하네스의 과적합을 다룬 6편에서 기본 설정을 의심하라고 썼던 게 여기서도 걸린다. 내가 아웃터 루프라고 부르는 것의 상당 부분은 하네스의 기본 실행 모델에 얹혀 있고, 그 기본값이 바뀌면 내 루프의 모양도 따라 흔들린다. 내 것과 준 것의 선이 그래서 계속 움직인다.

그래서

"루프 엔지니어링이란 무엇인가"를 검색하다 여기까지 온 사람에게, 정의를 한 번 더 얹어 줄 생각은 없다. 그런 글은 이미 충분히 많다. 대신 렌즈 하나를 건넨다.

당신의 repo나 워크플로우를 열고, 트리거와 토폴로지와 verifier와 stop rule 네 자리를 짚어 보라. 세션이나 작업을 무엇이 시작시키는가. 여러 단계나 에이전트를 어떤 순서로 이었는가. 결과물을 무엇으로 검증하는가. 어떤 조건에서 멈추게 해 놨는가.

이걸 하나씩 대 보면 아마 나처럼 놀랄 거다. 이미 짜 놓고 이름만 몰랐던 것들이 그 자리에 앉아 있을 가능성이 높다. 반대로 네 자리 중 비어 있는 칸이 보이면, 그게 지금 당신 워크플로우의 약한 고리다. verifier가 없으면 결과를 매번 눈으로 검수해야 하고, stop rule이 없으면 하지 말아야 할 순간을 사람이 매번 기억해야 한다. 나도 게이트에 막혀 보기 전엔 그 칸이 비어 있는 줄도 몰랐다.

그리고 네 자리를 다 채우고 나면, 아마 나와 같은 질문에 도착할 거다. 이 중에 진짜 내가 엔지니어링한 건 어디까지고, 어디부터가 하네스가 그냥 준 건가. 그 경계가 어디쯤 그어지는지는 다음에 이어서 더듬어 보려 한다. 지금은 선이 자꾸 움직인다는 것까지만, 정직하게 적어 둔다.

댓글

이 블로그의 인기 게시물

Google I/O 끝나고, 1편을 다시 펼쳤다

Google I/O 닷새 전, AI 전선이 세 갈래로 갈라졌다

M4 맥북에서 로컬 LLM으로 5일을 살아본 후