[Harness Engineering] 하네스 구조 해부




프로젝트 설정, 에이전트 정의, 스킬 파일 작성법


지금까지 하네스 엔지니어링의 개념, 기술, 구축 방법, 사례를 다뤘다. 이 편에서는 하네스의 물리적 구조를 해부한다. 실제 프로젝트에서 하네스를 구성하는 파일들이 어떤 역할을 하고, 어떻게 작성하고, 어떻게 연결되는지를 구체적으로 다룬다.


하네스의 물리적 구조

하네스는 거창한 인프라가 아니다. 저장소 안에 놓인 파일들의 집합이다. 구조를 분해하면 크게 세 가지 계층으로 나뉜다.

프로젝트/
├── .claude/                      # ── 하네스 루트 ──
│   ├── CLAUDE.md                 # 프로젝트 전체 규칙 (오케스트레이터)
│   ├── agents/                   # 전문 에이전트 정의
│   │   ├── architect.md          # 아키텍처 설계 전문
│   │   ├── implementer.md        # 구현 전문
│   │   ├── reviewer.md           # 코드 리뷰 전문
│   │   └── tester.md             # 테스트 전문
│   └── skills/                   # 재사용 가능한 스킬
│       ├── code-review/
│       │   └── skill.md
│       └── test-generation/
│           └── skill.md
│
├── src/                          # ── 제품 코드 ──
│   ├── CLAUDE.md                 # 레이어별 규칙 (선택적)
│   └── ...
│
├── tests/                        # ── 제약 강제 ──
│   ├── architecture.test.ts
│   └── ...
│
├── scripts/                      # ── 자동화 도구 ──
│   ├── entropy-score.ts
│   └── ...
│
├── .github/workflows/            # ── CI/CD ──
│   └── pr-check.yml
│
├── feature-list.json             # ── 상태 추적 ──
└── progress.md

각 구성 요소의 역할을 하나씩 살펴보자.


1. 오케스트레이터: CLAUDE.md

프로젝트 루트의 CLAUDE.md(또는 AGENTS.md)는 하네스의 중심이다. 에이전트가 가장 먼저 읽는 파일이며, 프로젝트의 전체 그림을 제공한다.

잘 작성된 오케스트레이터 파일은 세 가지를 포함한다. 프로젝트가 무엇인지, 어떤 구조인지, 어떤 규칙을 따르는지.

# CLAUDE.md

## 프로젝트 개요
SaaS 기반 프로젝트 관리 도구의 백엔드 API.
NestJS + TypeScript + PostgreSQL + Redis.
멀티 테넌트 구조로, 각 조직(Organization)이 독립된 데이터 공간을 가진다.

## 기술 스택
- Runtime: Node.js 20
- Framework: NestJS 10
- ORM: Prisma 5
- DB: PostgreSQL 16
- Cache: Redis 7
- Auth: JWT + Refresh Token
- Test: Jest + Supertest + Testcontainers

## 디렉토리 구조
src/
├── modules/              # 도메인 모듈 (각 모듈은 독립적)
│   ├── auth/             # 인증/인가
│   ├── organization/     # 조직 관리
│   ├── project/          # 프로젝트 관리
│   ├── task/             # 태스크 관리
│   └── notification/     # 알림
├── common/               # 모듈 간 공유 코드
│   ├── decorators/
│   ├── filters/
│   ├── guards/
│   ├── interceptors/
│   └── pipes/
├── infrastructure/       # 외부 서비스 연동
│   ├── database/
│   ├── cache/
│   ├── email/
│   └── storage/
└── config/               # 환경 설정

## 의존성 규칙
- modules/의 각 모듈은 다른 모듈의 내부 구현을 직접 import하지 않는다
- 모듈 간 통신은 NestJS의 Module imports + exported Service를 통해서만
- common/은 어떤 module도 import하지 않는다 (역방향 의존 금지)
- infrastructure/는 modules/를 import하지 않는다
- 모든 외부 API 호출은 infrastructure/를 통해서만

