[Harness Engineering] 컨텍스트 엔지니어링




에이전트에게 추측시키지 말고, 읽게 하라


새로 입사한 개발자에게 “결제 모듈 만들어줘”라고만 말하는 회사는 없다. 코딩 컨벤션 문서를 주고, 아키텍처 다이어그램을 보여주고, 기존 코드를 읽게 하고, 시니어 개발자가 코드 리뷰로 방향을 잡아준다. 그래야 그 회사의 방식에 맞는 코드가 나온다.

AI 에이전트도 다르지 않다. 아무런 맥락 없이 작업을 시키면, 에이전트는 훈련 데이터에서 본 가장 일반적인 패턴으로 코드를 생성한다. 프로젝트의 컨벤션, 아키텍처 결정, 금지 패턴 같은 것은 알 수가 없다. 존재하지만 전달되지 않은 정보는, 에이전트 입장에서는 존재하지 않는 것과 같다.

컨텍스트 엔지니어링은 이 문제를 해결한다. 에이전트가 올바른 결과물을 만들기 위해 필요한 정보를, 머신이 읽을 수 있는 형태로 구조화하고, 작업 시작 시 자동으로 제공하는 것이다.


정적 컨텍스트: 규칙 파일

컨텍스트의 가장 기본적인 형태는 규칙 파일이다. Claude Code에서는 CLAUDE.md, Codex에서는 AGENTS.md라는 이름으로 저장소 루트에 배치한다. 에이전트는 작업을 시작할 때 이 파일을 자동으로 읽는다.

나쁜 예: 모호하고 선언적인 규칙

# AGENTS.md

좋은 코드를 작성해주세요.
테스트를 꼼꼼히 해주세요.
기존 코드 스타일을 따라주세요.

이런 규칙은 인간에게도 모호하다. 에이전트에게는 더더욱 실행 불가능한 지시다. “좋은 코드”가 무엇인지, “꼼꼼한 테스트”가 어느 수준인지, “기존 스타일”이 구체적으로 뭔지 알 수 없다.

좋은 예: 구체적이고 기계적으로 검증 가능한 규칙

# AGENTS.md

## 프로젝트 구조
src/
├── domain/          # 비즈니스 로직 (외부 의존성 금지)
├── application/     # 유스케이스 오케스트레이션
├── infrastructure/  # DB, API 클라이언트, 외부 서비스
├── presentation/    # HTTP 핸들러, 응답 포맷팅
└── shared/          # 도메인 간 공유 타입, 유틸

## 의존성 규칙
- domain/ → 어디에도 의존하지 않음 (순수 비즈니스 로직)
- application/ → domain/만 참조 가능
- infrastructure/ → domain/, application/ 참조 가능
- presentation/ → application/만 참조 가능
- presentation/에서 infrastructure/를 직접 import하면 안 됨

## 코딩 컨벤션
- 함수명: camelCase
- 파일명: kebab-case.ts
- 타입/인터페이스: PascalCase
- 상수: UPPER_SNAKE_CASE
- any 타입 사용 금지 → unknown 사용 후 타입 가드로 좁히기
- console.log 금지 → Logger.info/warn/error 사용

## 에러 처리
- 도메인 에러는 DomainError를 상속받는 커스텀 에러 클래스 사용
- infrastructure에서 발생하는 외부 에러는 반드시 도메인 에러로 변환
- presentation에서 DomainError를 HTTP 상태 코드로 매핑

## 테스트 규칙
- 모든 도메인 로직에 단위 테스트 필수
- application 레이어는 통합 테스트
- 테스트 파일 위치: 대상 파일과 같은 디렉토리에 *.test.ts
- 테스트 네이밍: "should [기대 동작] when [조건]"

## 금지 패턴
- ORM 엔티티를 도메인 바깥으로 노출하지 않음
- 컨트롤러에서 직접 DB 쿼리 금지
- 비즈니스 로직 안에서 HTTP 요청 금지
- 글로벌 상태 변경 금지

차이가 명확하다. 두 번째 예시는 에이전트가 코드를 작성할 때 각 결정 지점에서 참조할 수 있는 구체적 기준을 제공한다. “이 파일은 어디에 위치해야 하는가?”, “이 모듈은 무엇을 import할 수 있는가?”, “에러를 어떻게 처리해야 하는가?”에 대한 답이 명시되어 있다.

계층적 AGENTS.md: 지도 vs 매뉴얼

OpenAI가 겪은 핵심 실패 중 하나는 “하나의 거대한 AGENTS.md” 접근이었다. 모든 규칙을 한 파일에 모아놓으니, 컨텍스트 윈도우를 과도하게 차지하고, 모든 것이 “중요”하니 아무것도 중요하지 않게 되었다.

해법은 계층적 구조다.

프로젝트 루트/
├── AGENTS.md                    # 전체 아키텍처, 핵심 규칙만
├── src/
│   ├── domain/
│   │   └── AGENTS.md            # 도메인 레이어 전용 규칙
│   ├── infrastructure/
│   │   └── AGENTS.md            # 인프라 레이어 전용 규칙
│   └── presentation/
│       └── AGENTS.md            # 프레젠테이션 레이어 전용 규칙

