Java

Spring 프로젝트에 하네스 엔지니어링 적용하기

SeolJY 2026. 3. 16. 23:24

TL;DR

  • 하네스 엔지니어링은 AI 에이전트에게 "잘 해달라고 부탁하는 것"이 아니라 "잘못할 수 없는 환경을 만드는 것"이다.
  • 하네스는 Constrain(제한), Inform(안내), Verify(검증), Correct(교정)의 네 축으로 구성되며, 이것들이 에이전트가 잘못할 수 없는 환경을 만든다.
  • 프로젝트에 적용해보니, 결국 하네스 엔지니어링은 사람을 위해 만들어둔 컨벤션, 문서, 자동화 도구들을 AI 에이전트도 읽고 활용할 수 있게 연결하는 작업이었다.

들어가며

AI 코딩 에이전트(Claude Code, Cursor, Codex 등)를 프로젝트에 본격적으로 활용하기 시작하면, 한 가지 불편한 사실을 마주하게 됩니다. 에이전트가 생성한 코드가 팀의 컨벤션을 따르지 않는다는 것입니다. 필드 주입을 쓴다거나, 레이어 의존 방향을 어긴다거나, 커스텀 예외 대신 RuntimeException을 던진다거나. 프롬프트에서 "우리 프로젝트 규칙을 따라라"고 아무리 말해도, 긴 대화가 이어지면 에이전트는 슬그머니 자기 방식으로 돌아갑니다.

 

2026년 2월, 이 문제를 체계적으로 다루는 개념에 이름이 붙었습니다.

하네스 엔지니어링(Harness Engineering) 입니다.

 

이 글에서는 하네스 엔지니어링이 무엇인지 살펴보고, 실제 Spring Boot 프로젝트에 어떻게 적용했는지를 공유합니다.

 

하네스 엔지니어링이란?

용어의 등장

2026년 2월 5일, HashiCorp 공동 창업자 Mitchell Hashimoto가 자신의 블로그에서 AI 에이전트가 실수할 때마다 같은 실수를 방지하는 장치를 쌓아가는 작업을 "하네스 엔지니어링"이라 불렀습니다. 며칠 뒤인 2월 11일, OpenAI가 "Harness engineering: leveraging Codex in an agent-first world"라는 글을 발표하면서 용어가 빠르게 확산되었습니다.

 

Martin Fowler는 이를 다음과 같이 정리했습니다.

I like "harness" as a word to describe the tooling and practices we can use to keep AI agents in check.

— Martin Fowler, "Harness Engineering" (2026.02.17)

"하네스"라는 단어가 AI 에이전트를 통제하기 위한 도구와 관행을 설명하는 데 적합하다고 본 것입니다.

 

정의

하네스(Harness)란 무엇일까요? LangChain 블로그에서 가장 깔끔하게 정의하고 있습니다.

If you're not the model, you're the harness. A harness is every piece of code, configuration, and execution logic that isn't the model itself. A raw model is not an agent. But it becomes one when a harness gives it things like state, tool execution, feedback loops, and enforceable constraints.

— LangChain, "The Anatomy of an Agent Harness" (2026.03.11)

모델이 아닌 모든 것이 하네스입니다. 원시 모델은 에이전트가 아니지만, 하네스가 상태 관리, 도구 실행, 피드백 루프, 강제 가능한 제약 조건을 부여하면 에이전트가 됩니다.

 

하네스의 4가지 원칙

NxCode의 가이드에서 하네스 엔지니어링의 역할을 네 가지로 정리하고 있습니다.

Harness engineering is the design and implementation of systems that: Constrain what an AI agent can do (architectural boundaries, dependency rules). Inform the agent about what it should do (context engineering, documentation). Verify that the agent did it correctly (testing, linting, CI validation). Correct the agent when it goes wrong (feedback loops, self-repair mechanisms).

— NxCode, "Harness Engineering: The Complete Guide" (2026.03)

  • Constrain — 에이전트가 할 수 있는 것을 제한한다
  • Inform — 에이전트가 해야 할 것을 알려준다
  • Verify — 에이전트가 올바르게 수행했는지 검증한다
  • Correct — 잘못되었을 때 교정한다

핵심은, "에이전트에게 잘 해달라고 부탁하는 것"이 아니라 "잘못할 수 없는 환경을 만드는 것"입니다. 이것이 프롬프트 엔지니어링과 하네스 엔지니어링의 근본적 차이입니다.

 