## 코딩 컨벤션
- NestJS 공식 스타일 가이드 준수
- 클래스명: PascalCase + 접미사 (TaskService, TaskController, TaskRepository)
- 파일명: kebab-case + 접미사 (task.service.ts, task.controller.ts)
- DTO: class-validator 데코레이터로 유효성 검증
- 모든 엔드포인트에 Swagger 데코레이터 필수

## 에러 처리
- 비즈니스 에러: src/common/exceptions/의 커스텀 예외 클래스 사용
- HTTP 매핑: GlobalExceptionFilter가 자동 처리
- 서비스 레이어에서 HttpException을 직접 던지지 않는다

## 테스트
- 서비스: 단위 테스트 (모든 의존성 mock)
- 컨트롤러: E2E 테스트 (Supertest + 실제 DB)
- 테스트 DB: Testcontainers로 PostgreSQL 인스턴스 자동 생성/파괴
- 커버리지 목표: 라인 80% 이상

## 에이전트 팀
이 프로젝트에서는 전문 에이전트를 활용한다.
에이전트 정의: .claude/agents/ 디렉토리 참조.
- architect: 모듈 설계, API 설계, 스키마 설계
- implementer: 서비스/컨트롤러/리포지토리 구현
- reviewer: 코드 리뷰, 아키텍처 준수 확인
- tester: 테스트 작성 및 커버리지 확보

핵심은 이 파일이 에이전트의 온보딩 문서 역할을 한다는 점이다. 새로 투입된 에이전트가 이 파일만 읽고도 “이 프로젝트에서 어떻게 작업해야 하는지”를 파악할 수 있어야 한다.


2. 전문 에이전트 정의

에이전트 팀 구조에서는 역할별로 전문화된 에이전트를 정의한다. 각 에이전트는 특정 영역에 집중하며, 다른 에이전트와 협업한다.

아키텍트 에이전트

# .claude/agents/architect.md

## 역할
시스템 아키텍처 설계와 기술 의사결정을 담당한다.
코드를 직접 작성하지 않는다. 설계 문서와 명세를 작성한다.

## 담당 업무
1. 새 모듈/기능의 아키텍처 설계
2. API 엔드포인트 설계 (OpenAPI 스펙)
3. 데이터베이스 스키마 설계 (Prisma 스키마)
4. 모듈 간 의존 관계 정의
5. 기술 의사결정 기록 (ADR 작성)

## 산출물 형식

### API 설계 문서
docs/api/{module-name}-api.md 에 작성:
- 엔드포인트 목록 (메서드, 경로, 설명)
- 요청/응답 스키마 (TypeScript 타입)
- 인증/인가 요구사항
- 에러 케이스

### 스키마 설계
prisma/schema.prisma에 반영:
- 새 모델 추가 시 기존 모델과의 관계 명시
- 인덱스 전략 포함
- 마이그레이션 파일 생성

### ADR (Architecture Decision Record)
docs/adr/NNNN-{title}.md 형식:
- 상태: proposed | accepted | deprecated
- 맥락: 왜 이 결정이 필요한가
- 결정: 무엇을 선택했는가
- 결과: 어떤 영향이 있는가

## 제약
- 코드 구현은 implementer 에이전트에게 위임한다
- 기존 아키텍처 패턴을 변경하려면 ADR을 먼저 작성한다
- 새 외부 의존성 추가 시 반드시 대안을 2개 이상 검토한다

## 다른 에이전트와의 협업
- implementer에게: API 설계 문서와 스키마를 전달
- reviewer에게: 아키텍처 준수 기준을 전달
- tester에게: 테스트 시나리오와 엣지 케이스를 전달

구현 에이전트

# .claude/agents/implementer.md

## 역할
architect가 설계한 명세를 바탕으로 실제 코드를 구현한다.

## 담당 업무
1. NestJS 모듈/컨트롤러/서비스/리포지토리 구현
2. DTO 클래스 작성 (class-validator 데코레이터 포함)
3. Prisma 클라이언트 연동
4. Swagger 데코레이터 추가
5. 에러 처리 구현