에이전트가 src/domain/ 안에서 작업할 때는 루트 AGENTS.md와 도메인 AGENTS.md만 로드된다. 인프라 레이어의 세부 규칙까지 컨텍스트에 올릴 필요가 없다. 에이전트에게 1,000페이지 매뉴얼이 아니라, 현재 위치에서 필요한 지도만 주는 것이다.

컨텍스트 활용률이 약 40%를 초과하면 오히려 성능이 저하된다는 실험 결과도 있다. 도구, 장황한 문서, 축적된 이력을 과다하게 제공하면 에이전트가 더 나빠진다. 적절한 양의 정확한 정보가 과다한 양의 포괄적 정보보다 낫다.


동적 컨텍스트: 진행 상태 관리

정적 규칙만으로는 부족하다. 에이전트는 “지금 프로젝트가 어떤 상태인가”도 알아야 한다.

기능 목록: JSON으로 진행 추적

Anthropic이 장기 실행 에이전트 실험에서 발견한 문제 중 하나는, 에이전트가 프로젝트를 조기에 “완료” 선언하는 것이었다. 몇 가지 기능이 구현된 것을 보고 “다 됐다”고 판단하는 것이다.

해결책은 구조화된 기능 목록이었다.

{
  "features": [
    {
      "id": "AUTH-001",
      "category": "authentication",
      "description": "이메일/비밀번호로 회원가입할 수 있다",
      "steps": [
        "회원가입 폼에 이메일과 비밀번호를 입력",
        "유효성 검증 (이메일 형식, 비밀번호 8자 이상)",
        "중복 이메일 검사",
        "비밀번호 해싱 후 DB 저장",
        "인증 이메일 발송"
      ],
      "passes": true
    },
    {
      "id": "AUTH-002",
      "category": "authentication",
      "description": "로그인 후 JWT 토큰을 발급받을 수 있다",
      "steps": [
        "로그인 폼에 이메일과 비밀번호를 입력",
        "자격 증명 검증",
        "access token + refresh token 발급",
        "refresh token은 HttpOnly 쿠키에 저장"
      ],
      "passes": false
    },
    {
      "id": "PAY-001",
      "category": "payment",
      "description": "신용카드로 결제할 수 있다",
      "steps": [
        "결제 정보 입력",
        "PG사 API 호출",
        "결제 결과 저장",
        "결제 확인 이메일 발송"
      ],
      "passes": false
    }
  ]
}

핵심 규칙은 두 가지다.

첫째, 에이전트는 이 파일에서 passes 필드만 변경할 수 있다. 기능을 삭제하거나 설명을 수정하는 것은 금지된다. “테스트를 삭제하거나 편집하는 것은 허용되지 않는다. 이는 기능 누락이나 버그로 이어질 수 있다”는 제약을 명시한다.

둘째, JSON 형식을 사용한다. Anthropic의 실험에서 Markdown보다 JSON이 에이전트의 부적절한 수정을 줄이는 데 효과적이었다. 구조화된 데이터 형식은 에이전트가 자의적으로 내용을 변경하기 어렵게 만든다.

진행 로그: 세션 간 기억 전달

AI 에이전트의 근본적 한계는 세션이 끝나면 기억이 사라진다는 것이다. Anthropic은 이를 “교대 근무에서 매번 기억을 잃는 엔지니어”에 비유했다. 이전 교대에서 무슨 일이 있었는지 모르는 상태로 새 엔지니어가 도착하는 것이다.

해결책은 파일시스템 기반의 진행 로그다.

# progress.md

## 2025-12-15 세션

### 완료
- AUTH-001: 회원가입 기능 구현 완료
  - bcrypt로 비밀번호 해싱 구현
  - 이메일 유효성 검증 추가
  - 중복 이메일 체크 로직 구현

### 발견된 이슈
- 이메일 발송 모듈 미구현 상태, 현재 로그만 출력
- UserRepository.findByEmail()에서 인덱스 누락 → 성능 이슈 가능

### 다음 세션에서 할 일
- AUTH-002: 로그인 및 JWT 발급 구현
- 이메일 발송 모듈 연결

### 아키텍처 결정
- refresh token 저장: Redis 대신 DB 사용 결정
  - 이유: 현재 규모에서 Redis 도입은 오버엔지니어링
  - 추후 사용자 1만 명 초과 시 Redis 전환 검토

---

## 2025-12-16 세션

### 시작 상태 확인
- progress.md 읽기 → 이전 세션 상태 파악
- git log 확인 → 최근 커밋 3건 확인
- feature-list.json 확인 → AUTH-001 passes:true, 나머지 false
- 개발 서버 기동 → 회원가입 기능 정상 동작 확인

### 완료
- AUTH-002: 로그인 및 JWT 발급 구현
  ...