프롬프트 → 컨텍스트 → 하네스: 진화의 흐름

시기 패러다임 범위

2023~2024 프롬프트 엔지니어링 질문 한 번, 답변 한 번. 지시문 최적화
2025 중반 컨텍스트 엔지니어링 RAG, MCP, 메모리 등 시스템 수준 맥락 설계
2026.02~ 하네스 엔지니어링 에이전트 전체 환경 설계 (컨텍스트 + 도구 + 검증 + 교정)

하네스 엔지니어링은 컨텍스트 엔지니어링을 포함하면서, 그 바깥의 모든 것(린터, CI, 테스트, 옵저버빌리티, 피드백 루프)까지 아우르는 개념입니다.

 

Spring 프로젝트에 적용하기

 

GitHub - toduck-App/toduck-backend at develop

Service for adult ADHD. Contribute to toduck-App/toduck-backend development by creating an account on GitHub.

github.com

 

여기서부터는 실제 Spring Boot 프로젝트에 하네스를 구축한 과정을 공유합니다. 프로젝트 스택은 Java 17 + Spring Boot 3.3.1 + Gradle 단일 모듈이며, AI 코딩 에이전트로 Claude Code를 주로 사용하되, 가능한 한 도구에 종속되지 않는 구조를 지향했습니다.

 

1. AGENTS.md — 도구 무관 Source of Truth

가장 먼저 한 일은 AGENTS.md를 프로젝트 루트에 배치한 것입니다.

현재 AI 코딩 도구마다 읽는 설정 파일이 다릅니다. Claude Code는 CLAUDE.md를, Codex CLI는 AGENTS.md를, Cursor는 .cursorrules를 찾습니다. 하지만 내용은 거의 같습니다. 이에 대해 DeployHQ 블로그에서 명확한 권장 사항을 제시하고 있습니다.

Maintain one source of truth (AGENTS.md) and have tool-specific files reference it. Don't copy-paste the same rules into CLAUDE.md, .cursorrules, and copilot-instructions.md.

— DeployHQ, "CLAUDE.md, AGENTS.md, and Every AI Config File Explained" (2026.03.11)

 

하나의 원본(AGENTS.md)을 유지하고, 도구별 파일은 이를 참조하게 하라는 것입니다.

여기서 중요한 것은 분량입니다. HumanLayer 블로그의 분석에 따르면, 프론티어 씽킹 모델도 약 150~200개의 명령만 합리적으로 따를 수 있고, 명령 수가 늘수록 준수 품질은 균일하게 저하됩니다. Claude Code의 시스템 프롬프트 자체가 이미 약 50개의 명령을 포함하고 있으므로, AGENTS.md는 가능한 한 간결해야 합니다.

같은 글에서 또 하나 중요한 지적이 있습니다.

Never send an LLM to do a linter's job. LLMs are comparably expensive and incredibly slow compared to traditional linters and formatters.

— HumanLayer, "Writing a good CLAUDE.md" (2025.11)

 

코드 스타일 가이드라인을 AGENTS.md에 넣지 말라는 것입니다. 린터가 할 일을 LLM에게 시키지 말라. 이건 Checkstyle, EditorConfig 같은 기존 도구가 이미 하고 있는 일입니다.

 

이 원칙에 따라, AGENTS.md는 약 50줄 이내로 작성했습니다:

# toduck-backend

ADHD 자기관리 앱 toduck의 백엔드 서버.
Java 17 · Spring Boot 3.3.1 · Gradle · 단일 모듈

## Commands
- Build: `./gradlew build`
- Test: `./gradlew test`
- Code style check: `./gradlew checkstyleMain` (Naver Checkstyle)

## Architecture
도메인별 Clean Architecture: `domain/{name}/` → common, domain(service/usecase),
persistence(entity/repository), presentation(controller/dto)

의존 방향: Controller → UseCase → Service → Repository (단방향)

## Code Style
Checkstyle(Naver rules)과 EditorConfig로 자동 강제됨.
에이전트가 코드 스타일을 수동으로 관리할 필요 없음.

## Core Rules
- 모든 메서드 파라미터에 `final` 키워드 필수
  (Builder 생성자 파라미터는 Lombok이 생성하므로 예외)
