LLM(거대언어모델) 기반의 AI 에이전트에게 복잡한 장기 프로젝트를 맡길 때, 대부분은 실패한다. 초기에는 놀라운 퍼포먼스를 보여주던 에이전트가 시간이 지날수록 환각(Hallucination)을 일으키거나, 이전에 했던 수정 사항을 망각하고 엉뚱한 코드를 생산하기 시작한다.
이유는 명확하다. LLM은 **Stateless(무상태)**이며, 유일한 기억 공간인 **Context Window(컨텍스트 윈도우)**는 유한하기 때문이다.
컨텍스트 윈도우가 가득 차는 순간, 에이전트의 지능은 급격히 하락한다. 이를 단순히 '모델의 성능 문제'로 치부해서는 안 된다. 이는 시스템 설계(System Design)의 부재다. AI 에이전트가 장기적인 작업을 수행할 수 있도록 붙잡아주는 안전장치, 즉 **'하네스(Harness)'**를 어떻게 설계해야 하는가에 대한 엔지니어링 관점의 고찰이 필요하다.
1. 컨텍스트 윈도우와 '교대 근무'의 역설
컨텍스트 윈도우가 교체되는 상황을 비유하자면, 개발자가 교대 근무를 하는 상황과 같다.
A 엔지니어가 8시간 동안 로그인 기능을 구현하고 퇴근했다. 다음 8시간을 맡은 B 엔지니어가 출근했다. 만약 A가 아무런 인수인계 문서 없이 퇴근했다면 B는 무엇을 해야 하는가?
대부분의 AI 에이전트 시스템은 B 엔지니어에게 A의 작업 내용을 전달하기 위해 '요약(Summarization)' 방식을 택한다. 하지만 이는 위험한 접근이다. 요약은 '정보 밀도'를 희석시킨다. "로그인 기능을 만들었다"는 텍스트에는, 구체적으로 어떤 의사결정을 거쳤는지, 왜 이 라이브러리를 썼는지에 대한 **맥락(Context)**이 거세되어 있다.
따라서 단순히 대화 내역을 요약하여 넘겨주는 것만으로는 부족하다. 다음 세션의 에이전트가 작업을 이어받기 위해서는 파레토 법칙에 입각한 **'핵심 맥락(Critical Context)'**의 보존이 필수적이다.
2. 실패하는 에이전트의 패턴: One-Shot과 완료 조건의 부재
우리는 에이전트에게 너무 많은 것을 한 번에 기대한다.
- One-Shot 패턴의 오만: 복잡한 기능을 한 번의 프롬프트로 완성하려는 시도는 필패한다. 작업은 반드시 원자 단위로 쪼개져야 한다.
- 모호한 완료 조건: 에이전트는 코드가 실행만 되면 '완료'라고 착각한다. 기능이 오작동해도 에러 로그가 없으면 성공으로 판단한다.
이러한 실패를 막기 위해서는 작업의 **'정의'**와 **'완료'**를 기계적으로 강제해야 한다.
3. 기계 가독성(Machine Readability): 에이전트 통제를 위한 프로토콜
사람이 읽기 좋은 문서(Markdown)는 에이전트에게 '해석의 자유'를 허용한다. 하지만 엔지니어링에서 자유는 곧 버그다. 에이전트의 고질적인 실패 패턴인 **'One-Shot(한 번에 다 하려는 욕심)'**과 **'완료 조건의 모호함'**을 해결하기 위해선, 해석이 불가능한 **구조화된 데이터(Structured Data)**로 지시를 내려야 한다.
우리는 역할에 따라 파일 포맷을 엄격히 분리해야 한다.
- Markdown (.md): 읽기 전용. 배경지식, 설계 사상 등 'Context' 주입용.
- YAML (.yaml): 환경 설정. 변경되지 않는 상수값.
- JSON (.json): 실행 제어(Control) 및 작업 명세.
특히 작업 명세서(Task Specification)는 반드시 JSON으로 관리되어야 한다. JSON은 파싱 에러를 통해 오염된 정보를 즉시 차단할 수 있고, 필드(Field) 단위로 에이전트의 사고 과정을 강제할 수 있기 때문이다.
3.1 One-Shot 방지: 작업의 원자성(Atomicity) 강제
에이전트가 "로그인 페이지를 만들어줘"라는 프롬프트를 받으면, HTML/CSS/JS/DB연동을 한 번에 쏟아내려다 컨텍스트 윈도우가 터진다. 이를 막기 위해 JSON 리스트로 작업을 잘게 쪼개서 주입해야 한다. 에이전트는 전체를 볼 필요 없이, 배열의 [0]번 인덱스(Current Task)만 처리하면 된다.
3.2 완료 조건(Definition of Done)의 코드화
"잘 작동하는지 확인해"라는 자연어 지시는 무의미하다. 에이전트는 실행만 되면 성공이라고 착각한다. 완료 조건을 자연어가 아닌 **검증 가능한 커맨드(Verifiable Command)**로 명시해야 한다.
다음은 이 전략이 적용된 tasks.json의 예시다.
{
"project_status": "in_progress",
"current_task_id": "AUTH-001",
"tasks": [
{
"id": "AUTH-001",
"category": "feature",
"priority": "high",
"description": "JWT 발급을 위한 유틸리티 함수 구현 (UI 구현 금지)",
"constraints": [
"One-Shot 금지: 오직 토큰 생성/검증 로직만 작성할 것",
"외부 라이브러리는 PyJWT만 사용할 것"
],
"definition_of_done": {
"checklist": [
"SECRET_KEY 환경변수 로딩 확인",
"HS256 알고리즘 적용 확인"
],
"validation_cmd": "python -m pytest tests/auth/test_jwt_core.py",
"expected_output": "passed"
},
"next_action": "git commit -m 'feat: implement jwt core logic'"
}
]
}
이 구조가 해결하는 문제는 명확하다.
- 범위 제한 (Constraints): description과 constraints 필드를 통해 "UI 구현 금지" 등 작업의 경계를 명확히 하여 One-Shot 시도를 원천 차단한다.
- 기계적 검증 (Validation Cmd): validation_cmd가 성공(passed)하지 않으면 다음 스텝으로 넘어가지 못하게 시스템적으로 막는다. 에이전트의 "다 했어요"라는 거짓말을 pytest가 걸러낸다.
- 의존성 관리: JSON 배열의 순서 자체가 작업의 의존성(Dependency)이 되어, 에이전트가 순서를 건너뛰는 것을 방지한다.
결국 기계 가독성을 높인다는 것은, 에이전트에게 **'읽기 좋은 글'**을 주는 것이 아니라 **'따를 수밖에 없는 규격'**을 주는 것이다.
4. 세션 관리와 계층적 컨텍스트: 코드보다 중요한 '의도'와 '구조'의 보존
단순히 progress.md 하나에 모든 진행 상황을 구겨 넣는 방식은 하수다. 프로젝트가 커질수록 컨텍스트 윈도우는 비명을 지르고, 정보의 휘발성은 높아진다.
성공적인 장기 프로젝트를 위해서는 컨텍스트를 정보의 성격과 변경 주기에 따라 계층적으로 분리(Tiering)해야 한다. 특히 에이전트가 코드를 망치지 않게 하려면 '무엇(Code)'을 짰는지보다, '왜(Why)' 그렇게 짰는지와 **'어디(Where)'**에 속하는지에 대한 정보가 훨씬 중요하다.
4.1 Static Context: 불변의 법칙 (ADR & Components)
에이전트가 가장 자주 저지르는 실수는, 인간이 고심 끝에 만들어놓은 '의도적인 비효율'을 '리팩터링 대상'으로 착각하고 수정해버리는 것이다. 이를 막기 위해 두 가지 핵심 문서는 반드시 로딩되어야 한다.
A. ADR (Architecture Decision Records): 'Why'의 보존
코드는 결과일 뿐, 그 코드가 나오게 된 맥락은 코드에 없다.
- 목적: 에이전트가 **"체스터턴의 울타리(Chesterton's Fence)"**를 걷어차지 못하게 막는다. 왜 우리가 RabbitMQ 대신 Kafka를 썼는지, 왜 이 구간에서 정규화를 포기했는지에 대한 의사결정을 문서화한다.
- 형식: /docs/adr/001-use-postgres-jsonb.md
- 활용: 에이전트가 특정 모듈을 수정하려 할 때, 관련된 ADR을 먼저 읽혀서 **"이 코드는 버그가 아니라 의도된 설계임"**을 주지시켜야 한다.
B. Component Specifications: 시스템의 '지도'
수천 줄의 코드를 다 읽고 시스템을 파악하는 것은 토큰 낭비다. 각 폴더나 모듈이 전체 시스템에서 어떤 역할을 하는지 정의한 고수준(High-level) 문서가 필요하다.
- 목적: 마이크로서비스나 주요 컴포넌트의 역할과 경계(Bounded Context)를 명확히 한다.
- 내용: /docs/components/auth-service.md
- Responsibility: 이 컴포넌트가 하는 일 (예: 유저 인증, 토큰 발급)
- Inbound/Outbound: 누구에게 요청을 받고, 누구를 호출하는가.
- Key Files: 핵심 진입점 파일 위치.
- 효과: 에이전트가 auth 폴더를 건드릴 때, 결제 로직을 섞어 짜는 아키텍처 위반(Architectural Erosion)을 방지한다.
4.2 Dynamic Context: 진행의 흐름 (Progress & Snapshot)
시스템의 상태는 변한다. 변하는 정보는 별도로 관리하여 최신성을 유지해야 한다.
- Active Task (Hot): 현재 수행 중인 작업의 tasks.json. 가장 높은 우선순위로 컨텍스트에 상주한다.
- Recent Session Log (Warm): 최근 3~5개 세션의 progress.md. 구체적인 작업 내역과 트러블슈팅 기록.
- Project Snapshot: tree 명령어나 핵심 파일의 요약본. 전체 구조가 변경되었을 때만 갱신한다.
4.3 컨텍스트 로딩 전략: Lazy Loading
이 모든 문서를 한 번에 다 프롬프트에 넣을 수는 없다. 따라서 Lazy Loading 전략을 취해야 한다.
- 초기화 단계: Project Snapshot과 tasks.json만 로딩하여 현재 위치 파악.
- 작업 할당 단계: 작업이 "결제 시스템 수정"이라면, 에이전트(혹은 매니저)가 스스로 /docs/components/payment.md와 /docs/adr/*payment*.md를 찾아 읽도록 툴을 제공한다.
- 구현 단계: 필요한 소스 코드와 최근 progress.md를 로딩하여 작업 수행.
결국 핵심은 **'문서의 계층화'**다. 에이전트에게 소스 코드만 던져주는 것은 지도가 없는 탐험가를 정글에 밀어 넣는 것과 같다. ADR로 제약을 걸고, Component Spec으로 방향을 잡아주어야 에이전트는 길을 잃지 않는다.
5. 컨텍스트 부트스트래핑(Context Bootstrapping): 작업 재개를 위한 시퀀스
LLM은 매번 호출될 때마다 기억상실증에 걸린 상태로 깨어난다. 따라서 작업을 재개하기 전, 에이전트의 뇌를 프로젝트의 현재 상태와 동기화시키는 '부트스트래핑(Bootstrapping)' 과정이 필수적이다.
무작정 모든 파일을 Context Window에 쑤셔 넣는 것은 하수다. 우리는 가장 적은 토큰으로 가장 높은 해상도의 맥락을 복원해야 한다. 이를 위해 다음과 같은 **'4단계 로딩 파이프라인'**을 제안한다.
Step 1. 목표 조준 (Target Acquisition)
가장 먼저 로딩해야 할 것은 '무엇을 해야 하는가'이다.
- Action: tasks.json을 읽어 current_task_id와 해당 작업의 상세 명세(Description, Constraints, DoD)를 로딩한다.
- Reason: 목표가 설정되어야 이후 어떤 문서를 읽을지 결정할 수 있다.
Step 2. 지형 파악 (Terrain Analysis)
내가 어디에 서 있는지 파악한다. 전체 코드를 읽는 것이 아니라, 구조를 읽는다.
- Action:
- 프로젝트 루트의 파일 구조 트리 (tree 명령어 결과, .git 등 제외)
- docs/components/*.md (주요 컴포넌트 명세)
- Reason: 자신이 건드려야 할 파일이 어디에 위치해 있고, 어떤 컴포넌트에 속하는지 '지도'를 머릿속에 그리기 위함이다.
Step 3. 시점 동기화 (Time Synchronization)
과거와 현재를 연결한다.
- Action:
- Short-term: progress.md의 최근 1~2개 세션 기록 (바로 직전 엔지니어가 남긴 인수인계 노트).
- Long-term: git log --oneline -n 10 (최근 커밋 내역).
- Reason: 이전 작업자가 어디까지 진행했는지, 마지막으로 수정한 파일이 무엇인지 파악하여 작업의 연속성을 확보한다.
Step 4. 심층 로딩 (Deep Loading)
Step 1(목표)과 Step 2(지도)를 바탕으로, 실제 수술이 필요한 부위만 정밀 로딩한다.
- Action:
- 작업과 관련된 ADR(아키텍처 의사결정 문서) 읽기 (예: 인증 관련 작업이면 docs/adr/*auth*.md).
- 실제 수정해야 할 소스 코드 파일 읽기.
- 관련된 테스트 코드 읽기.
⚠️ 필수 절차: 선 로딩 후 출력 (Pre-load & Echo)
이 모든 정보를 로딩했다고 해서 바로 코드를 짜게 해서는 안 된다. 에이전트가 정보를 제대로 소화했는지 확인하는 'Echo' 절차가 반드시 필요하다.
에이전트는 코딩을 시작하기 전, 로딩된 정보를 바탕으로 다음과 같은 **'작업 계획서'**를 먼저 출력해야 한다.
[Agent Output Example]
- Current Task: JWT 토큰 만료 로직 구현 (from tasks.json)
- Context Loaded:
- progress.md: 이전 세션에서 Secret Key 설정 완료 확인.
- git log: feat: setup jwt env 커밋 확인.
- docs/adr/001-auth.md: Access Token 수명을 30분으로 제한한 결정 확인.
- Target Files: src/auth/utils.py, tests/test_auth.py
- Plan: utils.py에 만료 체크 함수를 추가하고, 테스트를 수행하겠습니다.
이 출력이 완료된 시점이 비로소 **'작업 재개(Resume)'**가 승인된 시점이다. 이 과정이 있어야만 에이전트의 환각을 사전에 차단하고, 논리적인 작업 수행이 가능하다.
6. 실행 제어: TDD를 통한 강제적 정합성 검증 (Execution Control via Mandatory TDD)
컨텍스트 로딩이 완료된 에이전트에게 "기능을 구현해"라고 지시하면 안 된다. 에이전트는 확률적으로 그럴듯한 코드를 뱉어낼 뿐, 그 코드가 논리적으로 결함이 없는지는 스스로 판단하지 못한다.
따라서 우리는 에이전트의 작업 프로세스에 **TDD(테스트 주도 개발)**를 강제해야 한다. 여기서 TDD는 '좋은 습관'이 아니라, 환각을 방지하고 작업 완료를 기계적으로 판단하기 위한 **'생존 도구'**다.
6.1 선(先) 테스트, 후(後) 구현의 철칙
에이전트가 코드를 먼저 작성하고 나중에 테스트를 작성하게 하면, 자신의 로직에 맞춰 테스트를 끼워 맞추는 '자기 충족적 예언(Self-Fulfilling Prophecy)' 오류를 범한다. 버그가 있는 코드에 통과하는 테스트를 만드는 것이다.
이를 방지하기 위해 작업 순서는 시스템 레벨에서 다음과 같이 강제되어야 한다.
- RED (Spec to Test):
- 에이전트는 로딩된 tasks.json의 요구사항을 바탕으로 반드시 실패하는 테스트 코드를 가장 먼저 작성해야 한다.
- 이 단계에서 에이전트는 구현 세부 사항이 아닌, '인터페이스'와 '기대 결과'에만 집중하게 된다.
- 시스템은 테스트 파일이 생성되었는지 확인하고, 테스트를 실행하여 '실패(Fail)'가 뜨는지 확인한다. (컴파일 에러가 아닌, Assertion Error여야 한다.)
- GREEN (Implementation):
- 오직 작성된 테스트를 통과시키기 위한 최소한의 프로덕션 코드를 작성한다.
- 이때 에이전트의 목표는 '완벽한 코드'가 아니라 '테스트를 통과하는 코드'로 좁혀진다. 이는 불필요한 오버 엔지니어링을 막아준다.
- REFACTOR (Optimization):
- 테스트가 통과(Green)된 상태에서만 리팩터링을 허용한다.
6.2 테스트 코드 = 살아있는 명세서 (Living Specs)
자연어로 된 문서는 모호하지만, 코드로 된 테스트는 명확하다. 에이전트에게 작업의 완료 조건은 "내가 보기에 잘 짠 것 같아요"가 아니라, **"테스트 러너(Test Runner)의 Exit Code가 0인가"**이다.
- 검증의 외주화: 코드의 정합성 판단을 에이전트의 두뇌(LLM)에 맡기지 말고, 파이썬/노드 인터프리터라는 **결정론적 기계(Deterministic Machine)**에 맡겨야 한다.
- 회귀 방지: TDD 사이클을 통해 작성된 테스트 케이스는 프로젝트에 누적된다. 이는 향후 다른 에이전트가 작업을 이어받을 때, 기존 기능을 망가뜨리지 않게 막아주는 안전망(Safety Net) 역할을 수행한다.
결국 코딩 에이전트에게 **"테스트 없는 코드는 커밋할 수 없다"**는 룰을 하드 코딩 수준으로 박아넣어야 한다. 테스트 통과 없이는 progress.md 갱신도, git commit도 불가능하게 차단할 때, 우리는 에이전트를 신뢰할 수 있다.
7. 엔트로피 관리자: 청소부 에이전트 (The Janitor Agent)
소프트웨어 개발에는 '엔트로피(무질서도) 증가의 법칙'이 작용한다. 특히 AI 에이전트는 이 엔트로피를 인간보다 훨씬 빠른 속도로 증가시킨다.
앞서 언급한 Coding Agent는 본질적으로 '근시안적(Myopic)'이다. 그들의 목표는 당장의 테스트 통과(Green)와 기능 구현이지, 우아한 아키텍처나 중복 제거가 아니다. 에이전트에게 맡겨두면 코드는 필연적으로 스파게티가 되고, 변수명은 제각각이 되며, utils.py는 온갖 잡동사니의 무덤이 된다.
따라서 우리는 기능 구현과 유지보수의 역할을 철저히 분리해야 한다. 밤이 되면 어지러진 공사장을 정리하는 **'청소부 에이전트(Janitor Agent)'**가 반드시 필요하다.
7.1 역할: 코드베이스의 위생(Hygiene) 관리
Janitor Agent는 새로운 기능을 만들지 않는다. 오직 기존 코드를 닦고, 조이고, 기름칠한다.
- 구조적 리팩터링 & 중복 제거 (DRY):
- Coding Agent는 비슷한 로직을 발견하면 재사용하기보다 카피-페이스트 하는 경향이 있다.
- Janitor Agent는 주기적으로 전체 코드베이스를 스캔하여 중복된 로직을 공통 함수로 추출하고, 비대한 클래스를 분리한다.
- 문서 현행화 (Documentation Synchronization):
- 코드는 변했는데 문서는 그대로인 경우가 허다하다. 이는 나중에 투입될 Coding Agent에게 치명적인 환각의 원인이 된다.
- Janitor Agent는 변경된 코드와 기존의 docs/components/*.md, ADR 문서를 대조하여, 업데이트되지 않은 문서를 최신 상태로 동기화(Sync)한다.
- 일관성 강제 (Consistency Enforcement):
- 변수 명명 규칙(Snake_case vs CamelCase), 디렉토리 구조, 에러 처리 패턴 등이 프로젝트 표준을 따르고 있는지 감시하고 교정한다.
결론: 하네스 없이는 자율도 없다
에이전트가 코딩을 못하는 것이 아니다. 우리가 에이전트에게 맥락을 제공하는 방식이 틀렸다.
이전 세션의 맥락을 다음 세션으로 손실 없이, 그러나 효율적으로 넘기는 파이프라인을 구축해야 한다. 무작정 에이전트를 돌리는 것이 아니라, Git Log, 구조화된 JSON, 계층적 문서를 통해 단단한 **하네스(Harness)**를 채워줄 때, 비로소 AI 에이전트는 장기적인 프로젝트를 완주할 수 있다.
핵심은 **'기억'**을 에이전트의 두뇌(Context Window)가 아닌, **'시스템(System & Docs)'**에 맡기는 것이다.
'AI tools' 카테고리의 다른 글
| AI Agent 검증의 논리와 기법 (0) | 2025.11.29 |
|---|---|
| Code execution with MCP: 더 효율적인 에이전트 구축을 위한 패러다임의 전환 (0) | 2025.11.29 |
| Cursor 1.0 기념 다시 정리하는 효율적 사용법 (0) | 2025.06.08 |
| Cursor 사용 가이드 (0) | 2024.09.05 |
| Github Copilot 사용 가이드 (0) | 2024.06.13 |