새 세션의 에이전트는 이 파일을 읽는 것으로 시작한다. 무엇이 완료되었고, 어떤 이슈가 있고, 어떤 아키텍처 결정이 내려졌는지를 빠르게 파악한 후, 기능 목록에서 다음 작업을 선택한다.

이 접근의 핵심은 대화 기록이 아니라 파일시스템이 메모리라는 점이다. 대화 기록은 세션이 끝나면 사라지지만, 파일시스템에 기록된 진행 상태는 영속적이다. git 커밋과 함께 진행 로그를 관리하면, 필요할 때 이전 상태로 되돌릴 수도 있다.


단일 진실 소스: 저장소에 없으면 존재하지 않는다

컨텍스트 엔지니어링의 가장 중요한 원칙은 이것이다.

에이전트의 관점에서, 실행 중 컨텍스트 안에서 접근할 수 없는 것은 존재하지 않는다.

구글 독스에 있는 기획 문서, 슬랙에서 결정된 아키텍처 방향, 시니어 개발자의 머릿속에만 있는 “우리는 이렇게 하지 않아”라는 규칙 — 이 모든 것은 에이전트가 접근할 수 없다.

이것은 비기술직 리더에게도 중요한 시사점이다. 중요한 결정, 정책, 표준이 사람들의 머릿속이나 메신저 대화에만 존재한다면, AI 도구는 그것을 따를 수 없다. 인간 엔지니어는 이런 암묵지를 회의, 코드 리뷰, 잡담을 통해 흡수하지만, 에이전트는 불가능하다.

실천 방법은 단순하지만 철저해야 한다.

저장소에 넣어야 할 것:
  ✅ 아키텍처 결정 기록 (ADR: Architecture Decision Records)
  ✅ 코딩 컨벤션
  ✅ API 설계 원칙
  ✅ 금지 패턴과 그 이유
  ✅ 환경 설정 방법 (init.sh)
  ✅ 에러 처리 정책

저장소 밖에 두면 안 되는 것:
  ❌ "슬랙에서 결정한" 규칙
  ❌ "다들 알고 있는" 컨벤션
  ❌ "위키에 적어둔" 아키텍처 문서
  ❌ "회의에서 합의한" 방향

초기화 스크립트: 에이전트의 첫 출근

Anthropic의 실험에서 효과적이었던 패턴 중 하나는 초기화 스크립트다. 에이전트가 매 세션 시작 시 일관된 절차를 밟도록 만드는 것이다.

#!/bin/bash
# init.sh — 에이전트의 매 세션 시작 루틴

# 1. 현재 위치 확인
pwd

# 2. 최근 변경 사항 파악
git log --oneline -10

# 3. 진행 상태 읽기
cat progress.md

# 4. 기능 목록 확인
cat feature-list.json | jq '.features[] | select(.passes == false) | .id + ": " + .description'

# 5. 개발 서버 기동
npm run dev &

# 6. 기본 기능 동작 확인 (스모크 테스트)
sleep 5
curl -s http://localhost:3000/health | jq .

이 스크립트가 하는 일은 간단하다. 에이전트가 “지금 어디에 있고, 무엇이 완료되었고, 다음에 무엇을 해야 하는지”를 파악한 후, 기존 기능이 정상 작동하는지 확인하고 나서 새 작업에 착수하도록 만드는 것이다.

초기화 없이 바로 작업에 들어가면, 에이전트가 이전 세션에서 남겨둔 불완전한 상태 위에 새 기능을 구현하면서 문제가 더 꼬이는 경우가 빈번했다. 새 기능을 만들기 전에 기존 상태를 확인하는 것이, 결과적으로 더 빠른 진행을 만들었다.


컨텍스트 엔지니어링의 핵심 원칙 정리

1. 구체적으로 작성하라
   "좋은 코드를 써줘" (X)
   "함수명은 camelCase, any 타입 금지, DB 접근은 Repository를 통해서만" (O)

2. 계층적으로 구성하라
   하나의 거대한 파일 (X)
   루트 규칙 + 레이어별 세부 규칙 (O)

3. 컨텍스트 양을 관리하라
   모든 정보를 한꺼번에 제공 (X)
   현재 작업에 필요한 정보만 선별 제공 (O)

4. 상태를 파일시스템에 기록하라
   대화 기록에 의존 (X)
   progress.md + feature-list.json + git (O)

5. 저장소를 단일 진실 소스로 만들어라
   슬랙, 위키, 머릿속에 분산 (X)
   모든 규칙과 결정을 저장소 안에 (O)

컨텍스트 엔지니어링은 하네스의 첫 번째 기둥이다. 에이전트에게 “무엇을 해야 하는가”를 알려주는 영역이다. 하지만 알려주는 것만으로는 부족하다. 에이전트가 규칙을 알면서도 위반할 수 있기 때문이다. 그래서 두 번째 기둥이 필요하다 — 아키텍처 제약. 규칙을 어기면 코드가 저장소에 들어갈 수 없게 만드는 구조적 강제다.




댓글 남기기