- 엔티티는 `BaseEntity` 상속, `@Builder` + private 생성자
- Service는 자기 도메인 Repository만 의존. 교차 도메인은 UseCase에서 조합
- UseCase 클래스는 `@UseCase` 어노테이션 사용 (`@Service` 아님)
- API 응답: `ApiResponse.createSuccess(data)` 또는 `ApiResponse.createSuccessWithNoContent()`
  - `ApiResponse.onSuccess()`는 존재하지 않음 — 절대 사용 금지
- 예외: `CommonException.from(ExceptionCode.XXX)`, VO 검증은 `VoException`
- DTO는 Java record + Bean Validation
- 조회: `@Transactional(readOnly = true)`, 수정: `@Transactional`
- 이벤트 리스너: `@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)`
  (`@EventListener` 아님)

## Testing
- 베이스 클래스: `UseCaseTest`, `ServiceTest`, `RepositoryTest`
- 한글 `@DisplayName`, Given-When-Then, AssertJ
- 상세: `.agent-docs/test-conventions.md`

## Detailed Conventions
새 도메인 개발 시 반드시 참조: `.agent-docs/conventions.md`

## Git
- 커밋 메시지: commitlint 규칙 준수 (commitlint.config.js)
  - 타입: feat, fix, docs, style, refactor, test, chore
  - 스코프 사용 안 함, 헤더 100자 제한

 

CLAUDE.md는 AGENTS.md를 가리키는 포인터로 축소했습니다. 5줄입니다:

# toduck-backend

프로젝트 루트의 AGENTS.md를 따를 것.
상세 개발 컨벤션: .agent-docs/conventions.md
테스트 컨벤션: .agent-docs/test-conventions.md

 

기존에는 1,173줄짜리 CLAUDE.md가 매 대화 시작마다 컨텍스트에 로드되었는데, 이제 5줄만 로드됩니다. 99.6%의 컨텍스트 절감입니다.

 

2. Progressive Disclosure — 상세 문서 분리

그러면 기존 CLAUDE.md에 있던 상세 컨벤션은 어디로 갔을까요? .agent-docs/ 디렉토리로 분리했습니다.

에이전트에게 모든 정보를 미리 주는 대신, 핵심만 진입점에 두고 상세 내용은 필요할 때 찾아보게 하는 패턴입니다. HumanLayer 블로그에서는 이를 "Progressive Disclosure"라고 부릅니다.

프로젝트 루트/
├── AGENTS.md                    # ~50줄, 핵심 규칙만
├── .claude/
│   └── CLAUDE.md                # 5줄, AGENTS.md 참조 포인터
└── .agent-docs/
    ├── conventions.md           # 개발 컨벤션 상세 (~1,100줄)
    └── test-conventions.md      # 테스트 컨벤션 상세 (~360줄)

 

conventions.md는 패키지 구조, 엔티티 작성 규칙, 레포지토리 패턴, 서비스/유스케이스 아키텍처, DTO 규칙, 예외 처리 등을 코드 예시와 함께 상세하게 기술합니다. 일부를 발췌하면:

## 프로젝트 구조 및 패키지 구성

새로운 도메인을 개발할 때는 다음과 같은 패키지 구조를 엄격히 따라야 합니다:

domain/{도메인명}/
├── common/              # 도메인 내 공통 유틸리티
│   ├── converter/       # JPA 컨버터
│   ├── dto/             # 도메인 내부 DTO
│   ├── helper/          # 도메인별 헬퍼 클래스
│   └── mapper/          # 엔티티-DTO 매핑 클래스
├── domain/              # 핵심 비즈니스 로직
│   ├── event/           # 도메인 이벤트
│   ├── service/         # 도메인 서비스 (데이터 접근 로직)
│   └── usecase/         # 유스케이스 (비즈니스 로직 조합)
├── persistence/         # 데이터 계층
│   ├── entity/          # JPA 엔티티
│   ├── repository/      # 레포지토리 인터페이스 및 구현
│   │   └── querydsl/    # QueryDSL 구현체
│   └── vo/              # 값 객체
└── presentation/        # 프레젠테이션 계층
    ├── api/             # API 문서화 인터페이스
    ├── controller/      # REST 컨트롤러
    └── dto/             # 요청/응답 DTO

 

test-conventions.md는 기존에 문서화되지 않았던 테스트 패턴을 실제 테스트 코드에서 추출하여 새로 작성했습니다:

## 베이스 클래스 선택 기준

