Skip to main content

Command Palette

Search for a command to run...

높은 생산성을 위한 Harness 환경 구성

Updated
12 min read

모델은 같은데, 왜 결과가 다를까?

같은 팀, 같은 모델, 같은 IDE. 그런데 A 엔지니어는 10분 만에 복잡한 리팩토링을 끝내고, B 엔지니어는 1시간을 할루시네이션과 씨름한다. 이 차이는 코딩 실력에서 오는 것이 아니다.

LangChain 팀이 이를 증명했다. Terminal Bench 2.0에서 모델을 바꾸지 않고 환경만 개선했더니 52.8%에서 66.5%로 성능이 뛰었다. 모델은 그대로인데, 에이전트가 동작하는 환경을 바꾸자 결과가 달라진 것이다.

2025년이 에이전트의 해였다면, 2026년은 에이전트 하네스의 해다. 이제 "어떤 모델을 쓸 것인가"보다 "에이전트가 동작하는 환경을 어떻게 설계할 것인가"가 생산성을 결정한다.


Harness란 무엇인가

Harness는 말의 고삐에서 온 비유다. AI 모델은 강력하지만 예측 불가능한 말이다. 고삐 없이 놓아두면 아무 방향으로 달린다. 하네스는 그 힘을 원하는 방향으로 이끄는 장치다.

Terraform의 창시자 Mitchell Hashimoto가 2026년 2월 자신의 블로그에서 이 개념을 체계적으로 정리하며 대중화했다.

"에이전트가 실수할 때마다, 그 실수를 다시는 하지 않도록 환경을 엔지니어링한다."

구체적으로 하네스는 에이전트가 동작하는 제약, 도구, 문서, 피드백 루프의 총체다. CLAUDE.md 같은 설정 파일, pre-commit hook, 커스텀 린터, slash command, MCP 서버 — 이 모든 것이 하네스를 구성한다.

이전 글에서 다뤘던 Context Engineering이 "무엇을 줄 것인가"의 문제였다면, Harness Engineering은 "어떤 환경에서 동작하게 할 것인가"의 문제다. 컨텍스트는 하네스의 한 구성 요소이고, 하네스는 컨텍스트를 포함한 전체 시스템이다.


Anthropic의 접근: 장기 실행 에이전트를 위한 하네스

Anthropic은 "Effective Harnesses for Long-Running Agents"에서 핵심 문제를 짚었다. 에이전트가 여러 컨텍스트 윈도우에 걸쳐 일관된 진행을 유지하지 못한다. 새 세션이 시작되면 이전 작업의 기억이 없고, 토큰을 낭비하며 재탐색하거나 이전 진행을 되돌린다.

2단계 아키텍처

이 문제를 해결하기 위해 Anthropic은 에이전트를 둘로 나눴다.

Initializer Agent — 첫 세션에서만 실행되며 작업 환경을 구축한다.

  • init.sh: 개발 서버를 띄우는 스크립트

  • claude-progress.txt: 작업 이력을 기록하는 파일

  • Feature list (JSON): 구현할 기능 200개 이상을 사전 분해

  • 초기 git commit: 추가된 파일들의 스냅샷

Coding Agent — 이후 세션마다 실행되며, 세션당 하나의 기능만 구현한다.

매 세션의 시작 패턴은 동일하다:

pwd → claude-progress.txt 읽기 → feature_list.json 확인 → git log 확인
→ init.sh로 dev server 시작 → 기존 테스트 통과 확인 → 다음 기능 구현

왜 JSON인가

Feature list를 Markdown이 아닌 JSON으로 관리하는 것은 의도적인 설계다. 에이전트는 Markdown의 자유로운 형식에서 테스트 항목을 삭제하거나 수정하는 경향이 있다. JSON은 구조가 엄격해서 에이전트가 passes 필드 외에는 건드리기 어렵다.

{
  "category": "functional",
  "description": "새 채팅 버튼이 새 대화를 생성한다",
  "steps": ["메인 인터페이스 이동", "버튼 클릭", "생성 확인"],
  "passes": false
}

에이전트는 이 passes 값만 바꿀 수 있다. 테스트 자체를 삭제하거나 변경하는 것은 프롬프트에서 명시적으로 금지한다.