## 작업 절차
1. architect의 설계 문서 확인 (docs/api/, docs/adr/)
2. feature-list.json에서 구현할 기능 선택
3. 해당 모듈 디렉토리의 CLAUDE.md 확인
4. 코드 구현
5. 린터 + 타입 검사 통과 확인
6. tester에게 테스트 작성 요청 또는 직접 기본 테스트 작성
7. feature-list.json의 passes 필드 업데이트

## 코드 작성 규칙
- 하나의 PR에 하나의 기능만 구현
- 모든 서비스 메서드에 JSDoc 주석 필수
- DB 쿼리는 Prisma Client를 통해서만 (raw SQL 금지)
- 환경 변수는 ConfigService를 통해 접근 (process.env 직접 접근 금지)

## 금지 사항
- architect의 설계를 임의로 변경하지 않는다
- 다른 모듈의 내부 서비스를 직접 import하지 않는다
- 테스트 없이 기능을 완료로 표시하지 않는다

## 다른 에이전트와의 협업
- architect로부터: API 설계, 스키마, ADR 수신
- tester에게: 구현 완료 알림, 테스트 시나리오 제안
- reviewer에게: PR 리뷰 요청

리뷰어 에이전트

# .claude/agents/reviewer.md

## 역할
다른 에이전트가 작성한 코드의 품질과 아키텍처 준수 여부를 검증한다.

## 검증 항목

### 아키텍처 준수
- 의존성 방향 규칙 준수 여부
- 모듈 경계 위반 여부
- 에러 처리 패턴 준수 여부

### 코드 품질
- 네이밍 컨벤션 준수
- DTO 유효성 검증 누락 여부
- Swagger 데코레이터 누락 여부
- 하드코딩된 값 존재 여부
- 불필요한 주석 또는 주석 부재

### 보안
- SQL 인젝션 가능성
- 인증/인가 누락
- 민감 정보 로깅 여부
- 환경 변수 직접 접근

## 산출물 형식
리뷰 결과를 다음 형식으로 작성:

리뷰 결과: {기능명}

통과 항목

  • [x] 의존성 방향 준수
  • [x] 네이밍 컨벤션 준수

수정 필요

  • [ ] src/modules/task/task.service.ts:45 — ConfigService 대신 process.env 직접 접근
  • [ ] src/modules/task/dto/create-task.dto.ts — @IsNotEmpty() 누락

권장 사항 (필수 아님)

  • TaskService.findAll()에 페이지네이션 파라미터 추가 고려

## 제약
- 코드를 직접 수정하지 않는다. 리뷰 결과만 전달한다.
- 아키텍처 변경이 필요한 경우 architect에게 에스컬레이션한다.

테스터 에이전트

# .claude/agents/tester.md

## 역할
구현된 기능에 대한 테스트를 작성하고, 테스트 커버리지를 확보한다.

## 담당 업무
1. 서비스 단위 테스트 작성
2. 컨트롤러 E2E 테스트 작성
3. 엣지 케이스 및 에러 시나리오 테스트
4. 테스트 커버리지 확인 및 보고

## 테스트 작성 규칙

### 단위 테스트 (*.service.spec.ts)
- 모든 외부 의존성은 mock
- 테스트 네이밍: "should [동작] when [조건]"
- 정상 케이스 + 에러 케이스 + 경계값 각각 최소 1개

### E2E 테스트 (*.e2e-spec.ts)
- Testcontainers로 실제 PostgreSQL 사용
- 인증이 필요한 엔드포인트는 JWT 토큰 발급 후 테스트
- 응답 상태 코드 + 응답 바디 구조 모두 검증

### 테스트 템플릿