| 베이스 클래스 | 용도 | 특징 |
|---|---|---|
| `UseCaseTest` | UseCase 계층 테스트 | `@SpringBootTest`, 외부 서비스 `@MockBean`,
|               |                      | `TestFixtureBuilder` 주입 |
| `ServiceTest` | Service 계층 및 동시성 테스트 | `@SpringBootTest`, `EntityManager` 포함 |
| `RepositoryTest` | Repository/QueryDSL 테스트 | `@DataJpaTest`, 경량 JPA 테스트 |

 

이 문서들을 작성하면서 중요한 발견이 있었습니다.

실제 코드와 대조했을 때, 기존 문서에 불일치가 꽤 있었다는 것입니다. 문서에는 `@EventListener`라고 적혀 있지만 코드에서는 `@TransactionalEventListener`을 쓰고 있었고, 이벤트 클래스에 `occurredAt` 필드가 있다고 적혀 있지만 실제 코드에는 없었습니다.

사람은 이런 불일치를 암묵적으로 처리합니다. "아, 문서가 좀 오래됐구나" 하고 코드를 따르죠. 하지만 에이전트는 문서를 그대로 따릅니다. 문서에 `@EventListener`라고 적혀 있으면 `@EventListener`를 씁니다. 당연한 말이지만, 문서와 코드의 불일치는 사람에게는 사소한 불편일지라도 에이전트에게는 컨벤션 위반의 직접적 원인이 됩니다.

 

3. PostToolUse Hook — 즉각 피드백

AGENTS.md가 에이전트에게 "규칙을 알려주는 것"(Inform)이라면, Hook은 "규칙을 강제하는 것"(Verify)입니다. 이 둘의 차이는 결정적입니다. CLAUDE.md에 적힌 내용은 어디까지나 제안입니다. 에이전트가 보통은 따르지만, 긴 대화에서 컨텍스트 윈도우 밖으로 밀려나면 무시될 수 있습니다. 반면 Hook은 결정적(deterministic)으로, 매번 반드시 실행됩니다.

 

저희 프로젝트에서는 .claude/settings.json에 PostToolUse Hook을 정의하여, 에이전트가 Java 파일을 수정할 때마다 Checkstyle을 자동으로 실행하도록 했습니다:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path // empty' | { read file; if echo \"$file\" | grep -qE '\\.java$'; then ./gradlew checkstyleMain -q 2>&1 | tail -20; fi; }"
          }
        ]
      }
    ]
  }
}

 

Write나 Edit 도구가 실행된 직후, 수정된 파일이 .java인 경우에만 checkstyleMain을 실행합니다. 위반이 있으면 에이전트는 즉시 피드백을 받고 스스로 수정합니다.

 

이 Hook이 왜 필요한지는, 기존에 설정해둔 Husky pre-commit과 비교하면 바로 와닿습니다(에이전트도 git commit을 실행하므로, 이것 역시 하네스의 일부로 작동합니다). 저희 프로젝트에는 이미 Husky로 커밋 시점에 checkstyleMain과 editorconfigCheck를 돌리고, commitlint로 커밋 메시지 형식을 강제하고 있었습니다:

# .husky/pre-commit
./gradlew checkstyleMain
./gradlew editorconfigCheck
./gradlew editorconfigFormat
# .husky/commit-msg
npx --no-install commitlint --edit

 

그런데 Husky는 git commit 시점에 한 번 실행됩니다. 에이전트가 파일 10개를 수정한 뒤 커밋하면, 그제야 10개 파일의 위반이 한꺼번에 쏟아지는 거죠. PostToolUse Hook은 매 수정마다 실행됩니다. 오류가 누적되기 전에 한 파일 단위로 잡아냅니다.

 

저희 프로젝트(약 300개 Java 파일) 기준으로 실행 시간은 약 5초 수준으로, 개발 흐름을 크게 방해하지 않습니다. 다만 Hook에 너무 무거운 작업을 걸면 에이전트의 작업 속도가 느려지기 때문에, ./gradlew test 전체를 Hook에 거는 것은 비현실적입니다. Checkstyle처럼 빠르게 끝나는 정적 분석만 Hook에 걸고, 전체 테스트는 기존대로 커밋/CI 시점에서 실행하는 것이 현실적입니다.

 

4. 서브에이전트 — 전문화된 역할 분리