Git을 상태 복구 도구로

Anthropic이 발견한 가장 효과적인 패턴은 git을 에이전트의 상태 복구 도구로 활용하는 것이다. 에이전트에게 매 작업마다 설명적인 커밋 메시지로 커밋하게 하면, 나쁜 변경이 생겼을 때 git revert로 작동하던 상태로 되돌릴 수 있다.

progress 파일과 git history의 조합으로, 새 세션의 에이전트는 이전에 무슨 일이 있었는지 추측하느라 시간을 낭비하지 않는다. 명확한 상태와 이력이 있으니 바로 다음 작업에 들어간다.


토스의 접근: 팀의 생산성 저점을 올리는 하네스

토스 기술 블로그의 "Software 3.0 시대, Harness를 통한 조직 생산성 저점 높이기"는 다른 각도에서 같은 문제를 본다. Anthropic이 기술적 아키텍처에 집중했다면, 토스는 조직의 역량 편차에 집중한다.

문제: 각자도생

현재 많은 팀이 LLM을 도입했지만 실상은 "각자도생"이다. A 엔지니어는 작업 전에 레포의 코딩 가이드라인, lint 규칙, 기존 패턴을 에이전트에 주입한다. B 엔지니어는 단순 질문으로 시작해 수정 루프에 갇힌다. 도구는 같은데 결과가 다르다.

이 차이를 개인 역량에 맡겨두면, 팀 전체의 생산성은 가장 느린 사람에 의해 결정된다.

해법: 플러그인을 하네스로

토스는 Claude Code의 플러그인 생태계를 하네스로 활용한다. 핵심은 세 가지 특성이다.

Frictionless Integration — 브라우저로 나가서 챗봇에 코드를 붙여넣는 문맥 교환 비용을 없앤다. 터미널 안에서 자연어와 코드가 끊김 없이 섞인다.

Executable SSOT (실행 가능한 단일 진실 공급원) — Wiki나 Notion 문서는 작성되는 순간부터 낡는다. 하지만 플러그인 형태의 지식은 사람이 읽으면 업무 가이드라인이 되고, LLM이 읽으면 시스템 프롬프트가 된다. 플러그인 코드를 업데이트하면 팀원 모두의 에이전트 행동이 즉시 바뀐다.

저점 상향 평준화 — oh-my-zsh처럼 누군가 미리 고민해둔 베스트 프랙티스를 즉시 가져다 쓸 수 있다. 하지만 여기서 한 발 더, 팀의 도메인 맥락을 반영한 특화된 플러그인이 핵심이다.

3-Layer 아키텍처

토스는 지식을 세 계층으로 분리한다.

Layer 범위 예시
Global 전사 공통 보안 정책, 기본 코딩 스타일
Domain 팀/비즈니스별 결제 도메인 로직, 정산 규칙
Local 레포지토리 특화 프로젝트별 구현 디테일

신입에게 전사 문서를 통째로 던지지 않듯, LLM에게도 현재 작업에 필요한 지식만 주입한다. 이 계층화된 플러그인들이 모이면 별도의 RAG 시스템 없이도 살아있는 지식 베이스가 된다.

노하우의 민주화

가장 인상적인 부분은 팀 최고 엔지니어의 워크플로우를 slash command로 배포하는 패턴이다.

/new-feature 입력
→ Claude가 구현 기능의 맥락 수집
→ Jira 이슈 발급, 브랜치 생성, 구현 계획 작성
→ 엔지니어 검토/승인
→ 구현 시작

B 엔지니어도 /new-feature 하나로 A 엔지니어와 동일한 품질의 워크플로우를 실행한다. LLM 활용 능력이 더 이상 개인의 센스 영역이 아니라, 팀이 설계하고 배포하는 시스템의 영역으로 넘어간다.


Harness Engineering의 4대 요소

Anthropic, 토스, OpenAI, Stripe 등의 사례를 종합하면 효과적인 하네스는 네 가지 요소로 구성된다.

1. Architecture as Guardrails: 구조가 곧 제약