서비스 단위 테스트:
```typescript
describe('TaskService', () => {
  let service: TaskService;
  let prisma: DeepMockProxy<PrismaClient>;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [
        TaskService,
        { provide: PrismaService, useValue: mockDeep<PrismaClient>() },
      ],
    }).compile();

    service = module.get(TaskService);
    prisma = module.get(PrismaService);
  });

  describe('create', () => {
    it('should create a task when valid input is provided', async () => {
      // Arrange
      const input = { title: 'Test Task', projectId: 'proj-1' };
      prisma.task.create.mockResolvedValue({ id: 'task-1', ...input });

      // Act
      const result = await service.create(input);

      // Assert
      expect(result.id).toBe('task-1');
      expect(prisma.task.create).toHaveBeenCalledWith({ data: input });
    });

    it('should throw ProjectNotFoundError when project does not exist', async () => {
      // Arrange
      prisma.project.findUnique.mockResolvedValue(null);

      // Act & Assert
      await expect(service.create({ title: 'Test', projectId: 'invalid' }))
        .rejects.toThrow(ProjectNotFoundError);
    });
  });
});

커버리지 기준

  • 전체 라인 커버리지: 80% 이상
  • 새로 추가된 파일: 90% 이상
  • 커버리지 미달 시 implementer에게 추가 구현 요청

---

## 3. 스킬 파일: 재사용 가능한 전문 지식

스킬은 특정 작업 유형에 대한 전문 지식을 캡슐화한 파일이다. 에이전트가 필요한 시점에 로드하여 사용한다. 모든 지식을 CLAUDE.md에 넣는 대신, 스킬로 분리하면 컨텍스트 효율이 높아진다.

```markdown
# .claude/skills/code-review/skill.md

## 트리거
- "코드 리뷰해줘", "이 PR 검토해줘", "리뷰"
- 새 PR이 생성되었을 때

## 체크리스트

### 필수 확인 (하나라도 실패하면 수정 요청)
- [ ] 아키텍처 의존성 방향 준수
- [ ] any 타입 사용 여부
- [ ] console.log 사용 여부
- [ ] 하드코딩된 비밀값/URL 여부
- [ ] 에러 처리 패턴 준수 (커스텀 예외 사용)
- [ ] DTO 유효성 검증 데코레이터 존재
- [ ] 테스트 파일 존재 여부

### 권장 확인 (코멘트로 제안)
- [ ] 함수 길이 30줄 초과 여부
- [ ] 중복 코드 여부
- [ ] 누락된 엣지 케이스
- [ ] 성능 관련 잠재적 이슈 (N+1 쿼리 등)

## 출력 형식
리뷰 결과를 마크다운 체크리스트로 작성.
수정 필요 항목은 파일 경로와 라인 번호를 포함.
# .claude/skills/test-generation/skill.md

## 트리거
- "테스트 작성해줘", "이 서비스 테스트", "커버리지 올려줘"
- 새 서비스 파일이 생성되었을 때

## 절차
1. 대상 파일의 public 메서드 목록 추출
2. 각 메서드별 테스트 시나리오 도출:
   - 정상 입력 → 기대 출력
   - 잘못된 입력 → 적절한 에러
   - 경계값 → 올바른 처리
   - 의존성 실패 → 에러 전파
3. 테스트 코드 작성 (프로젝트의 테스트 컨벤션 준수)
4. 테스트 실행 및 통과 확인
5. 커버리지 확인

## 의존성 mock 규칙
- Prisma: jest-mock-extended의 mockDeep 사용
- Redis: ioredis-mock 사용
- 외부 API: jest.fn()으로 manual mock
- 시간: jest.useFakeTimers()

4. 상태 추적 파일

하네스의 동적 요소인 상태 추적 파일은 에이전트의 세션 간 기억을 담당한다.

feature-list.json의 실전 구조