Claude Code는 .claude/agents/ 디렉토리에 전문화된 서브에이전트를 정의할 수 있습니다. 서브에이전트는 자체 컨텍스트와 허용 도구 세트 내에서 실행되기 때문에, 메인 에이전트의 컨텍스트를 오염시키지 않으면서 전문적인 작업을 수행할 수 있습니다. 특히 코드 리뷰 같은 경우, 자기가 방금 작성한 코드를 자기가 리뷰하면 편향이 생기기 마련인데, 별도 컨텍스트의 서브에이전트에게 맡기면 이 문제를 줄일 수 있습니다.

저희 프로젝트에서는 두 개의 서브에이전트를 정의했습니다.

 

security-reviewer — Spring 특화 보안 리뷰 전문 에이전트입니다. SQL Injection, @PreAuthorize 누락, IDOR, Mass Assignment, 민감 정보 로깅, 소프트 삭제 누수 등을 체크리스트 기반으로 점검합니다:

<!-- .claude/agents/security-reviewer.md -->
---
name: security-reviewer
description: Spring Security 및 데이터 보호 관점의 보안 리뷰 에이전트
---

# Security Reviewer

프로젝트 컨벤션은 `AGENTS.md` (프로젝트 루트)를 참조할 것.

## 체크리스트

### 1. SQL Injection
- `@Query` 사용 시 반드시 파라미터 바인딩(`:paramName`) 사용 여부 확인
- QueryDSL 사용 시 `Expressions.stringTemplate` 등에서 사용자 입력이
  직접 삽입되지 않는지 확인

### 2. 인증/인가 누락
- 모든 컨트롤러 엔드포인트에 `@PreAuthorize("isAuthenticated()")`
  또는 적절한 권한 체크가 있는지 확인

### 3. Mass Assignment
- 요청 DTO에서 엔티티로 직접 바인딩하는 코드가 없는지 확인
- 반드시 Mapper를 통해 변환해야 함

### 4. IDOR (Insecure Direct Object Reference)
- 리소스 접근 시 소유권 검증(`isOwner()`) 또는
  `findByIdAndUser()` 패턴 사용 여부 확인

### 5. 민감 정보 로깅
- `log.info/debug/error`에 민감 데이터가 포함되지 않는지 확인

### 6. 소프트 삭제 누수
- 소프트 삭제를 사용하는 엔티티에 `@SQLRestriction("deleted_at is NULL")`이
  적용되어 있는지 확인

 

test-writer — 프로젝트 테스트 컨벤션을 준수하는 테스트 코드 작성 전문 에이전트입니다:

<!-- .claude/agents/test-writer.md -->
---
name: test-writer
description: 프로젝트 테스트 컨벤션에 맞는 테스트 코드 작성 에이전트
---

# Test Writer

프로젝트 컨벤션은 `AGENTS.md` (프로젝트 루트)를 참조할 것.
테스트 컨벤션은 `.agent-docs/test-conventions.md`를 반드시 읽고 따를 것.

## 베이스 클래스 선택
- UseCase 테스트 → `extends UseCaseTest`
- Service 테스트 → `extends ServiceTest`
- Repository 테스트 → `extends RepositoryTest`

## 필수 규칙
1. 한글 `@DisplayName` 사용
2. `@Nested`로 성공/실패 케이스 그루핑
3. Given-When-Then 주석 구조
4. `assertSoftly` 사용 (여러 검증 시)
5. Mockito BDD 스타일: `given().willReturn()`
6. 테스트 픽스처는 `testFixtureBuilder` + `*Fixtures` 클래스 사용
7. 예외 테스트: `assertThatThrownBy` + `hasFieldOrPropertyWithValue`
8. 변수명은 대문자 상수 스타일 (`USER`, `ROUTINE`)

## 워크플로우
1. 대상 프로덕션 코드를 읽고 테스트 대상 메서드 파악
2. 기존 Fixture 클래스 확인 (`src/test/java/im/toduck/fixtures/`)
3. 필요하면 새 Fixture 추가
4. 테스트 작성
5. `./gradlew test` 실행하여 통과 확인

 

사용법은 단순합니다. 메인 대화에서 "이 코드를 security-reviewer로 리뷰해줘" 또는 "test-writer로 이 서비스의 테스트를 작성해줘"라고 요청하면 됩니다.

 

5. 전체 하네스 구조

지금까지 소개한 요소들과, 프로젝트에 설정되어 있는 도구들을 합치면, 전체 하네스는 다음과 같은 계층 구조를 가집니다:

계층 도구 실행 시점 하네스 원칙
에이전트 컨텍스트 AGENTS.md, .agent-docs/ 세션 시작 Inform
에이전트 수정 직후 PostToolUse Hook 매 Java 파일 수정 Verify
커밋 시점 Husky pre-commit git commit Verify, Constrain
커밋 시점 Husky commit-msg git commit Constrain
빌드 시점 Checkstyle, EditorConfig ./gradlew build Constrain
테스트 시점 ArchUnit 11개 테스트 ./gradlew test Constrain, Verify
CI 시점 GitHub Actions PR → main/develop Verify
호출 시 서브에이전트 명시적 요청 Verify, Inform
[에이전트 코드 작성]
  │
  ├─ AGENTS.md / .agent-docs/ ─── 컨벤션 인지 (Inform)
  │
  ├─ PostToolUse Hook ─────────── checkstyle 즉각 피드백 (Verify)
  │
  ├─ git commit
  │   ├─ Husky pre-commit ─────── checkstyle + editorconfig (Verify)
  │   └─ Husky commit-msg ─────── commitlint (Constrain)
  │
  ├─ ./gradlew build
  │   ├─ Checkstyle ───────────── 코딩 스타일 (Constrain)
  │   └─ EditorConfig ─────────── 포맷팅 (Constrain)
  │
  ├─ ./gradlew test
  │   └─ ArchUnit ─────────────── 아키텍처 규칙 (Constrain + Verify)
  │
  └─ PR → GitHub Actions CI
      └─ gradlew build + JaCoCo ─ 전체 검증 + 커버리지 (Verify)

 

위에서 아래로 갈수록 검증 범위가 넓어지고 실행 비용이 높아집니다. 가볍고 빠른 검증을 위(에이전트 수정 직후)에서 잡고, 무겁지만 포괄적인 검증을 아래(CI)에서 잡는 구조입니다.

 

적용 후 느낀 점

사실 특별한 게 아니었다

전체 하네스 구조를 정리하고 나서 깨달은 건, 테이블에 있는 요소 중 상당수가 이번에 새로 만든 게 아니라는 점이었습니다.

 

ArchUnit으로 레이어 의존성을 강제하는 11개의 테스트:

@ArchTest
static final ArchRule 레이어_의존성_규칙을_준수한다 = layeredArchitecture()
    .consideringAllDependencies()
    .layer(API.name()).definedBy(API.getFullPackageName())
    .layer(CONTROLLER.name()).definedBy(CONTROLLER.getFullPackageName())
    // ... 8개 레이어 정의
    .whereLayer(CONTROLLER.name()).mayNotBeAccessedByAnyLayer()
    .whereLayer(SERVICE.name()).mayOnlyBeAccessedByLayers(USECASE.name(), SERVICE.name())
    .whereLayer(USECASE.name()).mayOnlyBeAccessedByLayers(API.name(), CONTROLLER.name())
    .whereLayer(REPOSITORY.name()).mayOnlyBeAccessedByLayers(SERVICE.name());

Controller에 @PreAuthorize를 강제하는 규칙:

@ArchTest
static final ArchRule Controller_메서드는_인증된_메서드만_포함한다 =
    methods()
        .that().areDeclaredInClassesThat().resideInAPackage(CONTROLLER.getFullPackageName())
        .and().arePublic()
        .should().beAnnotatedWith(PreAuthorize.class);

UseCase에서만 분산 락을 호출하도록 제한하는 규칙:

@ArchTest
static final ArchRule DistributedLock_메서드는_UseCase_클래스에서만_호출된다 =
    methods()
        .that().areDeclaredIn(DistributedLock.class)
        .should().onlyBeCalled().byClassesThat().resideInAPackage(USECASE.getFullPackageName())
        .orShould().beDeclaredIn(DistributedLock.class);

 

이것들은 전부 하네스 엔지니어링이라는 단어가 생기기 훨씬 전부터 프로젝트에 설정해둔 것들입니다. 휴먼 에러를 방지하기 위해서요. 코드 리뷰에서 "이 레이어에서 저 레이어를 직접 호출하면 안 돼요"를 반복하는 대신, 빌드에서 자동으로 잡아내기 위해 만든 것들입니다.

그런데 이 도구들은 코드를 누가 작성했는지 구분하지 않습니다. 사람이 쓴 코드든, AI 에이전트가 생성한 코드든, 동일하게 검증합니다. 결국 사람의 실수를 방지하기 위해 자동화해둔 것들이 AI 에이전트에게도 그대로 하네스로 작동하고 있었던 겁니다.

 