여기서 짚고 넘어가야 할 것이 있다. CLAUDE.md에 "이 프로젝트는 헥사고날 아키텍처를 따릅니다"라고 적으면 에이전트가 지켜줄까? 아니다. CLAUDE.md는 본질적으로 비강제(non-enforcing)다. 에이전트는 할루시네이션에 의해 규칙을 잊거나 무시할 수 있다. "하지 마세요"라는 지시는 확률적으로 무시될 수 있지만, 빌드가 깨지는 것은 무시할 수 없다.

이것이 ArchUnit, Konsist 같은 아키텍처 테스트 도구가 하네스에서 핵심적인 이유다. 컨벤션을 문서가 아니라 코드로 강제한다.

Java 진영의 ArchUnit은 아키텍처 규칙을 단위 테스트로 작성한다:

@Test
void 도메인_레이어는_인프라에_의존하지_않는다() {
    noClasses()
        .that().resideInAPackage("..domain..")
        .should().dependOnClassesThat()
        .resideInAPackage("..infrastructure..")
        .check(importedClasses);
}

Kotlin 프로젝트라면 Konsist가 같은 역할을 한다. 네이밍 컨벤션, 패키지 구조, 클래스 가시성, 의존성 방향까지 — 코딩 규칙을 테스트 코드로 표현한다:

@Test
fun `UseCase 클래스는 반드시 execute 메서드를 가진다`() {
    Konsist.scopeFromProject()
        .classes()
        .withNameEndingWith("UseCase")
        .assertTrue { it.hasFunction { func -> func.name == "execute" } }
}

에이전트가 이 규칙을 어기면 테스트가 실패하고 빌드가 깨진다. 에이전트는 빌드 실패를 감지하면 스스로 수정한다. CLAUDE.md의 "~하지 마세요"는 무시할 수 있지만, 빨간 테스트는 무시할 수 없다. 이것이 "문서로 안내"와 "코드로 강제"의 결정적 차이다.

무신사도 이와 같은 접근을 적용하고 있다. AI 에이전트를 도입하면서 기존의 아키텍처 테스트와 린터를 하네스의 일부로 활용한다. 에이전트가 생성한 코드가 팀의 컨벤션을 위반하면 CI에서 잡히고, 에이전트가 자동으로 수정한다. 사람이 리뷰할 때쯤이면 이미 컨벤션을 준수한 코드가 나온다.

OpenAI도 같은 원리를 적용했다. Codex 에이전트로 수동 코드 0줄, 100만 줄 코드베이스를 5개월간 3명의 엔지니어로 만들었다. 이것이 가능했던 핵심은 엄격한 아키텍처 제약이다.

의존성 방향을 기계적으로 강제했다:

Types → Config → Repo → Service → Runtime → UI

에이전트는 이 레이어 안에서만 동작하고, 커스텀 린터가 구조적 위반을 자동으로 잡는다. 직관에 반하지만, AI가 생성하는 코드는 자유도를 줄여야 품질이 올라간다. Vercel도 처음엔 풍부한 도구 라이브러리를 제공했다가, 오히려 도구를 줄이고 선택지를 단순화하자 에이전트가 더 빠르고 안정적으로 동작했다.

정리하면, 컨벤션 강제의 스펙트럼은 이렇다:

수단 강제력 예시
CLAUDE.md 약함 (비강제, 할루시네이션으로 무시 가능) "헥사고날 아키텍처를 따릅니다"
Hook 중간 (특정 액션 시점에 검증) commit 시 브랜치명 검사, lint 실행
ArchUnit / Konsist 강함 (빌드 실패로 강제) 레이어 의존성 위반 시 테스트 실패
커스텀 린터 강함 (에러 메시지가 수정 방법까지 안내) 구조적 위반 감지 + 에이전트 자동 수정

효과적인 하네스는 이 수단들을 조합한다. 안내는 CLAUDE.md로, 검증은 hook으로, 강제는 아키텍처 테스트로.

2. Tools as Foundation: 도구가 곧 기반

에이전트에게는 인간 엔지니어와 동일한 도구 접근권이 필요하다.

Stripe의 Minions는 약 500개의 내부 도구를 MCP 서버로 노출해서 사전 준비된 샌드박스 환경에서 에이전트가 동작하게 한다. 개발자가 Slack에 태스크를 올리면, 에이전트가 코드를 작성하고, CI를 통과시키고, 리뷰 가능한 PR을 열어준다. 코딩 과정에서 인간의 개입은 없다. 최종 PR은 사람이 리뷰한 뒤 머지되며, 주간 1,000개 이상의 PR이 이 방식으로 처리된다.

