
AI 코딩 에이전트를 며칠 이상 써보면 이상한 순간이 온다.
어제 분명히 같은 문제를 고쳤다.
같은 파일을 읽었다.
같은 테스트가 실패했다.
왜 그 접근을 버렸는지도 정리했다.
그런데 새 세션을 열면 다시 처음부터 시작한다.
이 파일 구조를 먼저 확인하겠습니다.
관련 코드를 탐색하겠습니다.
가능한 원인을 분석하겠습니다.
개발자 입장에서는 답답하다.
이미 확인한 내용인데 또 읽는다.
이미 실패한 접근인데 다시 시도한다.
어제 남긴 판단과 오늘의 판단이 충돌한다.
그래서 많은 팀이 대화 기록을 길게 보존하려고 한다.
하지만 대화 기록을 전부 들고 가는 방식은 오래 못 간다.
세션 로그
도구 호출
전체 파일 내용
빌드 로그
실패 로그
리뷰 코멘트
수정 설명
이 모든 것을 다음 세션에 넘기면 컨텍스트는 금방 더러워진다.
AI 에이전트에게 필요한 것은 긴 기억이 아니다.
다시 시작할 때 필요한 기억만 남기는 구조다.
이 글에서는 그 구조를 Durable Memory와 Run Ledger로 나눠 설명한다.
Durable Memory
→ 다음 작업에서도 재사용할 지식
Run Ledger
→ 현재 작업을 이어가기 위한 실행 기록
둘을 구분하면 AI 에이전트가 같은 실수를 반복하는 문제를 크게 줄일 수 있다.
먼저 가장 많이 하는 오해부터 정리하자.
대화가 길면 AI가 많이 기억한다.
어느 정도는 맞다.
하지만 실무에서는 대화 기록과 작업 메모리를 구분해야 한다.
대화 기록에는 필요 없는 내용이 너무 많다.
초기 추측
폐기된 아이디어
중복 설명
전체 빌드 로그
오래된 테스트 결과
이미 해결된 오류
예를 들어 한 버그를 고치는 동안 다음 일이 있었다고 하자.
1차 접근
→ Repository에 상태 추가
→ 아키텍처 위반으로 폐기
2차 접근
→ ViewModel guard 추가
→ 테스트 실패
3차 접근
→ 실패 상태 복구 추가
→ 테스트 통과
다음 세션에 필요한 것은 전체 대화가 아니다.
현재 채택된 접근
왜 1차 접근을 버렸는지
현재 변경 파일
마지막 테스트 결과
남은 위험
이 다섯 가지만 있으면 된다.
대화는 과정이고, 메모리는 결과다.
둘은 목적이 다르다.
여러 작업에서 재사용할 수 있는 장기 지식이다.
이 프로젝트의 아키텍처 규칙
이 팀이 금지하는 패턴
특정 모듈의 주의사항
반복적으로 발생한 장애 원인
검증된 해결 방식
예를 들면 이런 내용이다.
# memory/ios-concurrency.md
- UI 상태는 MainActor에서 변경한다.
- ViewModel이 Repository를 직접 생성하지 않는다.
- 비동기 제출 함수는 중복 진입을 guard로 막는다.
- 실패 후 재시도 가능 상태를 명시적으로 복구한다.
- 테스트를 실행하지 않았다면 통과했다고 보고하지 않는다.
이 정보는 다음 버그에서도 쓸 수 있다.
현재 작업 하나를 이어가기 위한 실행 기록이다.
지금 어느 단계인지
누가 무엇을 했는지
어떤 파일이 바뀌었는지
무엇이 실패했는지
다음 담당자가 무엇을 해야 하는지
예를 들면 이런 구조다.
{
"run_id": "run-20260713-001",
"task": "checkout-duplicate-submit",
"phase": "review",
"current_agent": "reviewer",
"attempt": 2,
"last_successful_step": "implementation",
"next_step": "review_diff",
"changed_files": [
"CheckoutViewModel.swift",
"CheckoutViewModelTests.swift"
],
"last_test": {
"status": "passed",
"command": "xcodebuild test ..."
},
"remaining_risks": [
"서버 idempotency는 별도 작업"
]
}
Run Ledger는 프로젝트 전체 지식이 아니다.
현재 작업의 인수인계 문서다.
실무에서는 다음 정도로 시작할 수 있다.
.ai/
├── memory/
│ ├── architecture.md
│ ├── coding-lessons.md
│ ├── failure-patterns.md
│ └── module-notes/
│ ├── payment.md
│ └── auth.md
│
├── runs/
│ └── checkout-duplicate-submit/
│ ├── ledger.json
│ ├── handoff.md
│ ├── latest-failure.json
│ ├── decisions.md
│ └── artifacts/
│ ├── plan.json
│ ├── diff.patch
│ ├── review.json
│ └── test-result.json
│
└── policies/
├── memory-policy.yaml
└── compaction-policy.yaml
폴더 역할은 단순하다.
memory/
→ 여러 작업에서 다시 쓸 지식
runs/
→ 현재 작업의 실행 기록
artifacts/
→ 계획, diff, 리뷰, 테스트 증거
policies/
→ 무엇을 기억하고 무엇을 버릴지 규칙
AI에게 “중요한 건 기억해”라고 하면 기준이 흔들린다.
정책으로 정한다.
version: 1
durable_memory:
allow:
- architecture_rule
- security_rule
- verified_failure_pattern
- verified_fix_pattern
- module_constraint
- team_workflow_rule
deny:
- raw_chat_history
- unverified_guess
- full_build_log
- temporary_file_path
- personal_opinion
- duplicated_note
require_evidence:
- verified_failure_pattern
- verified_fix_pattern
evidence_sources:
- passing_test
- accepted_review
- merged_diff
- official_project_document
retention:
architecture_rule: permanent
module_constraint: permanent
verified_fix_pattern: 180d
temporary_workaround: 14d
핵심은 require_evidence다.
AI가 한 번 추측한 내용을 장기 기억으로 저장하면 안 된다.
추측
→ 세션 메모
테스트와 리뷰로 검증
→ Durable Memory 후보
AI 메모리에서 가장 위험한 것은 틀린 기억이다.
예를 들어 AI가 이렇게 적었다고 하자.
PaymentRepository는 UI 상태를 관리한다.
실제로는 잘못된 아키텍처일 수 있다.
이 내용이 Durable Memory에 들어가면 다음 작업에서도 반복된다.
그래서 메모리는 상태를 가져야 한다.
id: memory-20260713-001
type: verified_fix_pattern
status: candidate
summary: "중복 제출은 ViewModel의 isSubmitting guard로 차단한다."
evidence:
tests:
- CheckoutViewModelTests.testDuplicateSubmit
review:
- review.json#issue-3
created_at: 2026-07-13T10:00:00+09:00
approved_by: null
검증이 끝나면 상태를 바꾼다.
status: approved
approved_by: human
approved_at: 2026-07-13T10:35:00+09:00
Durable Memory는 자동 저장소가 아니라 승인된 지식 저장소에 가깝다.
Run Ledger는 일기처럼 쓰면 안 된다.
사건 중심으로 기록한다.
{
"events": [
{
"seq": 1,
"type": "task_created",
"at": "2026-07-13T09:00:00+09:00",
"actor": "human",
"artifact": "task.md"
},
{
"seq": 2,
"type": "plan_approved",
"at": "2026-07-13T09:08:00+09:00",
"actor": "human",
"artifact": "plan.json"
},
{
"seq": 3,
"type": "implementation_completed",
"at": "2026-07-13T09:22:00+09:00",
"actor": "codex",
"artifact": "implementation.json"
},
{
"seq": 4,
"type": "test_failed",
"at": "2026-07-13T09:28:00+09:00",
"actor": "test-runner",
"artifact": "latest-failure.json"
}
]
}
이 방식은 Event Sourcing과 비슷하다.
현재 상태만 저장하는 대신 어떤 일이 있었는지 순서대로 남긴다.
장점이 있다.
누가 상태를 바꿨는지 알 수 있다.
중간 단계로 되돌아갈 수 있다.
실패 시 마지막 성공 지점부터 재개할 수 있다.
다음 에이전트가 전체 대화를 읽지 않아도 된다.
import json
from dataclasses import asdict, dataclass
from datetime import datetime
from pathlib import Path
from typing import Any
@dataclass(frozen=True)
class LedgerEvent:
seq: int
type: str
at: str
actor: str
payload: dict[str, Any]
class RunLedger:
def __init__(self, path: Path) -> None:
self.path = path
def load(self) -> dict[str, Any]:
if not self.path.exists():
return {
"version": 1,
"events": [],
}
return json.loads(
self.path.read_text(encoding="utf-8")
)
def append(
self,
*,
event_type: str,
actor: str,
payload: dict[str, Any],
) -> LedgerEvent:
ledger = self.load()
events = ledger["events"]
event = LedgerEvent(
seq=len(events) + 1,
type=event_type,
at=datetime.now().astimezone().isoformat(),
actor=actor,
payload=payload,
)
events.append(asdict(event))
self.path.write_text(
json.dumps(
ledger,
ensure_ascii=False,
indent=2,
),
encoding="utf-8",
)
return event
사용은 단순하다.
ledger.append(
event_type="implementation_completed",
actor="codex",
payload={
"artifact": "implementation.json",
"changed_files": [
"CheckoutViewModel.swift",
"CheckoutViewModelTests.swift",
],
},
)
사건 기록만 있으면 현재 상태를 다시 만들 수 있다.
from dataclasses import dataclass
@dataclass
class RunSnapshot:
phase: str = "created"
attempt: int = 0
last_artifact: str | None = None
last_failure: str | None = None
waiting_human: bool = False
def reduce_ledger(
ledger: dict,
) -> RunSnapshot:
snapshot = RunSnapshot()
for event in ledger["events"]:
event_type = event["type"]
payload = event["payload"]
if event_type == "plan_approved":
snapshot.phase = "implementing"
snapshot.last_artifact = payload.get("artifact")
elif event_type == "implementation_completed":
snapshot.phase = "reviewing"
snapshot.attempt += 1
snapshot.last_artifact = payload.get("artifact")
elif event_type == "review_passed":
snapshot.phase = "testing"
snapshot.last_artifact = payload.get("artifact")
elif event_type == "test_failed":
snapshot.phase = "failed_test"
snapshot.last_failure = payload.get("artifact")
elif event_type == "waiting_human":
snapshot.phase = "waiting_human"
snapshot.waiting_human = True
elif event_type == "completed":
snapshot.phase = "completed"
return snapshot
프로세스가 종료돼도 ledger를 읽으면 어디서 재개할지 알 수 있다.
Run Ledger는 기계가 읽기 좋다.
다음 에이전트에게는 더 짧은 인수인계 문서가 필요하다.
# Handoff
## Current Goal
결제 버튼 중복 탭으로 주문 생성 요청이 여러 번 실행되는 문제를 막는다.
## Current Phase
Fixing
## Last Completed Step
Claude Reviewer가 첫 번째 구현을 검토했다.
## Current Findings
- 중복 진입 guard는 정상이다.
- 실패 후 isSubmitting이 false로 복구되지 않는다.
- PaymentAPI 수정은 현재 범위 밖이다.
## Files Allowed to Edit
- CheckoutViewModel.swift
- CheckoutViewModelTests.swift
## Latest Failure
CheckoutViewModelTests.testSubmitOrder_whenFailed_allowsRetry
## Next Action
failure-context.json의 실패 한 건만 수정한다.
## Do Not Repeat
- PaymentRepository에 UI 상태를 추가하지 않는다.
- 전체 결제 구조를 리팩토링하지 않는다.
여기서 Do Not Repeat가 중요하다.
이미 실패한 접근을 다시 시도하지 않게 만든다.
from pathlib import Path
def build_handoff(
*,
goal: str,
phase: str,
findings: list[str],
allowed_files: list[str],
latest_failure: str | None,
next_action: str,
rejected_approaches: list[str],
output: Path,
) -> None:
lines = [
"# Handoff",
"",
"## Current Goal",
"",
goal,
"",
"## Current Phase",
"",
phase,
"",
"## Current Findings",
"",
]
lines.extend(f"- {item}" for item in findings)
lines.extend([
"",
"## Files Allowed to Edit",
"",
])
lines.extend(f"- {path}" for path in allowed_files)
lines.extend([
"",
"## Latest Failure",
"",
latest_failure or "None",
"",
"## Next Action",
"",
next_action,
"",
"## Do Not Repeat",
"",
])
lines.extend(
f"- {item}" for item in rejected_approaches
)
output.write_text(
"\n".join(lines),
encoding="utf-8",
)
새 세션은 전체 대화 대신 handoff.md와 최신 artifact만 읽는다.
메모리 파일이 많아지면 또 다른 문제가 생긴다.
AI가 모든 메모리를 읽기 시작한다.
architecture.md
coding-lessons.md
failure-patterns.md
payment.md
auth.md
analytics.md
network.md
작은 작업에 전부 넣으면 다시 Context Budget이 깨진다.
그래서 Memory Router가 필요하다.
routes:
payment:
include:
- memory/architecture.md
- memory/coding-lessons.md
- memory/module-notes/payment.md
auth:
include:
- memory/architecture.md
- memory/security-rules.md
- memory/module-notes/auth.md
ui:
include:
- memory/architecture.md
- memory/ui-patterns.md
현재 작업과 관련된 메모리만 읽는다.
from pathlib import Path
import yaml
class MemoryRouter:
def __init__(
self,
*,
root: Path,
policy_file: Path,
) -> None:
self.root = root
self.policy = yaml.safe_load(
policy_file.read_text(encoding="utf-8")
)
def files_for(
self,
domain: str,
) -> tuple[Path, ...]:
route = self.policy["routes"].get(domain)
if route is None:
return tuple()
return tuple(
self.root / relative
for relative in route.get("include", [])
)
에이전트 작업이 길어지면 로그와 산출물이 계속 쌓인다.
attempt-1.log
attempt-2.log
attempt-3.log
review-1.json
review-2.json
diff-1.patch
diff-2.patch
모두 지우면 감사 기록이 사라진다.
모두 컨텍스트에 넣으면 토큰이 낭비된다.
그래서 저장과 주입을 분리한다.
디스크에는 원본 보관
컨텍스트에는 요약본만 주입
compaction-policy.yaml 예시는 다음과 같다.
version: 1
inject:
- current_handoff
- current_plan
- current_diff_stat
- latest_failure
- approved_memory
summarize:
- previous_attempts
- previous_reviews
- long_tool_outputs
archive_only:
- full_build_logs
- previous_diffs
- raw_agent_transcripts
limits:
max_injected_files: 10
max_log_tail_characters: 6000
max_previous_attempts: 3
작업이 끝났다고 모든 내용을 기억할 필요는 없다.
종료 시점에 후보만 추출한다.
{
"memory_candidates": [
{
"type": "verified_fix_pattern",
"summary": "비동기 제출 함수는 ViewModel에서 중복 진입을 막고 실패 후 상태를 복구한다.",
"evidence": [
"CheckoutViewModelTests.testDuplicateSubmit",
"CheckoutViewModelTests.testRetryAfterFailure"
]
},
{
"type": "module_constraint",
"summary": "PaymentAPI 변경은 payment-owner 승인이 필요하다.",
"evidence": [
"protected-files.yaml"
]
}
]
}
후보는 자동 승인하지 않는다.
Eval Gate처럼 메모리에도 승격 단계가 필요하다.
Candidate
→ Review
→ Approved
→ Durable Memory
승격 조건을 정한다.
promotion:
require:
- evidence
- no_conflict_with_existing_memory
- accepted_review
- human_approval_for_architecture_rules
reject_if:
- unverified_guess
- one_off_workaround
- user_specific_secret
- duplicated_memory
- stale_rule
특히 아키텍처 규칙은 사람 승인을 받는 편이 좋다.
모델과 프로젝트는 계속 바뀐다.
예전에 맞던 규칙이 지금은 틀릴 수 있다.
이 프로젝트는 UIKit만 사용한다.
몇 달 뒤 SwiftUI가 도입되면 오래된 기억이 된다.
따라서 메모리에 날짜와 유효 범위를 둔다.
id: memory-architecture-014
status: approved
summary: "Checkout 화면 상태는 ViewModel이 소유한다."
scope:
- "Sources/Checkout/**"
created_at: 2026-07-13
review_after: 2026-10-13
source:
- architecture.md
review_after가 지나면 다시 검토한다.
기존 메모리와 새 후보가 충돌할 수 있다.
기존:
Repository가 재시도 정책을 관리한다.
신규:
UseCase가 재시도 정책을 관리한다.
AI가 둘 중 하나를 임의로 선택하게 하지 않는다.
{
"conflict": true,
"existing_memory": "memory-021",
"candidate_memory": "candidate-104",
"resolution": "waiting_human",
"reason": "아키텍처 책임 변경에 해당합니다."
}
메모리 충돌은 단순 최신값 우선으로 처리하면 위험하다.
처음부터 복잡하게 만들 필요는 없다.
최소 버전은 이 정도면 된다.
.ai/
├── memory/
│ ├── architecture.md
│ └── lessons.md
└── runs/
└── current/
├── ledger.json
├── handoff.md
├── latest-failure.json
└── artifacts/
운영 순서는 다음과 같다.
1. 작업 시작 시 ledger 생성
2. 주요 단계마다 event 기록
3. 실패 시 latest-failure.json 저장
4. 세션 종료 전 handoff.md 생성
5. 새 세션은 handoff와 최신 artifact만 읽기
6. 작업 완료 후 memory candidate 추출
7. 사람이 승인한 것만 memory에 반영
Task
↓
Load Approved Memory
↓
Load Current Handoff
↓
Run Agent
↓
Append Ledger Event
↓
Save Artifacts
↓
Compact Context
↓
Generate Next Handoff
↓
Continue or Pause
작업이 끝나면 다음 흐름이 추가된다.
Completed Run
↓
Extract Memory Candidates
↓
Memory Promotion Gate
↓
Approved Durable Memory
마지막 성공 단계와 다음 작업이 남아 있다.
Claude가 하던 작업을 Codex가 인수할 수 있다.
폐기된 접근을 Do Not Repeat에 남긴다.
전체 대화가 아니라 사건과 산출물을 본다.
여러 실행의 Ledger를 분석해 반복 장애를 찾을 수 있다.
저장과 컨텍스트 주입은 다르다.
후보와 승인 상태를 분리해야 한다.
Temporary workaround에는 만료일이 필요하다.
작업 도메인별 Memory Router가 필요하다.
검토 날짜와 scope가 필요하다.
로그는 artifact이고, 검증된 교훈만 memory다.
AI 에이전트가 짧은 작업만 한다면 메모리 시스템은 필요 없다.
하지만 작업 시간이 길어질수록 문제가 달라진다.
여러 세션
여러 모델
여러 subagent
여러 번의 실패
사람 승인 대기
다음 날 재개
이때 중요한 것은 모델의 지능이 아니다.
작업을 잃지 않는 구조다.
앞으로 개발자는 다음을 설계하게 된다.
무엇을 장기 기억으로 남길지
무엇을 현재 실행 기록으로만 보관할지
어떤 기억은 사람 승인이 필요한지
세션이 끊기면 어디서 재개할지
오래된 기억을 언제 폐기할지
다른 모델이 작업을 어떻게 이어받을지
이건 단순한 프롬프트 기술이 아니다.
데이터 모델링과 이벤트 설계에 가깝다.
Memory
→ 승인된 지식
Ledger
→ 실행 이력
Artifact
→ 증거
Handoff
→ 인수인계
Checkpoint
→ 재개 지점
AI 에이전트에게 필요한 것은 모든 것을 기억하는 능력이 아니다.
필요한 것만 남기고, 다음 작업자가 바로 이어갈 수 있게 만드는 능력이다.
대화 기록을 길게 유지하는 것
≠
좋은 메모리
좋은 메모리는 다음 조건을 만족한다.
검증된 내용만 남는다.
작업별 기록과 장기 지식이 분리된다.
오래된 정보는 다시 검토된다.
새 세션은 전체 대화가 아니라 handoff를 읽는다.
작업 상태는 ledger로 복구할 수 있다.
한 줄로 정리하면 이렇다.
AI 에이전트의 진짜 기억력은
얼마나 많이 저장하느냐가 아니라,
다음 실행에 필요한 상태와 교훈을
얼마나 정확하게 남기느냐에서 결정된다.
모델은 세션을 잊어도 된다.
Runtime이 기억하면 된다.