In a human-first workflow, these rules might feel pedantic or constraining. With agents, they become multipliers: once encoded, they apply everywhere at once.

— OpenAI, "Harness engineering: leveraging Codex in an agent-first world" (2026.02.11)

사람 중심 워크플로에서는 지나치게 세세하게 느껴질 수 있는 규칙들이, 에이전트를 쓰면 한 번 인코딩된 것이 어디에나 한꺼번에 적용되어 효과가 몇 배로 증폭된다는 것입니다.

 

이전에 ArchUnit으로 아키텍처 규칙 검증하기, 그리고 하네스 엔지니어링을 작성할 때, "테스트 코드가 살아있는 문서 역할을 한다"고 했었는데, 지금 와서 보면 그 문서의 독자가 사람에서 AI 에이전트로 확장된 셈입니다. 새로운 것을 다 갖추는 게 아니라, 이미 있는 것 위에서 에이전트가 프로젝트 규칙을 효율적으로 읽을 수 있도록 문서를 계층화하고, 위반을 커밋 시점이 아니라 수정 직후에 잡아주도록 피드백 시점을 앞당기고, 보안 리뷰나 테스트 작성 같은 전문 작업을 에이전트가 스스로 수행할 수 있도록 역할을 분리하는 작업이었습니다.

 

권장하는 적용 순서

ROI가 높은 순서대로 정리하면 다음과 같습니다:

  1. 기존 도구 점검 — ArchUnit, Checkstyle, Husky, CI가 이미 있다면 그 자체가 하네스. 빠진 게 있으면 이 시점에 채우는 게 가장 가성비가 좋음.
  2. AGENTS.md 작성 — 비용이 거의 없음. 50줄 이내로 핵심 규칙만 담기
  3. PostToolUse Hook 추가 — Checkstyle처럼 빠른 정적 분석을 에이전트 수정 직후에 걸어두면, 커밋 시점에 위반이 한꺼번에 터지는 상황을 방지 가능
  4. 상세 문서 분리 — .agent-docs/로 Progressive Disclosure를 구현.
  5. 서브에이전트 정의 — security-reviewer, test-writer 등 역할별 전문 에이전트.

 

하네스가 해결하지 못하는 것

하네스는 "정해진 규칙을 지키는가"를 검증하는 데 강합니다. 하지만 "이 설계가 맞는가", "이 비즈니스 로직이 요구사항과 일치하는가"는 여전히 사람의 판단 영역입니다. Checkstyle은 코드 스타일을 잡아주지만, 잘못된 도메인 모델링을 잡아주진 않습니다. ArchUnit은 레이어 의존 방향을 강제하지만, 불필요한 추상화를 경고하진 않습니다.

에이전트가 생성한 코드가 "컨벤션에 맞는 코드"인지와 "좋은 코드"인지는 다른 문제이고, 후자는 여전히 사람이 리뷰해야 합니다. 하네스는 리뷰의 부담을 줄여주지만, 리뷰 자체를 대체하지는 않습니다.

 

마무리

하네스 엔지니어링의 피드백 루프를 OpenAI는 이렇게 요약합니다.

When the agent struggles, we treat it as a signal: identify what is missing — tools, guardrails, documentation — and feed it back into the repository.

— OpenAI, "Harness engineering: leveraging Codex in an agent-first world" (2026.02.11)

 

에이전트가 어려움을 겪으면, 그것을 신호로 받아들여 무엇이 빠졌는지(도구, 가드레일, 문서) 파악하고 저장소에 다시 반영하라는 것입니다. 이 루프를 반복할수록 하네스는 두꺼워지고, 에이전트는 더 안정적으로 동작합니다.

 

사실 이건 우리가 신규 팀원 온보딩 때 하는 일과 크게 다르지 않습니다. 신규 팀원이 컨벤션을 어기면, 코드 리뷰에서 짚어주고, 자주 틀리는 부분은 ArchUnit 테스트나 Checkstyle 규칙으로 자동화합니다. 에이전트도 마찬가지입니다. 다만 에이전트는 "아, 그렇구나" 하고 기억하는 대신, 매번 새 세션에서 같은 실수를 반복할 수 있다는 점만 다릅니다. 그래서 "말로 알려주는 것"(AGENTS.md)과 "기계적으로 강제하는 것"(Hook, 린터, 테스트)을 함께 갖추는 게 중요합니다.

 

참고 자료