L24 — 에이전트에게 증거를 떠먹이기: psql·서버 로그·traceback 접근을 제도화하기 (수정중)
L23은 "증거(로그·응답 본문)를 먼저 봐라" 였다. 이 강의는 그 짝이다 — 에이전트가 그 증거에 닿을 수 있게 프로젝트를 어떻게 설계했나. L23의 디버깅이 가능했던 전제 조건을 푼다.
핵심 명제 하나로 시작하자.
AI 에이전트의 디버깅 품질 = 닿을 수 있는 증거의 양. 더 똑똑한 프롬프트가 아니라, 더 많은 진짜 증거 통로(DB·로그·traceback)를 주는 게 효과가 크다. 증거에 못 닿는 에이전트는 추측만 한다 — L23에서 Claude가 두 번 헛다리를 짚은 게 그 증거다.
이 강의는 그 증거 통로를 세 개 열고, 그걸 제도화(말→강제→격리) 한 이야기다.
1. 증거 통로 ① — traceback이 로그에 남게 한다
로그를 읽어도 traceback이 없으면 소용없다.
except 에서 예외를 잡고 logger.error(e) 만 쓰면 메시지만 남고 스택은 버려진다 — 어디서 터졌는지 모른다.
같은 예외를 두 방식으로 로깅해 보면 차이가 한눈에 보인다.
# ✗ logger.error(e) — 메시지 한 줄만 남는다
try:
summary = transcribe(meeting_audio)
except Exception as e:
logger.error(e) # 로그: "division by zero" ← 어디서? 모른다
raise
# ✓ logger.exception(...) — except 안에서 호출하면 traceback이 자동으로 붙는다
try:
summary = transcribe(meeting_audio)
except Exception:
logger.exception("회의록 전사 실패") # 인자로 예외를 넘길 필요도 없다
raise
logger.exception 이 로그에 남기는 건 메시지 + 스택 전체다 — 에이전트(와 사람)가 진짜 터진 줄에 바로 닿는다:
ERROR 회의록 전사 실패
Traceback (most recent call last):
File "meetings/services.py", line 88, in transcribe
ratio = total / silence_frames
~~~~~~^~~~~~~~~~~~~~~~~
ZeroDivisionError: division by zero
위쪽 로그(division by zero 한 줄)로는 services.py:88 을 절대 못 짚는다. 아래쪽이 있어야 디버깅이 시작된다.
그래서 규칙을 박았다 — CLAUDE.md ## Code Patterns > Error Handling:
except블록에서 예외를 로깅할 때는logger.error/logger.warning대신 반드시logger.exception을 쓴다. (traceback이 무조건 로그에 남아야 진단이 된다.)
말로만 두면 안 지켜진다.
그래서 hook으로 강제한다 — .claude/hooks/check-except-logger-exception.sh → .claude/hooks/check_except_logger.py.
이건 커밋 시점에 staged .py 를 AST로 파싱해, except 블록 안에서 logger.error/.warning 을 쓴 곳을 잡아 커밋을 차단한다(exit 2).
두 가지 설계 포인트: - AST 파싱 — 단순
grep이 아니라 구문 트리로 "except 블록 안" 인지 본다. except 밖의 논리적 실패 로깅(logger.error)은 정상이므로 안 잡는다. - 신규 위반만 — 이번 커밋에서 추가된 라인만 본다. 기존 코드의 옛 위반으로 커밋이 막히는 노이즈를 피한다.
출처는 popax의 "전사/회의록 task traceback 유실" 사건이다 — 한 번 데인 교훈이 1층 코드 규칙 + hook으로 굳었다.
2. 증거 통로 ② — 서버 로그에 닿게 한다
traceback이 로그에 남아도, 에이전트가 그 로그를 읽을 수 있어야 한다.
그래서 로그 읽기를 전담하는 read-only 서브에이전트를 만들었다 — .claude/agents/debug-server-log.md.
name: debug-server-log
tools: Read, Grep, Glob, Bash # ← 쓰기 도구(Edit/Write) 없음 = read-only 강제
이 에이전트는 추측이나 코드 추정 전에 로그부터 읽어 traceback과 요청 상태를 보고한다. 로그 위치도 명시돼 있다:
| 환경 | 로그 | 접근 |
|---|---|---|
| dev (runserver) | Django 콘솔 stdout | BashOutput(백그라운드 shell) |
| prod (Apache) | error 로그 (Django traceback) | sudo tail /var/log/apache2/*-error.log |
| prod (Apache) | access 로그 (요청·상태·응답크기) | sudo tail /var/log/apache2/*-access.log |
닿기 위한 접근권한도 깔려 있다 — prod 박스는 개인키 SSH(~/.ssh/popup-chris-prod.pem)와 devteam NOPASSWD sudo(팀 wiki infrastructure/sudo-policy.md 참고)로 로그를 읽는다.
예를 들어 "로그인이 403으로 막힌다"는 제보가 오면, 이 에이전트는 코드를 보기 전에 prod access 로그부터 길어 온다:
$ sudo tail -n 3 /var/log/apache2/education-prod-access.log
1.2.3.4 - - [30/Jun/2026:23:10:02] "POST /api/login/ HTTP/1.1" 403 748 ← 사용자(브라우저)
5.6.7.8 - - [30/Jun/2026:23:11:40] "POST /api/login/ HTTP/1.1" 403 5209 ← 내 curl 재현
상태코드(403)는 둘 다 같다. 단서는 응답 크기다 — 사용자는 748, 내 재현은 5209. 크기가 다르면 둘은 다른 403 이고, "내 재현이 실패 조건과 다르다"(L23 3번 규칙)는 자각이 여기서 나온다. 그다음 error 로그의 응답 본문({"detail":"Invalid username/password."})이 진짜 원인(BasicAuth)을 지목한다. 에이전트가 access 로그에 닿지 못했으면 이 한 줄을 영영 못 본다.
왜 read-only 전용 에이전트인가 — L12에서 봤듯,
tools에Edit/Write를 안 주면 "로그만 보고 코드는 못 고친다"가 구조적으로 강제된다. 조사와 수정을 분리해, 조사 단계에서 섣불리 코드를 건드리는 사고를 막는다. (수정 결정은 메인 Claude 가 한다.)
3. 증거 통로 ③ — DB에 닿게 한다
"분명히 저장했는데" 류의 버그는 DB를 직접 봐야 풀린다 — 추정하면 틀린다. 이건 별도 스킬이 필요 없었다.
Bash도구로psql을 직접 치거나,python manage.py shell_plus(CLAUDE.mdCommon Commands) 로 ORM을 직접 굴린다.- prod DB도 같은 SSH + sudo로 닿는다.
L23에서 "유저 chris@popupstudio.ai 가 실제로 존재하는가"를 psql로 1초 만에 확인한 게 이 통로다.
$ psql education_prod -c "SELECT id, email, is_active FROM accounts_user WHERE email='chris@popupstudio.ai';"
id | email | is_active
----+-----------------------+-----------
7 | chris@popupstudio.ai | t
(1 row)
(1 row) 한 줄이 "유저는 멀쩡히 있다"를 확정한다 — 그러니 403의 원인은 유저 부재가 아니다. 추측이라면 "혹시 계정이 없나? 비활성인가?"를 한참 맴돌았겠지만, DB를 직접 보면 그 가지가 즉시 잘린다. 남는 가설(인증 헤더 문제)로 곧장 좁혀지는 것이다.
만약 이걸 못 봤으면 "유저가 없어서 그런가?"를 한참 추측했을 것이다.
4. 제도화 — 말 → 강제 → 격리 (M3 3종 세트)
증거 통로 세 개를, 사람이 매번 챙기지 않아도 굴러가게 제도화했다. M3에서 배운 세 장치가 그대로 쓰인다.
| 층 | 장치 | 역할 | 강의 |
|---|---|---|---|
| 말 | CLAUDE.md ## Debugging Rules |
"로그 먼저, 응답 본문 보기, 재현 조건 확인, DB 직접" 을 문서로 박음 | template CLAUDE.md |
| 강제 | hook (check_except_logger.py) |
logger.exception 안 쓰면 커밋 차단 (말이 안 지켜질 때의 안전망) | L13 |
| 격리 | agent (debug-server-log) |
로그 읽기를 read-only 전용 창으로 분리 | L12 |
CLAUDE.md(말)는 어기기 쉽고, hook(강제)은 어길 수 없지만 만들기 번거롭다. 반복해서 데는 규칙만 hook으로 승격한다 — L13의 "말→강제 승격" 원칙 그대로다.
5. payoff — 이번 세션이 그 증명
L23의 진짜 원인(브라우저 캐시 Authorization: Basic + DRF BasicAuthentication)은 세 통로가 다 있었기에 찾았다.
- DB 접근(psql) → 유저 존재 확인, "익명 요청인데 왜 인증 에러?" 의 단서.
- prod 로그 접근(journalctl·Apache access) → 사용자 403의 응답 크기(748)가 내 재현(5209)과 다름을 발견 → "내 재현은 가짜"를 자각.
- 실제 응답 본문 접근 →
{"detail":"Invalid username/password."}한 줄이 BasicAuth를 지목.
셋 중 하나라도 없었으면 추측에서 끝났을 것이다. 증거 통로가 곧 디버깅 능력이라는 명제의 산 증거다.
6. 일반화 — 컨텍스트 엔지니어링 체크리스트
새 프로젝트에서 에이전트의 디버깅 컨텍스트를 깔 때 점검할 것.
- traceback이 로그에 남는가 —
except로깅이logger.exception인가. (hook으로 강제) - 에이전트가 로그를 읽을 수 있는가 — dev/prod 로그 경로 + 읽기 권한(SSH·sudo) + 로그 읽기 전용 에이전트.
- DB를 직접 볼 수 있는가 — psql/shell_plus 접근.
- 이 통로들이 말→강제→격리로 제도화돼 있는가 — CLAUDE.md(말) / hook(강제) / read-only agent(격리).
- 안전한가 — 조사 도구는 read-only로 격리(
tools제한). 증거 접근을 넓히되 사고 반경은 좁힌다.
한 줄 요약: 에이전트에게 줄 것은 더 긴 프롬프트가 아니라 더 많은 진짜 증거 통로다 — 단, 격리와 강제를 곁들여서.
오늘 정리 + 다음
- 정리: 에이전트의 디버깅 품질은 닿을 수 있는 증거(traceback·로그·DB)의 양으로 결정된다. 우리는 세 통로(logger.exception로 traceback 보장 / debug-server-log 에이전트·SSH·sudo로 로그 / psql·shell_plus로 DB)를 열고, 이를
CLAUDE.md(말)·hook(강제)·read-only agent(격리)의 3층으로 제도화했다. L23의 디버깅이 가능했던 건 이 전제 덕이다. - 흔한 함정:
except에서logger.error(e)만 쓰는 것 — 로그를 봐도 traceback이 없어 "왜 터졌는지 모르는" 상태가 된다. (그래서 hook으로 막는다.) - 다음 시간: 조사 도구의 read-only 격리를 어디까지 넓힐지 — MCP·외부 도구 접근과 권한 경계.
- 자습 권장:
.claude/agents/debug-server-log.md,.claude/hooks/check_except_logger.py를 직접 열어보기. 디버깅을 겪은 사례는 L23. 접근권한(SSH·sudo)은 팀 wikiinfrastructure/sudo-policy.md·prod-server-host.md참고(자습용).