4. 함부로 실행하지 않는 Agent

"검증했다"는 말을 의심하기

이 차시를 마치면

'검증했다'는 말이 무엇까지 확인한 것인지 구분해 말합니다.

"검증했다"는 말이 실제로 무엇을 뜻하는지 구분합니다. 이 교재의 코드는 어디까지 확인되었는지 숫자로 밝힙니다.

검증 환경

항목
실행 방식Docker 컨테이너 (python:3.12-slim)
Python3.12.14
langgraph1.2.11
langgraph-checkpoint4.2.0
검증일2026-08-15

학습팩 권장 버전은 Python 3.11 이상입니다. 이 조건을 컨테이너로 맞췄기 때문에, 호스트에 파이썬을 새로 설치하지 않고도 검증할 수 있었습니다.

결과 — 38개 테스트 전부 통과

기준테스트개수결과
권한 최소화test_sec_tools.py10PASS
승인 게이트ApprovalGateTest5PASS
멱등성IdempotencyTest2PASS
④ 추적 가능성AuditLogTest2PASS
A-3 종료 조건TerminationTest1PASS
구조GraphBuildTest2PASS
감사 회귀 (A-3·A-4·A-5)test_audit.py16PASS
합계3838 / 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_actionresult 에 포함됨
자동 승인도 기록에 남는다저위험 경로의 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개가 재발을 막습니다.

교육생에게

이 차시의 목적은 코드를 가르치는 것이 아니라 검증 범위를 부풀리지 않는 습관을 만드는 것입니다. "테스트 통과" 라는 말을 들으면 몇 개를, 어떤 환경에서, 무엇을 확인했는지 되묻는 것이 기본입니다.

다음 차시에서 이 검증을 직접 재현하는 방법을 다룹니다.

이해도 확인

문항을 불러오는 중입니다.