4. 함부로 실행하지 않는 Agent
"검증했다"는 말을 의심하기
이 차시를 마치면
'검증했다'는 말이 무엇까지 확인한 것인지 구분해 말합니다.
"검증했다"는 말이 실제로 무엇을 뜻하는지 구분합니다. 이 교재의 코드는 어디까지 확인되었는지 숫자로 밝힙니다.
검증 환경
| 항목 | 값 |
|---|---|
| 실행 방식 | Docker 컨테이너 (python:3.12-slim) |
| Python | 3.12.14 |
| langgraph | 1.2.11 |
| langgraph-checkpoint | 4.2.0 |
| 검증일 | 2026-08-15 |
학습팩 권장 버전은 Python 3.11 이상입니다. 이 조건을 컨테이너로 맞췄기 때문에, 호스트에 파이썬을 새로 설치하지 않고도 검증할 수 있었습니다.
결과 — 38개 테스트 전부 통과
| 기준 | 테스트 | 개수 | 결과 |
|---|---|---|---|
| ① 권한 최소화 | test_sec_tools.py | 10 | PASS |
| ② 승인 게이트 | ApprovalGateTest | 5 | PASS |
| ③ 멱등성 | IdempotencyTest | 2 | PASS |
| ④ 추적 가능성 | AuditLogTest | 2 | PASS |
| A-3 종료 조건 | TerminationTest | 1 | PASS |
| 구조 | GraphBuildTest | 2 | PASS |
| 감사 회귀 (A-3·A-4·A-5) | test_audit.py | 16 | PASS |
| 합계 | 38 | 38 / 38 | |
Ran 38 tests in 0.076s
OK
이 숫자가 처음부터 38이었던 것은 아닙니다
초판은 22개였고 전부 통과했습니다. 그런데 2026-08-15 보안 감사에서 결함 세 건이 나왔습니다. 테스트가 통과하고 있었는데도 말입니다. 이유는 테스트가 확인해야 할 것을 확인하지 않았기 때문입니다.
자세한 내용은 이 차시 아래 "테스트가 통과해도 안심할 수 없는 이유" 에 있습니다.
무엇이 실제로 확인되었는가
| 주장 | 확인 방법 |
|---|---|
| 고위험 입력은 실행 전에 멈춘다 | invoke 결과에 __interrupt__ 가 있고 result 가 "실행 완료"가 아님 |
| 저위험 입력은 승인 없이 실행까지 된다 | __interrupt__ 없이 종료하고 result 가 "실행 완료" 로 시작 |
| 승인하면 실행된다 | Command(resume="approve") 후 result 가 "실행 완료" 로 시작 |
| 반려하면 실행되지 않는다 | resume="reject" 후 approved 가 거짓 |
| Interrupt 전달값을 직렬화할 수 있다 | json.dumps() 가 예외 없이 통과 |
| 같은 요청은 한 번만 실행된다 | 키를 입력에서 파생하므로 두 번째 호출이 "이미 실행됨" 반환 |
| 승인 화면에 보인 값이 그대로 실행된다 | interrupt 전달값의 pending_action 이 result 에 포함됨 |
| 자동 승인도 기록에 남는다 | 저위험 경로의 audit_log 에 "저위험으로 자동 승인 (사람 개입 없음)" |
| 미승인 상태는 절대 실행되지 않는다 | 실행 기록 집합이 비어 있음 |
| 감사 로그가 덮어써지지 않는다 | 다섯 단계 기록이 모두 남아 있음 |
| 승인이 실행보다 먼저 기록된다 | 로그 안에서 두 항목의 순서 비교 |
| 반려를 반복해도 끝난다 | MAX_RETRY 뒤 __interrupt__ 없이 종료 |
| 심볼릭 링크 우회도 막힌다 | 허용 폴더 안에 밖을 가리키는 링크를 만들어 읽기 시도 → 거부 |
가장 중요한 확인 — 재개하면 정말 다시 실행되는가
교재가 "Interrupt 앞에 되돌릴 수 없는 일을 두지 말라"고 가르치는 근거는 "재개 과정에서 Node 가 다시 실행될 수 있다" 는 서술입니다. 이 서술이 실제로 참인지 계측했습니다.
== 멈춘 시점까지의 호출 ==
before_interrupt
ask:before-interrupt
== 재개 이후의 호출 ==
ask:before-interrupt ← 다시 실행됨
ask:after-interrupt
finish
interrupt 앞 코드가 실행된 횟수: 2
앞선 별도 node 가 실행된 횟수: 1
결과가 명확합니다.
| 대상 | 재개 시 재실행 | 뜻 |
|---|---|---|
interrupt() 를 부른 그 Node 의 앞부분 | 다시 실행됨 (2회) | 여기에 발송이나 삭제를 두면 두 번 나갑니다 |
| 이미 끝난 앞선 Node | 재실행되지 않음 (1회) | Checkpoint 가 완료된 단계를 건너뜁니다 |
즉 재실행 범위는 그래프 전체가 아니라 Interrupt 를 부른 Node 하나입니다. 이 사실이 설계 규칙을 정확히 설명합니다. side effect 를 승인 뒤의 별도 Node 에 두면 안전하고, 승인을 요청하는 Node 안에 두면 위험합니다.
비유
결재 서류를 올리다 멈추면, 그 서류 한 장은 처음부터 다시 쓰지만 이미 결재가 끝난 앞 서류들은 다시 쓰지 않습니다. 그러니 "다시 쓰는 그 한 장"에 송금 버튼을 그려 넣으면 안 됩니다.
세 가지 검증의 차이
| 검증 | 확인되는 것 | 확인되지 않는 것 |
|---|---|---|
py_compile | 문법 오류가 없다 | 실행하면 동작하는지, API 를 맞게 썼는지 |
import | 모듈이 실제로 만들어진다 | 기능이 의도대로 작동하는지 |
| 동작 테스트 | 정해진 입력에 정해진 결과가 나온다 | 테스트하지 않은 입력에서의 동작 |
이 구분을 놓친 실제 사례
학습팩의 검증 문서에서 이 문제가 나타났습니다. raw/07_CODE_ARCHITECTURE/05_PROJECT_TEMPLATE 의 pytest 는 No module named 'langgraph' 로 SKIPPED 였는데, 검증 문서는 이를 "구조 검증 가능" 으로 서술해 오해의 소지를 남겼습니다.
SKIPPED 는 테스트가 실행되지 않았다는 뜻입니다. PASS 가 아닙니다. 이 교재는 초판에서 같은 상태(문법 검사만 통과)에 머물러 있었고, 이번에 컨테이너로 환경을 맞춰 실제 실행으로 옮겼습니다. 분석
아직 확인되지 않은 것
테스트가 모두 통과했다고 해서 모든 것이 확인된 것은 아닙니다. 남은 항목을 밝힙니다.
- 확인 필요 durable Checkpointer — 검증은
InMemorySaver로만 했습니다. PostgreSQL 이나 Redis 를 붙였을 때의 동작은 확인되지 않았습니다 - 확인 필요 동시 실행 — 재개 요청 2개를 동시에 보내는 시험을 5회 했고 중복 실행이 없었습니다. 다만 실행 기록 집합의 확인과 추가가 원자적이지 않으므로 이 환경과 시도 횟수에서만 성립합니다
- 확인 필요 실제 LLM 연결 —
make_draft는 문자열을 만들 뿐 모델을 부르지 않습니다. 모델 호출이 들어가면 지연과 실패 처리가 추가로 필요합니다 - 확인 필요 승인자 인증 — 누가 승인했는지 확인하는 방법은 학습팩에 서술이 없습니다
- 확인 필요 langgraph 버전 의존 — 1.2.11 에서 확인한 결과입니다. 상위 버전에서 재개 동작이 달라질 수 있습니다
테스트가 통과해도 안심할 수 없는 이유
초판의 테스트 22개는 전부 통과하고 있었습니다. 그런데 2026-08-15 감사에서 결함 세 건이 나왔습니다. 그중 둘은 테스트가 있었는데도 드러나지 않았습니다.
| 테스트 | 무엇을 놓쳤는가 |
|---|---|
test_low_risk_runs_without_approval |
멈추지 않는다는 것과 위험 등급만 확인하고 result 를 확인하지 않았습니다. 저위험 경로가 아무것도 실행하지 않고 조용히 끝나고 있었는데 모르고 있었습니다 |
test_same_action_id_executes_once |
State 에 키를 미리 넣어 두고 확인했습니다. 실제 흐름에서는 그런 상태가 만들어지지 않습니다. 키가 매번 새로 생성되고 있었는데 테스트는 통과했습니다 |
두 경우의 공통점이 있습니다. 테스트가 확인한 것과 확인해야 했던 것이 달랐습니다. 통과했기 때문에 아무도 다시 보지 않았습니다.
비유
화재경보기를 달아 놓고 버튼을 눌러 소리가 나는지만 확인한 것과 같습니다. 소리는 잘 납니다. 그런데 연기를 감지하는지는 확인하지 않았습니다.
그래서 이 차시가 가르치는 것을 테스트 자체에도 적용해야 합니다. 이 테스트는 무엇을 확인하고 무엇을 확인하지 않는가. 분석
세 결함과 그 수정은 docs/audit/2026-08-15-보안감사보고서.md 에 기록되어 있고, 회귀 테스트 16개가 재발을 막습니다.
교육생에게
이 차시의 목적은 코드를 가르치는 것이 아니라 검증 범위를 부풀리지 않는 습관을 만드는 것입니다. "테스트 통과" 라는 말을 들으면 몇 개를, 어떤 환경에서, 무엇을 확인했는지 되묻는 것이 기본입니다.
다음 차시에서 이 검증을 직접 재현하는 방법을 다룹니다.