핵심 인사이트: 커스텀 린터의 에러 메시지가 이중 목적을 수행한다. 위반을 표시하면서 동시에 에이전트에게 수정 방법을 알려준다. 도구가 교육 기제가 되는 것이다.

3. Documentation as Living System: 문서가 곧 인프라

CLAUDE.md(또는 AGENTS.md)는 단순한 문서가 아니라 살아있는 인프라다.

Hashimoto의 Ghostty 프로젝트에서 AGENTS.md는 빌드 명령어, 구조적 안내와 함께 과거 에이전트가 저질렀던 실패를 방지하는 규칙들을 담고 있다. 에이전트가 실수할 때마다 그 실수를 방지하는 규칙이 문서에 추가된다. Hashimoto 본인도 이 규칙들을 추가한 뒤 문제가 "거의 완전히 해결되었다"고 밝혔다.

OpenAI는 여기서 한 발 더 나아가, 백그라운드 에이전트가 주기적으로 문서를 스캔해서 오래된 내용을 감지하고 정리 PR을 자동으로 여는 구조를 만들었다.

효과적인 문서의 원칙:

  • 에이전트가 실패할 때마다 업데이트

  • 코드에서 유추할 수 없는 정보만 포함

  • 모노레포에서는 중첩된 CLAUDE.md로 팀/패키지별 컨텍스트 분리

4. Verification & Feedback Loops: 검증과 피드백 순환

에이전트의 가장 흔한 실패 모드는 기능을 제대로 테스트하지 않고 완료로 표시하는 것이다.

Anthropic은 이를 해결하기 위해:

  • Feature list를 JSON으로 구조화하여 에이전트가 테스트 항목을 삭제하지 못하게 함

  • 브라우저 자동화(Puppeteer MCP)로 사용자 관점의 E2E 테스트 강제

  • 스크린샷을 통한 시각적 버그 감지 (Vision 활용)

피드백 루프에서 중요한 것은 성공은 조용히, 실패만 상세하게다. 매번 전체 테스트 결과를 출력하면 컨텍스트 윈도우가 넘친다. 통과한 테스트는 무시하고 실패한 테스트만 상세 로그를 보여줘야 한다.


실전: 나만의 하네스 구성하기

이론은 충분하다. 실제로 하네스를 구성하는 방법을 살펴보자.

CLAUDE.md: 하네스의 시작점

CLAUDE.md는 하네스의 가장 기본적인 구성 요소다. 에이전트가 세션을 시작할 때 자동으로 읽는 프로젝트 설명서이자 행동 지침이다.

하지만 CLAUDE.md에 대한 가장 흔한 실수는 코드에서 이미 알 수 있는 것을 적는 것이다. "이 프로젝트는 Spring Boot를 사용합니다", "패키지 구조는 domain/application/infrastructure입니다" — 에이전트는 코드를 읽으면 이것을 안다. 이런 내용은 토큰만 낭비할 뿐 아니라, 코드와 문서가 어긋나면 오히려 혼란을 준다.

반대로, 코드에서 절대 알 수 없는 것은 반드시 적어야 한다:

# 빌드 & 테스트
./gradlew build              # 전체 빌드 (ArchUnit 테스트 포함)
./gradlew test --tests "*UseCase*"  # UseCase 단위 테스트만
docker compose up -d          # 로컬 DB/Redis 실행

# 배포
dev 환경: main 브랜치 push 시 자동 배포
prod 환경: 릴리스 태그 생성 시 배포 (수동 승인 필요)

# 작업 규칙
- PR은 반드시 Jira 티켓 번호를 포함: feat/PROJ-123-description
- hotfix 외에는 main 직접 커밋 금지

CLI 명령어, 배포 절차, 브랜치 전략, 외부 시스템 연동 방법 — 이런 것들은 코드를 아무리 읽어도 알 수 없다. 이것이 CLAUDE.md에 담아야 할 내용이다.

