오늘도 공부
AI 에이전트는 기억하지 못한다: 시스템으로 지식을 저장하는 법 본문
CodeDeck - 개발자를 위한 코드 학습 카드 뉴스
프로그래밍 언어와 프레임워크를 카드 뉴스 형태로 쉽게 배우는 개발자 학습 플랫폼
www.codedeck.kr
문제의 발견
AI 에이전트와 코드를 작성하다 보면 같은 벽에 계속 부딪히게 됩니다. 지난 세션에서 분명히 고쳤던 실수를 다음 세션에서 또 반복합니다. 명명 규칙을 알려줘도 잊어버리고, 제약사항을 설명해도 다시 물어봅니다.
왜 그럴까요? AI 에이전트는 상태를 저장하지 않는(stateless) 시스템이기 때문입니다.
사람 개발자 vs AI 에이전트
사람 개발자와 일할 때:
- 한 번 설명하면 → 기억합니다
- 실수를 하면 → 배웁니다
- 투자한 시간이 → 축적됩니다
AI 에이전트와 일할 때:
- 한 번 설명하면 → 세션 종료 후 잊어버립니다
- 실수를 고쳐주면 → 다음에 또 반복합니다
- 투자한 시간이 → 증발합니다
이 차이를 이해하면 협업 방식을 완전히 바꿔야 한다는 걸 알게 됩니다.
패러다임 전환: 에이전트가 아닌 시스템에 투자하라
에이전트를 가르치려 하지 마세요. 대신 시스템이 원하는 것을 강제하도록 만드세요.
Claude Code는 이를 위한 세 가지 도구를 제공합니다:
도구 개요
필요한 것 사용할 도구 이유
| 매번 같은 작업 | Hook | 자동 실행 |
| 복잡한 워크플로우 | Skill | 다단계 절차 |
| 외부 데이터 접근 | MCP | 런타임 발견 |
예시:
- Git 커밋 메시지 템플릿 → Hook (커밋할 때마다 자동 적용)
- 블로그 포스트 발행 → Skill (읽기 → 패턴 분석 → 각색 → 발행)
- Google Drive 파일 검색 → MCP (실시간 데이터 조회)
Hook vs Skill vs MCP
동작 방식:
Hook: 외부 트리거
└─ 시스템이 언제 실행할지 결정
Skill: 내부 트리거
└─ 에이전트가 언제 사용할지 결정
MCP: 데이터 요청
└─ 에이전트가 필요할 때 조회
4가지 핵심 원칙
1. 명시적 인터페이스 (암묵적 규칙 금지)
❌ 문제: 규칙은 잊혀진다
// 매 세션마다...
개발자: "포트는 src/ports/에 만들고 명명 규칙 지켜주세요"
AI: "알겠습니다!"
// 다음 세션
AI: "포트를 어디에 만들어야 하나요?"
✅ 해결: 틀릴 수 없게 만든다
// 잘못된 방식: 암묵적 규칙
// "포트는 src/ports/에 만들고 {서비스명}/adapter.ts 형식으로 네이밍"
// 올바른 방식: 명시적 강제
export const PORT_CONFIG = {
directory: 'src/ports/',
pattern: '{serviceName}/adapter.ts',
requiredExports: ['handler', 'schema']
} as const;
// 런타임에 검증
function validatePortStructure(config: typeof PORT_CONFIG) {
const files = fs.readdirSync(config.directory);
files.forEach(file => {
if (!file.match(/^[a-z_]+\/adapter\.ts$/)) {
throw new Error(`잘못된 포트 구조: ${file}`);
}
});
}
MCP 활용:
// ❌ 에이전트가 하드코딩 (틀리거나 잊음)
const WHISPER_PORT = 8770;
// ✅ MCP 서버가 제공 (런타임에 조회)
const services = await fetch('http://localhost:8772/api/services')
.then(r => r.json());
// 반환: { whisper: { endpoint: '/transcribe', port: 8772 } }
2. 컨텍스트 임베딩 (외부 문서 금지)
❌ 문제: README는 읽히지 않는다
README.md: "항상 TypeScript strict 모드를 사용하세요"
AI: [읽지 않거나 잊어버림]
✅ 해결: 이유를 코드에 직접 작성
/**
* 왜 STRICT MODE를 사용하는가:
* - 런타임 에러가 컴파일 타임 에러로 전환됨
* - 운영 중 디버깅 비용 → 0
* - 절대 비활성화 금지: 타입 안정성 보장이 깨짐
*
* 초기 비용: +500줄의 타입 정의 작성
* 운영 비용: 컴파일러가 잡아주는 버그 = 0
*/
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true
}
}
Hook 활용:
#!/bin/bash
# .claude/hooks/UserPromptSubmit.sh
# 사용자 프롬프트 전에 자동 실행
cat <<EOF
## 프로젝트 컨텍스트
- 아키텍처: Clean Architecture (Ports & Adapters)
- 상태 관리: Riverpod 2.5.1 사용
- 데이터베이스: Drift (SQLite)
- 네이밍: snake_case (파일), camelCase (변수)
**중요:** 모든 새 기능은 src/features/ 아래 도메인별로 구조화
EOF
에이전트는 매번 프롬프트를 받을 때마다 이 컨텍스트를 자동으로 받습니다.
3. 자동화된 제약 (신뢰하지 말고 검증하라)
❌ 문제: 에이전트를 믿으면 안 된다
// 에이전트에게 말로 설명
개발자: "절대 프로덕션 DB를 직접 수정하지 마세요"
AI: "알겠습니다!"
// 나중에...
AI: [프로덕션 DB 직접 수정]
✅ 해결: 구조적으로 불가능하게 만든다
// 환경 검증
function validateEnvironment() {
if (process.env.NODE_ENV === 'production') {
if (!process.env.DB_READ_ONLY) {
throw new Error(
'프로덕션 환경에서는 읽기 전용 DB 연결만 허용됩니다.\n' +
'DB_READ_ONLY=true 환경 변수를 설정하세요.'
);
}
}
}
// 앱 시작 시 검증
validateEnvironment();
Hook으로 위험한 명령 차단:
#!/bin/bash
# .claude/hooks/PermissionRequest.sh
# 도구 사용 요청 시 자동 실행
COMMAND=$1
# 위험한 명령 패턴 차단
if echo "$COMMAND" | grep -q "rm -rf /"; then
echo '{"permissionDecision": "deny", "reason": "위험한 명령이 차단되었습니다"}'
exit 0
fi
if echo "$COMMAND" | grep -q "DROP DATABASE"; then
echo '{"permissionDecision": "deny", "reason": "데이터베이스 삭제 차단"}'
exit 0
fi
echo '{"permissionDecision": "allow"}'
4. 반복 프로토콜 (에러 → 시스템 패치)
❌ 문제: 깨진 루프
에이전트 실수 → 수정 → 세션 종료 → 같은 실수 반복
✅ 해결: 고정된 루프
에이전트 실수 → 시스템 패치 → 같은 실수 구조적으로 불가능
예시:
// ❌ 임시 방편 (에이전트에게 말로 설명)
// "포트 이름은 snake_case로 작성하세요"
// ✅ 영구 해결 (시스템 업데이트)
function validatePortName(name: string) {
const VALID_PATTERN = /^[a-z][a-z0-9_]*$/;
if (!VALID_PATTERN.test(name)) {
throw new Error(
`포트 이름은 snake_case 형식이어야 합니다: "${name}"\n\n` +
`올바른 예시: whisper_port, user_service, payment_api\n` +
`잘못된 예시: whisperPort, Whisper-Port, whisper-port\n\n` +
`규칙: 소문자로 시작, 소문자/숫자/언더스코어만 사용`
);
}
}
// 포트 생성 시 자동 검증
export function createPort(name: string, config: PortConfig) {
validatePortName(name); // 여기서 검증 실패 시 생성 불가
// ... 포트 생성 로직
}
Skill로 워크플로우 재사용:
---
name: flutter-feature-setup
description: Flutter 새 기능 모듈 초기화 (Clean Architecture)
---
# Flutter 기능 모듈 생성
## 단계별 프로세스
1. **디렉토리 구조 생성**
lib/features/{feature_name}/ ├── data/ │ ├── models/ │ ├── repositories/ │ └── data_sources/ ├── domain/ │ ├── entities/ │ ├── repositories/ │ └── usecases/ └── presentation/ ├── providers/ ├── screens/ └── widgets/
2. **엔티티 생성** (domain/entities/)
- 순수 Dart 클래스
- 비즈니스 로직만 포함
- 외부 의존성 없음
3. **리포지토리 인터페이스** (domain/repositories/)
- abstract class로 정의
- Future 기반 비동기 메서드
4. **유스케이스 생성** (domain/usecases/)
- 단일 책임 원칙
- call() 메서드 구현
5. **Provider 설정** (presentation/providers/)
- Riverpod StateNotifier 사용
- AsyncValue로 상태 관리
## 검증 포인트
- [ ] 모든 파일명이 snake_case인가?
- [ ] 순환 의존성이 없는가?
- [ ] domain 레이어에 Flutter 의존성이 없는가?
다음 세션에서 "Flutter 기능 추가"라고 하면 에이전트가 자동으로 이 Skill을 사용합니다.
실전 예시: AI 친화적 아키텍처
// 자가 검증, 자가 문서화, 자가 발견
import { z } from 'zod';
export const PORTS = {
whisper: {
endpoint: '/transcribe',
method: 'POST' as const,
input: z.object({
audio: z.string().describe('Base64 인코딩된 오디오 데이터'),
language: z.enum(['ko', 'en']).optional()
}),
output: z.object({
text: z.string(),
duration: z.number(),
confidence: z.number().min(0).max(1)
})
},
translation: {
endpoint: '/translate',
method: 'POST' as const,
input: z.object({
text: z.string(),
sourceLang: z.string(),
targetLang: z.string()
}),
output: z.object({
translatedText: z.string()
})
}
} as const;
// 에이전트가 포트를 호출할 때:
// ✓ 엔드포인트가 열거되어 있음 (오타 불가) [MCP]
// ✓ 스키마가 자동 검증 (잘못된 데이터 전송 불가) [제약]
// ✓ 타입이 자동완성됨 (IDE가 가이드) [인터페이스]
// ✓ 메서드가 제한됨 (잘못된 HTTP 동사 사용 불가) [검증]
암묵적 방식과 비교
// ❌ 에이전트가 기억하거나 추측해야 함
// "Whisper는 8770 포트에서 실행됨"
// "POST로 /transcribe 호출"
// "오디오는 base64 문자열로 전송"
// 에이전트가 할 실수들:
// - 잘못된 포트 하드코딩
// - 엔드포인트 오타
// - 잘못된 데이터 형식 전송
// - 필수 필드 누락
도구 선택 가이드
상황 도구 이유 예시
| 매번 같은 작업 | Hook | 자동, 빠름 | 커밋 시 Git 상태 표시 |
| 다단계 워크플로우 | Skill | 에이전트가 판단, 유연함 | 블로그 포스트 발행 |
| 외부 데이터 | MCP | 런타임 발견 | Drive/Slack/GitHub 조회 |
도구별 상세 설명
Hooks: 자동 동작
# .claude/hooks/commit.sh
# "commit" 키워드 감지 시 자동 실행
echo "📊 현재 Git 상태:"
git status --short
echo ""
echo "📝 최근 커밋 메시지:"
git log -1 --pretty=format:"%s"
echo ""
echo "💡 커밋 메시지 가이드:"
echo "- feat: 새 기능"
echo "- fix: 버그 수정"
echo "- docs: 문서 변경"
echo "- refactor: 리팩토링"
Skills: 복잡한 워크플로우
---
name: api-test-setup
description: API 엔드포인트 테스트 자동 생성
---
# API 테스트 자동 생성 워크플로우
1. **API 스펙 파싱**
- OpenAPI/Swagger 문서 읽기
- 엔드포인트 목록 추출
2. **테스트 케이스 생성**
- 각 엔드포인트당:
* 정상 케이스 (200)
* 실패 케이스 (400, 404, 500)
* 엣지 케이스
3. **Mock 데이터 생성**
- 스키마 기반 자동 생성
- 유효한 데이터 + 무효한 데이터
4. **테스트 파일 작성**
```typescript
describe('POST /api/users', () => {
it('should create user with valid data', async () => {
// ...
});
it('should reject invalid email', async () => {
// ...
});
});
- 검증
- 모든 엔드포인트 커버리지 확인
- 테스트 실행 및 통과 확인
#### MCP: 데이터 연결
```json
// claude_desktop_config.json
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed/files"]
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "your-token"
}
},
"google-drive": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-gdrive"]
}
}
}
도구 조합 예시
사용자: "이 블로그 포스트를 발행해줘"
→ Hook: Git 상태 자동 추가 (자동)
→ Skill: 발행 워크플로우 로드 (에이전트가 작업 감지)
→ 에이전트: 단계 수행, 필요시 MCP 사용 (외부 데이터)
→ Hook: 최종 출력 검증 (자동)
핵심 원칙 정리
1. 건망증을 위한 설계
- 매 세션은 제로 상태에서 시작
- 대화가 아닌 아티팩트에 컨텍스트 임베드
- 신뢰하지 말고 검증하라
2. 시스템에 투자
- 에이전트를 가르치지 말고 시스템을 변경하라
- 암묵적 규칙을 명시적 강제로 교체
- 자가 문서화 코드 > 외부 문서
3. 인터페이스 = 단일 진실 공급원
에이전트가 배우는 것:
- 타입 + 스키마 + 런타임 인트로스펙션 (MCP)
에이전트가 깰 수 없는 것:
- 검증 + 제약 + Fail-fast (Hooks)
에이전트가 재사용하는 것:
- 세션 간 지속되는 워크플로우 (Skills)
4. 에러 = 시스템 갭
에이전트 에러 → 시스템이 너무 허용적
수정 방법: 에이전트를 고치지 말고 시스템을 패치
목표: 실수를 구조적으로 불가능하게 만들기
멘탈 모델의 전환
❌ 기존 방식
AI 에이전트 = 교육이 필요한 주니어 개발자
✅ 새로운 방식
AI 에이전트 = 가드레일이 필요한 무상태 워커
핵심: 에이전트는 학습하지 않습니다. 시스템이 학습합니다.
모든 수정은 에이전트를 교육하는 것이 아니라 시스템을 강화해야 합니다. 시간이 지나면 잘못 사용하는 것이 구조적으로 불가능한 아키텍처를 구축하게 됩니다.
실전 적용 예시: Flutter 프로젝트
프로젝트 구조
my_app/
├── .claude/
│ ├── hooks/
│ │ ├── UserPromptSubmit.sh # 프로젝트 컨텍스트 자동 주입
│ │ ├── PermissionRequest.sh # 위험 명령 차단
│ │ └── commit.sh # Git 커밋 템플릿
│ └── README.md
├── lib/
│ ├── core/
│ │ ├── constants/
│ │ │ └── api_config.dart # MCP 대신 설정 집중화
│ │ └── validators/
│ │ └── input_validator.dart # 자동 검증
│ └── features/
└── claude_desktop_config.json # MCP 서버 설정
Hook 예시: Flutter 컨텍스트 자동 주입
#!/bin/bash
# .claude/hooks/UserPromptSubmit.sh
cat <<EOF
## Flutter 프로젝트 컨텍스트
**아키텍처:** Clean Architecture (도메인 주도 설계)
**상태 관리:** Riverpod 2.5.1
**데이터베이스:** Drift (SQLite)
**라우팅:** GoRouter 14.0.0
**네트워크:** Dio 5.4.0 + Retrofit
**명명 규칙:**
- 파일/폴더: snake_case
- 클래스: PascalCase
- 변수/함수: camelCase
- 상수: UPPER_SNAKE_CASE
**필수 규칙:**
1. 모든 네트워크 호출은 try-catch로 래핑
2. 새 기능은 features/ 아래 도메인별로 분리
3. UI와 비즈니스 로직 철저히 분리
4. 모든 입력값은 validator로 검증
**현재 작업 중:**
EOF
Skill 예시: Flutter 위젯 생성
---
name: flutter-stateless-widget
description: Flutter Stateless 위젯 생성 (Best Practice 적용)
---
# Flutter Stateless 위젯 생성
## 템플릿
```dart
import 'package:flutter/material.dart';
/// {위젯 설명}
///
/// 사용 예시:
/// ```dart
/// {WidgetName}(
/// // 파라미터 예시
/// )
/// ```
class {WidgetName} extends StatelessWidget {
const {WidgetName}({
super.key,
// 필수 파라미터
// 선택 파라미터
});
// 파라미터 선언
@override
Widget build(BuildContext context) {
return Container(
// 구현
);
}
}
체크리스트
- [ ] 위젯 이름이 PascalCase인가?
- [ ] const 생성자를 사용했는가?
- [ ] super.key를 전달했는가?
- [ ] 문서 주석(///)을 작성했는가?
- [ ] 필요한 경우 Theme.of(context) 사용했는가?
### 검증 코드 예시
```dart
// lib/core/validators/input_validator.dart
class InputValidator {
/// 이메일 검증
///
/// 반환: null이면 유효, String이면 에러 메시지
static String? email(String? value) {
if (value == null || value.isEmpty) {
return '이메일을 입력해주세요';
}
final emailRegex = RegExp(
r'^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\.[a-zA-Z]+'
);
if (!emailRegex.hasMatch(value)) {
return '올바른 이메일 형식이 아닙니다';
}
return null;
}
/// 비밀번호 검증 (8자 이상, 영문+숫자 조합)
static String? password(String? value) {
if (value == null || value.isEmpty) {
return '비밀번호를 입력해주세요';
}
if (value.length < 8) {
return '비밀번호는 8자 이상이어야 합니다';
}
if (!RegExp(r'[A-Za-z]').hasMatch(value) ||
!RegExp(r'[0-9]').hasMatch(value)) {
return '영문과 숫자를 조합해주세요';
}
return null;
}
/// 한국 휴대폰 번호 검증
static String? phoneNumber(String? value) {
if (value == null || value.isEmpty) {
return '휴대폰 번호를 입력해주세요';
}
final phoneRegex = RegExp(r'^01[0-9]-?[0-9]{4}-?[0-9]{4}$');
if (!phoneRegex.hasMatch(value)) {
return '올바른 휴대폰 번호 형식이 아닙니다 (예: 010-1234-5678)';
}
return null;
}
}
성과 측정
투자 전 (에이전트에게 매번 설명)
평균 세션당 시간: 45분
- 설명: 15분
- 구현: 20분
- 수정: 10분
같은 실수 반복: 주 3-4회
문서 업데이트: 수동, 자주 누락
투자 후 (시스템이 강제)
평균 세션당 시간: 20분
- 설명: 0분 (Hook이 자동 주입)
- 구현: 15분
- 수정: 5분 (검증이 사전 차단)
같은 실수 반복: 0회 (구조적으로 불가능)
문서 업데이트: 자동 (코드가 곧 문서)
초기 투자: 시스템 구축에 2-3일 운영 효율: 세션당 55% 시간 절약, 버그 80% 감소
시작하기
1단계: Hook 설정
# 프로젝트 루트에서
mkdir -p .claude/hooks
# 컨텍스트 자동 주입 Hook 생성
cat > .claude/hooks/UserPromptSubmit.sh <<'EOF'
#!/bin/bash
echo "## 프로젝트 컨텍스트"
echo "- 언어: Flutter/Dart"
echo "- 구조: Clean Architecture"
# ... 프로젝트별 정보 추가
EOF
chmod +x .claude/hooks/*.sh
2단계: 핵심 규칙 코드화
// lib/core/constants/project_rules.dart
/// 프로젝트 전역 규칙
///
/// AI 에이전트가 이 파일을 읽고 규칙을 따릅니다.
class ProjectRules {
/// 파일 명명 규칙
static const fileNaming = '''
모든 파일은 snake_case:
- ✓ user_profile_screen.dart
- ✓ payment_service.dart
- ✗ UserProfileScreen.dart
- ✗ paymentService.dart
''';
/// 폴더 구조 규칙
static const folderStructure = '''
features/{feature_name}/
├── data/ (데이터 소스, 모델, 리포지토리 구현)
├── domain/ (엔티티, 유스케이스, 리포지토리 인터페이스)
└── presentation/ (UI, 상태 관리)
''';
}
3단계: Skill 추가
# 홈 디렉토리에 Skills 폴더 생성
mkdir -p ~/.claude/skills/flutter-feature-setup
# Skill 정의 작성
cat > ~/.claude/skills/flutter-feature-setup/SKILL.md <<'EOF'
---
name: flutter-feature-setup
description: Clean Architecture 기반 Flutter 기능 모듈 생성
---
[Skill 내용은 위 예시 참고]
EOF
마무리
AI 에이전트는 기억하지 못합니다. 이것은 버그가 아니라 특성입니다.
이 특성을 이해하고 받아들이면, 협업 방식을 완전히 바꿀 수 있습니다:
- 명시적 인터페이스 - 추측하지 않아도 되게
- 임베디드 컨텍스트 - 찾지 않아도 되게
- 자동화된 제약 - 틀릴 수 없게
- 재사용 가능한 워크플로우 - 반복하지 않아도 되게
초기 비용은 높지만, 운영 비용은 0에 수렴합니다.
시스템을 제대로 구축하면, 에이전트는 실패할 수 없게 됩니다.
더 자세한 내용은 Claude 공식 문서를 참고하세요.
'AI' 카테고리의 다른 글
| LLM Council 아키텍처 (0) | 2025.11.23 |
|---|---|
| AI 활용을 위한 30가지 프롬프트 템플릿 완벽 가이드 (0) | 2025.11.11 |
| 프론트엔드 개발자가 오늘 바로 설치해야 할 4가지 MCP (0) | 2025.11.06 |
| AI 기반 개발의 세 가지 핵심 원칙 (0) | 2025.11.04 |
| SDD 개발 도구 비교 분석 (0) | 2025.11.03 |