{
  "project": "project-management-api",
  "lastUpdated": "2026-03-20T14:30:00Z",
  "features": [
    {
      "id": "AUTH-001",
      "module": "auth",
      "priority": 1,
      "description": "이메일/비밀번호 회원가입",
      "acceptanceCriteria": [
        "POST /api/auth/signup 엔드포인트 동작",
        "이메일 형식 검증",
        "비밀번호 8자 이상, 대소문자+숫자 포함",
        "중복 이메일 409 응답",
        "비밀번호 bcrypt 해싱",
        "E2E 테스트 통과"
      ],
      "passes": true,
      "completedAt": "2026-03-18T10:00:00Z"
    },
    {
      "id": "AUTH-002",
      "module": "auth",
      "priority": 1,
      "description": "JWT 기반 로그인",
      "acceptanceCriteria": [
        "POST /api/auth/login 엔드포인트 동작",
        "access token (15분) + refresh token (7일) 발급",
        "잘못된 자격 증명 시 401 응답",
        "refresh token은 HttpOnly 쿠키",
        "E2E 테스트 통과"
      ],
      "passes": true,
      "completedAt": "2026-03-19T09:00:00Z"
    },
    {
      "id": "PROJ-001",
      "module": "project",
      "priority": 2,
      "description": "프로젝트 CRUD",
      "acceptanceCriteria": [
        "POST/GET/PUT/DELETE /api/projects 엔드포인트",
        "조직 소속 멤버만 접근 가능",
        "프로젝트명 필수, 1~100자",
        "삭제 시 soft delete",
        "페이지네이션 지원 (cursor 기반)",
        "단위 테스트 + E2E 테스트 통과"
      ],
      "passes": false,
      "completedAt": null
    },
    {
      "id": "TASK-001",
      "module": "task",
      "priority": 3,
      "description": "태스크 생성 및 할당",
      "acceptanceCriteria": [
        "POST /api/projects/:id/tasks 엔드포인트",
        "담당자 할당 (조직 멤버 검증)",
        "우선순위 설정 (low/medium/high/urgent)",
        "마감일 설정 (과거 날짜 불가)",
        "단위 테스트 + E2E 테스트 통과"
      ],
      "passes": false,
      "completedAt": null
    }
  ]
}

이 파일이 feature-list의 역할을 넘어 프로젝트의 로드맵으로도 기능한다. 에이전트는 priority 순서대로 작업을 선택하고, acceptanceCriteria를 만족해야만 passes를 true로 변경할 수 있다.


전체 파일 간의 연결 관계

하네스의 각 파일은 독립적이지 않다. 서로 참조하고, 연결되어 하나의 시스템을 이룬다.

CLAUDE.md (오케스트레이터)
  │
  ├─ 참조 → agents/*.md          (누가 무엇을 하는가)
  ├─ 참조 → src/*/CLAUDE.md      (레이어별 상세 규칙)
  ├─ 참조 → feature-list.json    (무엇을 만들어야 하는가)
  │
agents/architect.md
  │
  ├─ 산출 → docs/api/*.md        (API 설계)
  ├─ 산출 → docs/adr/*.md        (아키텍처 결정)
  ├─ 산출 → prisma/schema.prisma (스키마)
  │
agents/implementer.md
  │
  ├─ 입력 ← docs/api/*.md        (설계 참조)
  ├─ 입력 ← feature-list.json    (작업 선택)
  ├─ 산출 → src/modules/*        (코드)
  ├─ 갱신 → feature-list.json    (passes 업데이트)
  ├─ 갱신 → progress.md          (진행 기록)
  │
agents/tester.md
  │
  ├─ 입력 ← src/modules/*        (테스트 대상)
  ├─ 산출 → tests/*              (테스트 코드)
  │
agents/reviewer.md
  │
  ├─ 입력 ← src/modules/*        (리뷰 대상)
  ├─ 입력 ← CLAUDE.md            (규칙 기준)
  ├─ 산출 → 리뷰 코멘트          (수정 요청)
  │
tests/architecture.test.ts
  │
  └─ 검증 → src/modules/*        (의존성 방향)

CI 파이프라인
  │
  └─ 실행 → 린터 + 타입 + 아키텍처 테스트 + 단위 테스트 + E2E

이 연결 관계가 하네스의 본질이다. 개별 파일이 아니라, 파일들 간의 연결이 에이전트가 일관되게 작업할 수 있는 환경을 만든다. 오케스트레이터가 전체 규칙을 정의하고, 전문 에이전트가 역할을 나누고, 스킬이 전문 지식을 제공하고, 상태 추적이 연속성을 보장하고, 테스트와 CI가 품질을 강제한다.

하나의 파일이 빠지면 시스템에 구멍이 생긴다. 하지만 모든 파일이 처음부터 필요한 것은 아니다. CLAUDE.md 하나로 시작해서, 필요에 따라 에이전트 정의를 추가하고, 스킬을 분리하고, 상태 추적을 도입하면 된다. 하네스는 한 번에 완성하는 것이 아니라, 프로젝트와 함께 성장하는 것이다.




댓글 남기기