코드 패턴이나 아키텍처 규칙은? CLAUDE.md에 쓰지 말고 ArchUnit이나 Konsist 테스트로 강제하라. 에이전트가 규칙을 어기면 빌드가 깨지고, 에이전트가 스스로 고친다. 문서에 적는 것보다 확실하고, 문서가 낡을 걱정도 없다.

핵심 원칙은 두 가지다:

  • "코드에서 알 수 있는가?" → 알 수 있으면 적지 않는다

  • "이것이 없으면 에이전트가 실패하는가?" → 실패하지 않으면 적지 않는다

Hooks: CLAUDE.md가 안내라면, Hook은 강제다

CLAUDE.md에 "main 브랜치에 직접 커밋하지 마세요"라고 적어도, 에이전트는 이를 무시하고 main에 커밋할 수 있다. 하지만 hook은 다르다. 물리적으로 차단한다.

토스의 사례:

Claude가 git commit 시도
→ Hook이 현재 브랜치 검사
→ "현재 main 브랜치입니다. feature/ 브랜치 생성 후 작업하겠습니다"
→ 자동 교정

Claude Code의 hook 시스템이나 pre-commit hook으로 이런 검증을 걸 수 있다:

  • 커밋 시점: 브랜치명 규칙 검사, lint 실행, ArchUnit/Konsist 테스트 실행

  • 파일 수정 시점: 금지된 파일(.env, 설정 파일) 수정 차단

  • 명령 실행 시점: 위험한 명령어(rm -rf, DROP TABLE) 차단

핵심은 안내와 강제의 역할 분리다. CLAUDE.md는 에이전트에게 방향을 알려주고, hook과 아키텍처 테스트는 그 방향을 벗어나지 못하게 강제한다. 안내만으로는 부족하고, 강제만으로는 맥락이 없다. 둘 다 필요하다.

Skills & Slash Commands: 워크플로우 패키징

반복되는 워크플로우를 스킬로 패키징하면 팀 전체의 역량 바닥이 올라간다.

좋은 스킬의 조건:

  • 단일 책임: 하나의 스킬은 하나의 워크플로우만 담당

  • 승인 게이트: 에이전트가 잘못된 방향으로 달리기 전에 사람이 검토

  • 검증 단계 내장: 결과물을 자체적으로 확인하는 단계 포함

MCP 서버: 도구 확장

Model Context Protocol로 에이전트에게 외부 도구 접근권을 제공한다. Jira 연동, Slack 알림, 데이터베이스 조회, 모니터링 대시보드 확인 등 — 인간 엔지니어가 쓰는 도구를 에이전트도 쓸 수 있게 한다.

settings.local.json: 권한 설계

에이전트의 권한을 명시적으로 설계한다. 모든 것을 허용하는 것도, 모든 것을 차단하는 것도 답이 아니다.

{
  "permissions": {
    "allow": [
      "Read", "Edit", "Write",
      "Bash(git diff *)", "Bash(git commit *)",
      "WebSearch"
    ],
    "deny": [
      "Bash(git push *)"
    ]
  }
}

안전한 작업은 자동 허용하고, 돌이킬 수 없는 작업(push, 프로덕션 배포 등)은 차단하거나 승인 게이트를 둔다.


실제 성과로 증명된 패턴

하네스 엔지니어링은 이론이 아니다. 실제 성과가 이를 뒷받침한다.

사례 방식 성과
OpenAI 내부 3명, 수동 코드 0줄, 아키텍처 제약 + 커스텀 린터 5개월간 100만 줄, 일평균 3.5 PR/인
Stripe Minions ~500 도구 MCP 노출, 샌드박스 환경 주간 1,000+ PR 처리, 코딩 과정 자동화
Peter Steinberger 4-10 에이전트 동시 운용, 아키텍처 감독 집중 1인 월 6,600+ 커밋
LangChain 모델 변경 없이 하네스만 개선 52.8% → 66.5% (Terminal Bench 2.0)

공통점이 보인다. 모델을 바꾸지 않았다. 환경을 바꿨다. 제약을 설계했다. 도구를 정비했다. 문서를 살아있게 만들었다. 그러자 같은 모델에서 다른 결과가 나왔다.


모델은 commodity, 하네스가 성패를 가른다

이 글의 핵심을 한 문장으로 요약하면 이것이다.

에이전트의 결과 품질은 모델이 아니라 환경이 결정한다.

Claude든 GPT든 Gemini든, 모델의 성능 차이보다 그 모델이 동작하는 환경의 설계가 결과에 더 큰 영향을 미친다. 빈 컨텍스트에 강력한 모델을 놓으면 할루시네이션과 삽질이 나오고, 잘 설계된 하네스에 적당한 모델을 놓으면 일관된 고품질 산출물이 나온다.

이전 글에서 "스킬은 바닥을 올리고, 사람은 천장을 결정한다"고 했다. 하네스는 그 스킬을 포함한 더 큰 그림이다. CLAUDE.md, hooks, skills, MCP, 권한 설계, 아키텍처 제약, 피드백 루프 — 이 모든 것이 하나의 시스템으로 엮일 때 에이전트는 비로소 안정적으로 동작한다.

오늘 당장 할 수 있는 것부터 시작해보자. 에이전트가 실수할 때마다 CLAUDE.md에 한 줄을 추가하는 것. 이것이 하네스 엔지니어링의 첫걸음이다.


참고 자료

More from this blog

카프카 입문 시리즈 2편: 토픽, 파티션, 오프셋

이 글은 Apache Kafka 입문 시리즈의 두 번째 글입니다. 1편에서 살펴본 구성 요소들 위에서, 메시지가 실제로 어떤 구조로 저장되고 관리되는지 알아보겠습니다. 1편을 마치며 세 가지 질문을 남겼습니다. 메시지는 브로커 안에서 어떤 구조로 저장될까? 토픽과 파티션은 정확히 무엇이고, 왜 필요할까? 컨슈머의 오프셋은 어떻게 동작할까? 이번 편에서 이 질문들에 하나씩 답하겠습니다. Topic: 메시지의 논리적 분류 토픽(Topic)은...

Mar 19, 202612 min read7

Java GC의 진화 — Serial에서 Generational ZGC까지

Java가 약속한 것 중 하나는 "메모리는 내가 관리할게"였다. C/C++ 개발자들이 malloc과 free로 메모리와 씨름하던 시절, Java는 Garbage Collector(GC)라는 자동 메모리 관리자를 들고 나왔다. 개발자는 객체를 만들기만 하면 되고, 치우는 건 GC가 알아서 한다. 하지만 "알아서"라는 말에는 대가가 있었다. GC가 동작하는 동안 애플리케이션이 멈추는 것이다. 이 멈춤을 Stop-The-World(STW) 일시 정지...

Mar 16, 20269 min read1

Spring의 3대 철학 — DI, AOP, PSA가 만드는 코드의 품격

Spring을 처음 배울 때, 나는 어노테이션 수집가였다. @Autowired를 붙이면 객체가 알아서 들어오고, @Transactional을 붙이면 트랜잭션이 알아서 관리되고, @Cacheable을 붙이면 캐시가 알아서 동작했다. "알아서"라는 말 뒤에 숨은 원리를 몰랐다. 그냥 마법이라고 생각했다. 그러다 문제가 생겼다. @Transactional을 붙였는데 롤백이 안 됐다. 같은 클래스 안에서 메서드를 호출했기 때문이었다. 원인을 찾는 데 ...

Mar 16, 202611 min read9

Spring Boot Docker 이미지, 한 줄 한 줄에 담긴 고민

처음 Spring Boot 애플리케이션을 Docker로 배포했을 때, Dockerfile은 딱 세 줄이었다. FROM openjdk:17 COPY build/libs/app.jar app.jar ENTRYPOINT ["java", "-jar", "app.jar"] 동작은 했다. 하지만 이미지 크기는 700MB를 넘겼고, 코드 한 줄 고칠 때마다 전체 JAR를 다시 빌드해야 했다. 프로덕션에 올릴 때는 root 권한으로 실행되고 있었다. "동작...

Mar 16, 202610 min read4

끄적끄적 테크 블로그

32 posts

물류 회사에 다니고 있는 개발자 블로그입니다. 개발을 너무 좋아해서 정신없이 작업하다가 중간에 끄적거리며 내용들을 몇개 적어봅니다 ㅎㅎ