Files
aio-01d4-stu/Last3Mon/76/LAB-01-Q-FAILURE-TYPES.md

55 KiB

Lab 01 · 실패를 겪어 보고, 항목마다 처리하고, 중복 없이 다시 해 보기

  • 관련 강의: 1단계 · 실패의 종류
  • 소요 시간: 50분 (2교시 한 시간) · 예제 1 10분 · 예제 2 15분 · 예제 3 7분 · 과제 1 8분 · 과제 2 10분
  • 준비: 실습 준비의 doctor → ready, base-intake · nightly-report 게시
  • 목표: 여러 건을 한 번에 처리할 때 한 건의 실패가 전체를 망치지 않게 한다. 잘못된 항목은 그 항목만 거절하고, 잠깐의 실패는 다시 해 보고, 다시 해도 접수 건이 두 번 생기지 않게 한다. 고장 종류마다 재시도가 무엇을 하는지 재 보고, LLM이 멈춰도 접수는 하고, 요청 안의 규칙(같은 id · 10건 상한)을 지킨다.
flowchart LR
  E1["예제 1<br/>기준을 망가뜨려 보기"] --> E2["예제 2<br/>항목 확인 · 재시도 ·<br/>멱등 키"] --> E3["예제 3<br/>고장 종류마다<br/>재시도가 하는 일"] --> T1["과제 1<br/>LLM이 멈춰도 접수"] --> T2["과제 2<br/>요청 안의 규칙"]

복사해 붙일 요청문은 없습니다. 막히면 ▶ 를 하나씩 펼칩니다. 연습이 끝나면 uv run tools/flow.py ops --reset.

시작하기 · 터미널 두 개

터미널 하는 일
① 에이전트 cd ~/aio-01d4-stu/Last3Mon/76 후 codex (또는 claude, agy). 내가 쓴 요청문을 보낸다
② 확인 cd ~/aio-01d4-stu/Last3Mon/76 에서 call · push · test · ops · fault를 내가 실행한다

cp templates/RUNBOOK-TEMPLATE.md my-work/RUNBOOK.md 후 표를 채워 갑니다. 쓴 요청문은 my-work/PROMPTS.md에 모읍니다.

예제 1 · 기준을 망가뜨려 보기 (10분, 에이전트 없이)

이 실습의 의도

한 줄로: 오늘 고칠 기준 워크플로우(base-intake)가 실패할 때 무엇을 숨기는지 직접 봅니다. 고치는 것은 예제 2부터이고, 여기서는 망가뜨려 보고 적기만 합니다.

생활 속 예 · 마트 계산대. 계산원이 "결제 실패"라고만 했는데 영수증을 보니 앞의 물건은 이미 결제돼 있다면, 다시 계산하면 두 번 냅니다. 예제 1은 일부러 이런 상황을 만들어, 들은 말(워크플로우의 답, call)과 실제로 일어난 일(영수증, ops)을 나란히 보는 실습입니다.

상황. base-intake는 문의 여러 건(requests[])을 한 번에 받아, 한 건씩 LLM으로 분류하고 데이터 API에 접수 건을 만듭니다. 정상 요청이면 잘 됩니다. 그런데 운영하다 보면 이런 일이 생깁니다.

  • 보낸 문의 중 하나가 잘못됨 (글이 비어 있음)
  • 데이터 API가 잠깐 끊김
  • 사용자가 답을 못 받았다며 같은 요청을 다시 보냄

이 셋을 하나씩 일부러 일으켜, 사용자가 받은 답과 데이터 API에 실제로 생긴 접수 건을 나란히 봅니다. 둘이 다르면 그것이 오늘 고칠 문제입니다.

실험 셋.

실험 일으키는 것 보낼 요청 볼 것
① 잘못된 항목 3건 중 가운데 글이 빔 batch-bad-item (R-201 · 빈 R-202 · R-203) 답은 성공인가 실패인가? 그런데 ops에 접수 건이 생겼나, 몇 개?
② 잠깐의 끊김 fault tickets flaky: 접수 건 만들기가 키마다 첫 시도만 실패 batch-two (R-301 · R-302) 한 번만 더 했으면 됐을 실패를 기준은 어떻게 끝내나?
③ 다시 보내기 고장을 끄고 정상 3건을 한 번 더 batch-ok (R-101~103) 두 번째 같은 문의의 접수 건이 몇 개가 되나? ops의 key 칸이 첫 번째와 같나?

call은 웹훅을 한 번 불러 워크플로우의 답을 보여 주고, ops는 데이터 API에 지금 있는 접수 건 · DLQ · 알림 · 고장 스위치를 보여 줍니다. ops는 답이 아니라 실제로 생긴 것을 보는 창입니다. 두 화면을 번갈아 봅니다.

끝나면 알게 되는 것. 기준은 ① 한 건 때문에 전체를 실패라고 하면서 다른 건은 이미 만들었고, ② 잠깐의 끊김도 바로 실패로 끝내고, ③ 같은 문의를 다시 보내면 또 만듭니다. 예제 2에서 이 셋을 항목 확인 · 재시도 · 멱등 키로 하나씩 고칩니다.

적을 곳. my-work/RUNBOOK.md 1.1 표 (한 일 · 사용자가 본 것 · 실제로 생긴 것), 실험마다 한 줄.

uv run tools/flow.py ops --reset                                      # 접수 건 · DLQ · 알림 비우기, 고장 스위치 모두 ok

# 0. 정상일 때 (비교 기준)
uv run tools/flow.py call base-intake requests/batch-ok.json          # 정상 3건

# ① 잘못된 항목
uv run tools/flow.py call base-intake requests/batch-bad-item.json    # 가운데 항목의 text가 비어 있음
uv run tools/flow.py ops                                              # 답은 실패였는데, 접수 건은 몇 개?

# ② 잠깐의 끊김
uv run tools/flow.py fault tickets flaky                              # 접수 건 만들기가 키마다 첫 시도에 실패
uv run tools/flow.py call base-intake requests/batch-two.json
uv run tools/flow.py fault tickets ok                                 # 고장 끄기

# ③ 다시 보내기
uv run tools/flow.py call base-intake requests/batch-ok.json          # 0번과 같은 요청을 다시
uv run tools/flow.py ops                                              # VPN 문의(R-101)의 접수 건은 몇 개? key는?

이해하기

여러 건을 한 번에 처리하는 일(일괄 처리, batch)에는 "반쯤 된" 상태가 있습니다. 한 건만 처리하면 결과는 됐다 · 안 됐다 둘 중 하나입니다. 세 건을 처리하다 두 번째에서 멈추면 첫 번째는 이미 끝났고, 세 번째는 했는지 안 했는지 모릅니다. 그런데 사용자에게는 500 하나만 갑니다.

flowchart LR
  Q(["requests 3건"]) --> S["하나씩 나누기"] --> C{{"LLM 분류"}} --> T["접수 건 만들기<br/>key = 실행 번호"]
  T -->|1번째 · 3번째| OK["만들어짐"]
  T -->|2번째: 빈 글 → 400| X["노드 오류 → 전체 500"]

오늘 기준이 가진 문제 셋.

문제 겉으로 실제로
한 건의 실패가 전체를 멈춤 HTTP 500 · Error in workflow 다른 건은 이미 접수됨 (부분 처리)
잠깐의 실패도 바로 실패 500 1초 뒤에 다시 했으면 됐을 것
다시 보내면 또 만듦 200 · 접수 건 번호 3개 같은 문의의 접수 건이 둘 (중복)

왜 중복이 생기나. 기준은 접수 건을 만들 때 키를 $execution.id + '-' + $itemIndex(이번 실행 번호와 순서)로 정합니다. 같은 문의를 다시 보내면 실행 번호가 다르니 데이터 API는 다른 요청으로 봅니다. "같은 문의"를 알아보게 하려면 실행마다 바뀌지 않는 것(요청의 id)이 키여야 합니다 (예제 2).

고장 스위치. 오늘 데이터 API에는 접수 건 만들기(tickets)와 DLQ(dlq)에 고장 스위치가 있습니다. flaky는 키마다 첫 시도만 실패시킵니다 (잠깐의 끊김), error는 늘 500, slow는 8초를 기다린 뒤 답합니다. 연습이 끝나면 ops --reset이 접수 건 · DLQ · 알림을 비우고 스위치를 모두 ok로 돌립니다.

기대 결과. 빈 글 하나가 섞인 3건 → 500인데 접수 건 2개가 생깁니다. flaky에서는 500. 같은 정상 3건을 두 번 보내면 VPN 문의의 접수 건이 두 개가 됩니다. RUNBOOK.md 1절에 "사용자가 본 것 / 실제로 생긴 것"을 나란히 적습니다.

▶ 1단계 · 실패를 나누기
실패 다시 하면 되나
입력이 잘못됨 (빈 글) 아니오 — 몇 번을 해도 같음
API가 잠깐 실패 예
API가 오래 멈춤 지금은 아니오, 나중에는 예

이 셋을 똑같이 "500"으로 끝내면 무엇을 잃을까요? 사용자는 무엇을 할까요?

▶ 2단계 · 중복은 왜 생기나

ops의 key 칸을 봅니다. 두 번째 batch-ok의 키는 첫 번째와 같나요? 같은 요청을 알아보게 하려면 무엇을 키로 써야 할까요?

실행 결과 · 기준을 망가뜨려 보기

수업 준비 때(10/29) 강사 노트북에서 실제로 나온 출력입니다 (줄임). LLM 분류 · 시간 · 접수 건 번호의 순서는 실행마다 조금씩 다를 수 있고, 500 · 접수 건 수는 같아야 합니다.

이 예제는 고치지 않습니다. 기준 base-intake에 실험 넷(0 · ① · ② · ③)을 차례로 해 보고 무엇이 망가지는지 봅니다. 고치는 것은 예제 2입니다.

0 · 정상 3건 (비교할 기준)

실행한 명령:

uv run tools/flow.py ops --reset
uv run tools/flow.py call base-intake requests/batch-ok.json

출력:

✔ created tickets, DLQ and alerts emptied, fault switch all ok

HTTP 200 · 5.2s   {"processed": 3, "tickets": ["T-2001", "T-2002", "T-2003"]}

왜 이렇게 나왔나: 고장 스위치가 모두 ok이고 세 문의 모두 글이 있으니 분류와 접수 건 만들기가 세 번 다 성공했습니다. 기준은 잘될 때는 잘됩니다. 아래 셋은 잘 안 될 때입니다.

① 빈 글이 섞인 요청 → 500인데 접수 건이 생김

실행한 명령:

uv run tools/flow.py call base-intake requests/batch-bad-item.json
uv run tools/flow.py ops

출력:

HTTP 500 · 3.2s   {"message": "Error in workflow"}

created tickets: 5
  T-2004  key 10058-0  장비/보통  프린터 용지가 걸려요
  T-2005  key 10058-2  소프트웨어/보통  메일 첨부가 안 열려요
DLQ: 0 (parked 0)
alerts: 0

왜 이렇게 나왔나

  • 응답은 500, 곧 "실패"입니다. 그런데 접수 건이 3개에서 5개로 늘었습니다 (0번 3개 + 이번 2개).
  • 키를 보면 까닭이 보입니다. 10058-0은 실행 번호 10058의 0번째 항목이고, 10058-2는 2번째 항목입니다. 1번째(빈 글)만 없습니다. 접수 건 만들기 노드는 항목을 하나씩 처리하다가 빈 글에서 데이터 API의 400(title이 비었음)을 받았고, 거기서 워크플로우 전체가 멈췄습니다. 앞뒤 항목은 이미 처리된 뒤였습니다.
  • 그래서 생긴 문제는 부분 성공을 실패라고 말한 것입니다. 사용자는 "실패"를 믿고 다시 보낼 것이고, 그러면 프린터 · 메일 문의가 한 번 더 생깁니다.

② 잠깐 끊김 (flaky) → 다시 해 보면 될 것을 바로 포기

실행한 명령:

uv run tools/flow.py fault tickets flaky
uv run tools/flow.py call base-intake requests/batch-two.json
uv run tools/flow.py fault tickets ok

출력:

HTTP 500 · 2.5s   {"message": "Error in workflow"}

왜 이렇게 나왔나: flaky는 키마다 첫 시도만 500을 주고, 두 번째부터는 성공하는 고장입니다. 한 번만 더 보냈으면 됐을 텐데, 기준에는 재시도가 없어 첫 500에서 바로 멈췄습니다. 이번 요청으로는 접수 건이 하나도 생기지 않았습니다.

③ 0번과 같은 요청을 다시 → 같은 문의가 두 번

실행한 명령:

uv run tools/flow.py call base-intake requests/batch-ok.json
uv run tools/flow.py ops

출력:

HTTP 200 · 3.0s   {"processed": 3, "tickets": ["T-2006", "T-2008", "T-2007"]}

created tickets: 8
  T-2006  key 10060-0  네트워크/높음  VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있
  …

왜 이렇게 나왔나

  • 0번과 똑같은 3건을 보냈는데 새 번호 3개(T-2006~2008) 가 생겼습니다. 접수 건 8개 = 0번 3 + ① 2 + ② 0 + ③ 3.
  • VPN 문의 하나에 접수 건이 둘(T-2001 · T-2006)입니다. 0번의 키는 10057-0, 이번 키는 10060-0입니다. 키가 실행 번호라 보낼 때마다 바뀌니, 데이터 API는 같은 문의인 줄 모릅니다.
  • 사용자가 ①에서 "실패"를 보고 다시 보내면 바로 이 일이 생깁니다.

정리 · 예제 2에서 고칠 것

실험 본 것 예제 2에서 고칠 것
① 빈 글 500인데 2건이 이미 생김 항목마다 확인하고, 결과에 항목마다 상태를
② flaky 한 번 더 하면 될 것을 바로 500 잠깐의 실패는 다시 해 보기
③ 같은 요청 같은 문의가 두 번 접수 키를 실행 번호가 아니라 요청 id로

예제 2 · 항목 확인 · 재시도 · 멱등 키 (15분)

이 실습의 의도

한 줄로: 예제 1에서 본 세 문제를 내 워크플로우(my-intake)에서 고칩니다. Lab 01의 중심 실습입니다.

생활 속 예 · 택배 배송 조회 + 송장 번호. 좋은 택배 회사는 ① 상자마다 "배달 완료 / 주소 불명 반송"을 따로 보여 주고(부분 성공), ② 벨을 눌러 대답이 없으면 한 번 더 눌러 보고(재시도), ③ 같은 송장 번호의 상자는 두 번 배달하지 않습니다(멱등 키). 예제 2는 my-intake를 이런 택배 회사로 만드는 실습입니다.

예제 1의 문제 고치는 방법 확인하는 테스트 케이스
① 한 건이 잘못되면 전체 500, 다른 건은 이미 접수 항목마다 확인해 잘못된 것만 rejected, 결과는 늘 200에 항목마다 상태 (부분 성공) 잘못된 항목 · 목록이 아님
② 잠깐의 끊김도 바로 실패 접수 건 만들기 노드에 재시도 3번, 그래도 실패면 failed로 적고 계속 가끔 실패 (flaky)
③ 다시 보내면 또 만듦 접수 건의 key를 실행 번호 대신 요청 id(R-101)로 같은 요청을 다시 · 접수 건 수

왜 셋을 한 번에. 셋은 서로 기대고 있습니다. 재시도(②)를 넣으면 n8n은 노드에 들어온 항목을 모두 다시 보냅니다. 이미 만든 접수 건도 또 보내므로, 멱등 키(③) 없이는 재시도가 중복을 만듭니다. 그리고 재시도 뒤에도 실패한 항목을 failed로 남기려면 항목마다 상태를 담는 결과 모양(①)이 먼저 있어야 합니다.

순서. 기준을 복사 → 같은 테스트를 기준에 먼저 돌려 0/6을 봄 (무엇이 틀렸는지 케이스마다 확인) → 요청문을 써서 에이전트에게 고치게 함 → push → 6/6.

끝나면 알게 되는 것. "일부가 실패해도 200 + 항목마다 상태"가 왜 500보다 정직한지, n8n 재시도가 노드의 어디에 붙고 무엇을 다시 보내는지, 멱등 키가 재시도를 어떻게 안전하게 만드는지.

만들 것: 기준을 복사해, 항목마다 확인하고, 접수 건 만들기를 다시 해 보게 하고, 요청 id를 멱등 키로. 결과는 늘 200 (부분 성공).

cp reference/baseline-intake.json my-work/intake.json
uv run tools/flow.py test cases/lab01.json --webhook base-intake -v     # 고치기 전 (기준에 같은 테스트)
조건 값
파일 · 웹훅 my-work/intake.json · my-intake (경로 · 이름을 바꿈)
거절 id · user · text가 없는 항목 → rejected와 이유, 나머지는 처리
재시도 timeout 3초, 3번, 0.3초 간격 · 그래도 실패면 failed (멈추지 않음)
결과 · 400 AGENTS.md 3절 (requests가 목록이 아니면 400)
기대 결과 cases/lab01.json

이해하기

지금 무엇이 문제인가. 예제 1의 세 문제(한 건이 전체를 멈춤 · 잠깐의 실패도 실패 · 다시 보내면 중복)를 한 번에 고칩니다. 셋은 따로 보이지만 서로 이어져 있습니다. 재시도를 넣으면 이미 만든 건을 또 보내게 되므로, 재시도와 멱등 키는 늘 함께 갑니다.

flowchart LR
  subgraph 지금
    A1(["3건"]) --> B1["하나씩"] --> C1{{"분류"}} --> D1["접수<br/>key = 실행 번호<br/>재시도 없음"] -->|하나라도 실패| X1(["500"])
  end
  subgraph 바꾼 뒤
    A2(["3건"]) --> V2{"항목 확인"}
    V2 -->|맞음| C2{{"분류"}} --> D2["접수<br/>key = 요청 id<br/>재시도 3 · 계속"] --> M2["모으기"]
    V2 -->|빠짐| R2["거절 · 이유"] --> M2
    M2 --> O2(["200 · 항목마다 상태"])
  end

부분 성공. 결과는 늘 HTTP 200이고, 항목마다 created · failed · rejected와 이유를 담습니다. "모두 되거나 500" 대신 "무엇이 되고 무엇이 안 됐는지"를 알려 줍니다. 받는 쪽은 rejected는 고쳐서, failed는 나중에 다시 보내면 됩니다.

n8n의 재시도는 노드 단위입니다. retryOnFail · maxTries · waitBetweenTries는 노드 바로 아래(parameters 밖)에 둡니다. 그리고 그 노드에 들어온 항목을 모두 다시 보냅니다. 세 건 중 하나만 실패해도 세 건을 다시 보냅니다. 그래서 키가 필요합니다.

멱등 키(idempotency key). 같은 요청을 몇 번 보내도 결과가 한 번 한 것과 같게 하는 표시입니다. 오늘 데이터 API는 POST /tickets의 key가 같으면 새로 만들지 않고 처음 번호를 돌려줍니다 (duplicate: true).

키 같은 요청을 다시 보내면
실행 번호 ($execution.id) 매번 새 키 → 중복
요청 id (R-101) "이미 만든 건" → 같은 번호, duplicate: true

실패해도 다음으로. 재시도 뒤에도 실패한 항목에서 멈추면 안 됩니다. onError: continueRegularOutput이면 실패한 항목이 error를 가진 채 다음 Code로 갑니다. 다음 Code는 앞 노드(Parse)의 항목과 같은 순서로 맞춰 created / failed를 정합니다.

테스트 케이스.

케이스 보내는 것 기대
정상 3건 batch-ok created: 3
같은 요청을 다시 batch-ok 또 created: 3, duplicates: 3
접수 건 수 /tickets-created/list count: 3 (중복 없음)
잘못된 항목 batch-bad-item created: 2, rejected: 1, 이유 text is required
목록이 아님 not-a-list HTTP 400
가끔 실패 flaky + batch-two created: 2, failed: 0 (재시도로 성공)

기대 결과. 고치기 전(기준에 같은 테스트) 0/6, 고친 뒤 6/6.

▶ 1단계 · 거절한 항목은 어디로

거절한 항목은 LLM · API로 보내지 않습니다. 하지만 결과에는 있어야 합니다. 두 갈래(처리 · 거절)를 다시 하나로 모으는 노드가 AGENTS.md 5절에 있습니다. 두 번째 입력은 index: 1.

▶ 2단계 · 재시도와 중복

n8n 노드의 재시도는 그 노드에 들어온 항목을 모두 다시 보냅니다. 이미 성공한 항목도요. 멱등 키가 있으면 데이터 API가 "이미 만든 건"이라며 같은 번호를 돌려줍니다 (duplicate: true).

▶ 3단계 · 실패한 항목도 다음으로

재시도 뒤에도 실패한 항목에서 워크플로우가 멈추면 안 됩니다. 실패한 항목이 error를 가진 채 다음 노드로 가게 하는 설정과, 다음 Code에서 앞 노드 항목과 순서를 맞추는 방법을 요청문에 적습니다.

▶ 빈칸 채우기 · 요청문(프롬프트) 뼈대

요청문에 꼭 들어갈 것: ① 파일 · 웹훅 ② 항목 확인과 거절 이유 ③ 재시도 설정과 실패해도 계속 ④ 멱등 키 ⑤ 두 갈래 모으기 ⑥ 결과 모양 · 기대 결과. 아래 빈칸 4개(____①~____④)를 채워 my-work/PROMPTS.md에 쓴 뒤 에이전트에게 보냅니다.

my-work/intake.json (baseline 을 복사한 것) 의 웹훅 경로를 my-intake 로, 이름을 "76 Intake (my)" 로 바꿔 줘.
requests 가 목록이 아니면 400 {"error": "requests must be a list"}.
항목마다 id · user · text 를 확인해서, 빠진 것이 있으면 LLM 과 API 로 보내지 말고 status rejected 와 이유("text is required" 등)로.
Create ticket 노드에 timeout 3000ms, 재시도 ____① 번, 간격 300ms, 그리고 실패해도 다음으로 가게 onError ____② .
접수 건의 key 는 ____③ (실행 번호가 아님).
처리한 갈래와 거절한 갈래를 Merge 로 모아 AGENTS.md 3절 결과 모양으로, HTTP ____④ .
기대 결과 cases/lab01.json.

빈칸 힌트 (답이 아니라 어디서 찾고 무엇을 생각하나):

  • ① 위 조건 표의 "재시도"입니다. 처음 한 번을 포함한 수입니다.
  • ② AGENTS.md 5절 접수 건 만들기 노드의 설정입니다. 멈추지 않고 실패한 항목이 다음으로 가는 값.
  • ③ 같은 문의를 다시 보내도 바뀌지 않는 것입니다. 예제 1의 ops에서 키가 왜 바뀌었는지 떠올립니다.
  • ④ 부분 성공의 상태 코드입니다. 일부가 실패해도 이것.

채운 요청문은 수업 뒤 공개되는 정답본의 요청문 예와 비교합니다. 노드 이름은 내 파일의 실제 이름을 씁니다.

uv run tools/flow.py push my-work/intake.json
uv run tools/flow.py test cases/lab01.json -v
uv run tools/flow.py ops

실행 결과 · 항목 확인 · 재시도 · 멱등 키

수업 준비 때(10/29) 강사 노트북에서 정답 워크플로우로 실제로 나온 출력입니다. 시간은 실행마다 조금씩 다르고, ✔ · ✘는 같아야 합니다.

고치기 전 · 기준(base-intake)에 같은 테스트

실행한 명령:

uv run tools/flow.py test cases/lab01.json --webhook base-intake -v

출력 (결과 줄만, -v의 요청 · 응답은 줄임):

✘ 정상 3건 · 2.7s · created = null (expected 3); failed = null (expected 0); rejected = null (expected 0)
✘ 같은 요청을 다시 보내도 중복 없음 · 3.1s · created = null (expected 3); duplicates = null (expected 3)
✘ 만들어진 접수 건은 3개 · 0.0s · count = 6 (expected 3)
✘ 잘못된 항목은 그 항목만 거절 · 3.4s · status 500 (expected 200); ...
✘ 목록이 아니면 400 · 0.1s · status 500 (expected 400)
✘ 가끔 실패하는 API · 다시 해 보기로 성공 · 2.0s · status 500 (expected 200); ...
Passed 0/6

왜 이렇게 나왔나 (고치기 전)

  • 정상 3건 ✘ created = null: 기준은 200을 주고 접수 건도 만들었습니다. 하지만 결과가 {processed, tickets} 모양이라 테스트가 찾는 created · failed · rejected 칸(AGENTS.md 3절의 결과 모양)이 없습니다. null은 "그런 칸이 없음"이라는 뜻입니다.
  • 같은 요청 ✘ · 접수 건 ✘ count = 6: 같은 3건을 두 번 보냈더니 6개가 되었습니다. 예제 1 ③과 같은 까닭으로, 키가 실행 번호라서입니다.
  • 잘못된 항목 ✘ 500: 예제 1 ①과 같습니다. 빈 글 하나 때문에 요청 전체가 500으로 끝났습니다.
  • 목록이 아니면 ✘ 500 (400 기대): 본문이 목록인지 보지 않고 바로 나누다가 n8n이 오류를 냈습니다. 400은 "보낸 쪽이 잘못"이고 500은 "받는 쪽이 고장"이라, 보낸 사람에게 틀린 신호를 줍니다.
  • 가끔 실패 ✘ 500: 예제 1 ②와 같습니다. 재시도가 없습니다.
  • 0/6은 예상한 결과입니다. 기준이 무엇을 못 하는지 숫자로 확인했습니다.

고친 뒤 · 내 my-intake

실행한 명령:

uv run tools/flow.py push my-work/intake.json
uv run tools/flow.py test cases/lab01.json -v
uv run tools/flow.py ops

출력 (결과 줄과 ops):

✔ 정상 3건 · 3.9s
✔ 같은 요청을 다시 보내도 중복 없음 · 3.1s
✔ 만들어진 접수 건은 3개 · 0.0s
✔ 잘못된 항목은 그 항목만 거절 · 1.7s
✔ 목록이 아니면 400 · 0.0s
✔ 가끔 실패하는 API · 다시 해 보기로 성공 · 2.4s
Passed 6/6

created tickets: 7
  T-2003  key R-103  소프트웨어/높음  엑셀 라이선스가 만료됐다고 나와요
  T-2004  key R-201  장비/보통  프린터 용지가 걸려요
  T-2005  key R-203  소프트웨어/보통  메일 첨부가 안 열려요
  T-2006  key R-301  계정/높음  사내 위키 로그인이 안 돼요
  T-2007  key R-302  장비/보통  노트북 배터리가 금방 닳아요

고친 뒤 이렇게 나온 까닭

케이스 고치기 전 고친 뒤 무엇이 바꿨나
정상 3건 created = null created: 3 결과를 오늘의 결과 모양으로
같은 요청을 다시 새로 3건 created: 3, duplicates: 3 키 = 요청 id
접수 건 수 6 3 같은 까닭
잘못된 항목 500 200 · 그 항목만 rejected 항목마다 확인
목록이 아님 500 400 (requests must be a list) 입구에서 본문 확인
가끔 실패 500 200 · created: 2 재시도 + 키
  • 두 번째 batch-ok: 응답에 duplicates: 3이 있습니다. 키가 R-101처럼 요청 id라, 데이터 API가 "이미 있는 건"으로 알아보고 새로 만들지 않고 처음 번호를 돌려줬습니다. created: 3은 "세 문의 모두 접수 건이 있다"는 뜻이고, 새로 생긴 것은 없습니다.
  • 잘못된 항목: 빈 글 항목만 rejected(text is required)이고 나머지 2건(R-201 · R-203)은 접수되었습니다. 1.7초로 빨라진 것은 빈 글을 LLM에 보내지 않고 거절했기 때문입니다.
  • 가끔 실패: 두 건 모두 첫 시도가 500이었지만 0.3초 뒤 두 번째 시도에서 성공했습니다. 재시도는 두 건을 모두 다시 보냈지만, 키가 같아 중복은 생기지 않았습니다.
  • ops의 7개 = 정상 3(R-101~103) + 잘못된 항목 요청의 2(R-201 · R-203) + 가끔 실패의 2(R-301 · R-302). 같은 요청을 다시 보낸 것은 하나도 늘리지 않았습니다. 화면에는 마지막 5개만 보입니다. 키가 모두 R-…인 것을 확인합니다.

예제 3 · 고장 종류마다 재시도가 하는 일 (7분, 에이전트 없이)

이 실습의 의도

한 줄로: 예제 2에서 넣은 재시도가 어떤 고장에는 도움이 되고, 어떤 고장에는 시간만 늘리는지 재 봅니다. 워크플로우는 고치지 않고(에이전트 없이), 데이터 API의 고장 종류만 바꿔 가며 같은 my-intake를 부릅니다.

생활 속 예 · 전화 다시 걸기. 통화 중이면 다시 걸면 됩니다(flaky). 상대 전화국이 고장이면 열 번 걸어도 시간만 갑니다(error). 상대가 받았는데 말소리가 늦게 들려 내가 먼저 끊었다면, 상대는 사실 내 말을 다 들었을 수 있습니다(slow). 예제 3은 세 경우에 "다시 걸기"가 각각 무엇을 하는지 재 봅니다.

왜 하나. 재시도를 넣으면 "이제 실패는 해결"이라고 생각하기 쉽습니다. 하지만 고장은 종류가 다릅니다.

고장 스위치 현실에서는 물어볼 것
flaky 네트워크가 잠깐 끊김 · 서버가 한 번 버벅임 재시도가 구해 주나?
error 데이터 API가 죽음 (몇 분 · 몇 시간) 세 번 다시 하면 달라지나? 무엇이 늘어나나?
slow 데이터 API가 느림 (8초) — 우리는 3초만 기다림 우리가 "실패"라고 포기한 요청은 정말 실패했나?

cases/lab01-faults.json이 이 셋을 차례로 켜고, 마지막에 8초를 기다린 뒤 접수 건 목록을 봅니다. slow에서 포기한 3건이 나중에 생겼는지가 이 예제의 핵심 관찰입니다.

끝나면 알게 되는 것. 재시도는 잠깐의 실패에만 약이고, 오래 멈춘 고장에는 다른 그물(Lab 02의 DLQ)이 필요하며, 시간 초과는 "실패"가 아니라 "모름"이라서 멱등 키가 꼭 있어야 한다는 것.

적을 곳. my-work/RUNBOOK.md 1.2 표 (고장 · 걸린 시간 · 결과 · 실제로 생긴 접수 건).

uv run tools/flow.py test cases/lab01-faults.json -v
uv run tools/flow.py ops

이해하기

재시도는 잠깐의 실패에만 약입니다.

고장 무엇 재시도를 하면
flaky (잠깐 끊김) 키마다 첫 시도만 500 두 번째에 성공 → 좋아짐
error (멈춤) 늘 500 세 번 다 실패 → 결과는 같고 시간만 더
slow (느림) 8초 뒤에 답 3초마다 끊고 다시 → 세 번 × 3초 + 간격, 그래도 실패
sequenceDiagram
  participant W as my-intake
  participant A as 데이터 API (slow)
  W->>A: 1번째 (key R-101)
  Note over W: 3초 timeout → 포기
  W->>A: 2번째 (같은 key)
  Note over W: 3초 timeout → 포기
  W->>A: 3번째 (같은 key)
  Note over W: 3초 timeout → failed
  A-->>A: 8초 뒤: 접수 건 R-101 만들기 (늦게)

시간 초과(timeout)는 "실패"가 아니라 "모름"입니다. 3초 안에 답이 오지 않아 포기했지만, 데이터 API는 요청을 받았고 8초 뒤에 접수 건을 만들었습니다. 워크플로우는 failed라고 말하지만 실제로는 만들어져 있습니다. 이때 같은 키로 다시 보내면 duplicate: true로 처음 번호를 받습니다. 멱등 키가 없었다면 두 번째 접수 건이 생깁니다. 이것이 "시간 초과 뒤 재시도"에 멱등 키가 꼭 필요한 까닭입니다 (Lab 03 예제 3에서 재처리로 다시 봄).

재시도는 사용자를 기다리게 합니다. slow에서 사용자는 3초 × 3번 + 간격 + LLM 분류 = 12초쯤 기다리고 failed를 받습니다. "재시도를 늘리면 더 잘 된다"가 아니라, 기다릴 수 있는 시간 안에서 몇 번 할지 정합니다 (바이브 코딩 tretry의 마감 --deadline).

테스트 케이스. 이 파일은 내 워크플로우를 바꾸지 않고 고장만 바꿔 가며 잽니다.

케이스 고장 기대
flaky tickets flaky · batch-two created: 2
error tickets error · batch-ok failed: 3
slow tickets slow · batch-ok failed: 3 (오래 걸림)
늦게 만들어짐 8초 기다린 뒤 /tickets-created/list count: 5 (flaky 2 + 실패라던 3)
다시 보냄 ok · batch-ok created: 3, duplicates: 3

기대 결과. 5/5. 시간은 flaky < error < slow이고, slow는 10초가 넘습니다. 실패라던 3건이 접수 건 목록에 있습니다.

▶ 1단계 · error에서 3번은 좋은가

데이터 API가 한 시간 멈췄다면, 요청마다 세 번씩 보내는 것은 무엇을 늘릴까요? 그 사이 다른 워크플로우도 똑같이 세 번씩 보낸다면? (강의 1단계 "재시도 폭풍")

▶ 2단계 · failed인데 만들어진 건

결과의 failed 3건과 ops의 접수 건 목록을 나란히 봅니다. 사용자에게 "실패"라고 말한 것이 사실은 됐다면, 사용자는 무엇을 할까요? 그때 무엇이 중복을 막나요?

실행 결과 · 고장 종류마다

수업 준비 때(10/29) 강사 노트북에서 예제 2 정답으로 실제로 나온 출력입니다. 시간은 실행마다 조금씩 다르고, ✔ · ✘는 같아야 합니다.

이 예제는 고치지 않습니다. 예제 2에서 고친 my-intake를 그대로 두고, 테스트가 케이스마다 고장 스위치를 바꿔 가며 재시도가 무엇을 하는지 봅니다.

실행한 명령:

uv run tools/flow.py test cases/lab01-faults.json -v
uv run tools/flow.py ops

출력 (결과 줄과 실패 이유만):

✔ flaky · 다시 해 보면 됨 · 2.4s
✔ error · 세 번 해도 실패 · 3.4s
      "reason": "500 - \"{\"error\": \"tickets service is failing (fault switch)\"}\""
✔ slow · 3초씩 세 번 기다리다 실패 · 12.4s
      "reason": "timeout of 3000ms exceeded"
… before: wait 8s
✔ 실패라던 3건이 늦게 만들어짐 (2 + 3) · 0.0s
✔ 같은 요청을 다시 · 멱등 키로 중복 없음 · 2.9s
Passed 5/5

created tickets: 5
  T-2001  key R-301  계정/높음  사내 위키 로그인이 안 돼요
  T-2002  key R-302  장비/보통  노트북 배터리가 금방 닳아요
  T-2003  key R-103  소프트웨어/높음  엑셀 라이선스가 만료됐다고 나와요
  T-2004  key R-102  장비/낮음  새 모니터를 하나 더 받을 수 있을까요?
  T-2005  key R-101  네트워크/높음  VPN이 아침부터 계속 끊겨서 고객 미팅을 못 하고 있

✔는 "재시도가 성공했다"가 아니라 "이 고장에서 기대한 대로 움직였다" 는 뜻입니다. error · slow는 실패하는 것이 기대입니다.

왜 이렇게 나왔나 (케이스마다)

케이스 고장 스위치 시간 결과 까닭
flaky 키마다 첫 시도만 500 2.4s created: 2 첫 시도 500 → 0.3초 뒤 두 번째 성공. 재시도가 도운 경우
error 계속 500 3.4s failed: 3 세 번 모두 500. 재시도가 결과는 못 바꾸고 시간만 더함 (분류 약 2.5초 + 시도 셋과 간격 둘)
slow 응답이 8초 늦음 12.4s failed: 3 시도마다 3초에 끊김(timeout of 3000ms exceeded). 사용자는 12초를 기다리고 실패를 받음
늦게 만들어짐 (8초 기다림) 0.0s 접수 건 5개 실패라던 R-101~103이 사실은 만들어짐
같은 요청을 다시 ok 2.9s duplicates: 3 이미 있는 것을 알아보고 새로 만들지 않음
  • 가장 중요한 줄은 "늦게 만들어짐"입니다. slow에서 n8n은 3초를 기다리다 끊었지만, 데이터 API는 요청을 이미 받았고 8초 뒤에 처리했습니다. 그래서 "실패"라던 R-101 · R-102 · R-103이 ops에 있습니다. 시간 초과는 "안 됨"이 아니라 "모름" 입니다.
  • 시도는 3건 × 3번 = 9번 갔지만, 키가 같아 접수 건은 3개만 생겼습니다. 키가 실행 번호였다면 9개가 될 수도 있었습니다.
  • ops의 5개 = flaky의 2(R-301 · R-302) + 늦게 만들어진 3(R-101~103). 마지막 "같은 요청을 다시"는 늘리지 않았습니다.
  • 이 표의 시간 · 결과 · 실제로 생긴 접수 건을 my-work/RUNBOOK.md 1.2 표에 적습니다. failed가 된 요청을 어디에 모을지는 Lab 02에서 다룹니다.

과제 1 · LLM이 멈춰도 접수는 (8분)

이 실습의 의도

한 줄로: LLM(OpenAI)이 멈춰도 문의 접수는 계속 되게 합니다. 분류만 "미분류"로 줄여서.

생활 속 예 · 카드 단말기가 고장 난 편의점. 단말기가 안 된다고 문을 닫지 않고 "오늘은 현금만"으로 판매를 계속합니다. 과제 1은 LLM(단말기)이 멈춰도 접수(판매)는 "미분류"로 계속하게 하는 실습입니다.

상황. 지금 my-intake는 분류(LLM) → 접수 순서입니다. 키가 잘못되거나 OpenAI에 장애가 나면 분류 노드에서 멈추고 500이 나며, 접수 건은 하나도 생기지 않습니다. 그 사이 들어온 문의는 모두 사라집니다. 분류는 나중에 사람이 해도 되지만, 접수되지 않은 문의는 아무도 모릅니다.

어떻게 시험하나. 실제 OpenAI 장애를 기다릴 수는 없으니 "LLM이 멈춘 상태"를 만듭니다. cases/lab01-challenge.json의 한 케이스가 llm down으로 n8n의 OpenAI Credential을 일부러 틀린 키로 바꾸고, 끝나면 llm up으로 진짜 키를 되돌립니다.

볼 것 고치기 전 고친 뒤 기대
LLM이 멈췄을 때 답 직접 확인 200, created: 3, unclassified: 3
접수 건의 분류 직접 확인 미분류
평소 요청 분류됨 그대로 분류됨

예제 2와의 관계. 예제 2에서 접수 건 만들기 노드에 쓴 "실패해도 계속"을 이번에는 분류 노드에 씁니다. 같은 설정, 다른 노드입니다. 그리고 분류 결과를 읽는 Code가 "실패한 분류"를 알아보고 대신 넣을 값을 정합니다.

끝나면 알게 되는 것. 핵심 일(접수)과 덧붙인 일(분류)을 나누고, 덧붙인 일이 실패하면 줄여서라도 핵심 일을 끝내는 법(우아한 성능 저하), 그리고 줄인 양(unclassified)을 숨기지 않는 법.

팀장이 요청했습니다.

"AI 분류가 안 된다고 문의 접수까지 막히면 안 돼요. 분류는 나중에 사람이 해도 되니, 일단 '미분류'로 받아 주세요."

uv run tools/flow.py test cases/lab01-challenge.json -v      # 고치기 전
조건 값
LLM이 실패하면 category: 미분류, priority: 보통으로 접수 (결과의 unclassified에 셈)
기대 결과 cases/lab01-challenge.json (케이스 하나가 llm down, 끝나면 llm up)

이해하기

핵심 일과 덧붙인 일. 접수는 이 워크플로우의 핵심 일입니다. 분류는 접수를 돕는 덧붙인 일입니다. 덧붙인 일이 실패했다고 핵심 일까지 멈추면, LLM이 멈춘 동안 들어온 문의가 모두 사라집니다. 덧붙인 일은 줄여서라도 핵심 일을 끝내게 합니다 (graceful degradation, 우아한 성능 저하).

flowchart LR
  subgraph 지금
    C1{{"분류 (LLM 멈춤)"}} -->|오류| X1(["500 · 접수 0"])
  end
  subgraph 바꾼 뒤
    C2{{"분류 (LLM 멈춤)<br/>실패해도 계속"}} -->|error| P2["Parse: 미분류 · 보통"] --> T2["접수"] --> O2(["200 · created 3<br/>unclassified 3"])
  end

72일차와 무엇이 다른가. 72일차에 LLM이 멈췄을 때는 FAQ 원문으로 "간단 답변"을 했습니다. 오늘은 "미분류로 접수"입니다. 둘 다 같은 생각입니다: LLM 없이도 할 수 있는 몫은 한다. 그리고 얼마나 줄였는지를 숨기지 않습니다 — unclassified 수가 있어야 나중에 사람이 몇 건을 분류해야 하는지 압니다.

테스트 케이스.

케이스 하는 것 기대
평소 batch-two created: 2, unclassified: 0
LLM 멈춤 llm down · batch-ok · llm up created: 3, unclassified: 3, failed: 0
접수 건 확인 /tickets-created/list 분류에 미분류가 있음

기대 결과. 고치기 전 1/3 (LLM 멈춤에서 500), 고친 뒤 3/3. lab01도 그대로 6/6.

먼저 생각해 보기 (2분): 어떤 일은 LLM 없이도 해야 하고, 어떤 일은 LLM 없이는 할 수 없나요? 오늘 문의 접수에서 LLM이 없으면 무엇을 잃나요?

▶ 1단계 · 실패한 분류를 알아보기

분류 노드가 실패한 항목은 답 글자(output) 대신 error를 가집니다. 노드가 실패한 항목을 다음으로 넘기게 설정하고, 다음 Code가 error를 보고 "미분류"로 정하게 합니다. LLM이 JSON이 아닌 글을 돌려준 경우도 같이 생각합니다.

▶ 빈칸 채우기 · 요청문(프롬프트) 뼈대

요청문에 꼭 들어갈 것: ① 고칠 파일 ② 분류 노드가 실패해도 계속 ③ 실패를 알아보는 법과 대신 넣을 값 ④ 결과에 셀 칸 ⑤ 기대 결과. 빈칸 4개를 채웁니다.

my-work/intake.json 에서 Classify(AI Agent) 노드가 실패해도 워크플로우가 멈추지 않게 onError ____① 로 해 줘.
Parse 에서 그 항목에 ____② 가 있거나 output 이 JSON 이 아니면 category "____③", priority "보통" 으로.
결과의 ____④ 에 미분류로 접수한 수를 세어 줘. 나머지는 그대로.
기대 결과 cases/lab01-challenge.json, cases/lab01.json 도 그대로 통과.

빈칸 힌트:

  • ① 예제 2의 Create ticket에 쓴 것과 같은 값입니다 (AGENTS.md 5절 AI Agent).
  • ② 실패한 항목이 답 글자 대신 가지는 칸입니다 (▶ 1단계).
  • ③ 팀장의 말에 있습니다.
  • ④ AGENTS.md 3절 결과 모양에서 미분류 수를 세는 칸입니다.
uv run tools/flow.py push my-work/intake.json
uv run tools/flow.py test cases/lab01-challenge.json -v
uv run tools/flow.py test cases/lab01.json

실행 결과 · LLM이 멈춰도 접수

수업 준비 때(10/29) 강사 노트북에서 실제로 나온 출력입니다.

고치기 전 · 예제 2까지 고친 my-intake

실행한 명령:

uv run tools/flow.py test cases/lab01-challenge.json -v

출력 (결과 줄만):

✔ 평소에는 분류됨 · 2.1s
✔ OpenAI credential: broken key (drill). Restore with: uv run tools/flow.py llm up
✘ LLM이 멈추면 미분류로 접수 · 0.8s · status 500 (expected 200); created = null (expected 3); unclassified = null (expected 3); failed = null (expected 0)
✔ OpenAI credential: real key from .env restored
✘ 미분류도 접수 건으로 · 0.0s · tickets has none of ['미분류']
Passed 1/3

왜 이렇게 나왔나 (고치기 전)

  • 가운데 OpenAI credential 두 줄은 케이스가 아니라 테스트가 대신 해 준 준비와 뒷정리입니다. 두 번째 케이스 전에 llm down(일부러 틀린 키), 끝난 뒤 llm up(.env의 진짜 키로 되돌림)을 했습니다.
  • LLM이 멈추면 ✘ 0.8초에 500: 키가 틀려 OpenAI가 호출을 거절하자 분류 노드가 오류로 멈췄고, n8n은 Error in workflow를 돌려줬습니다. 분류 뒤에 있는 접수 건 만들기까지 가지 못했으니 세 문의 모두 사라졌습니다. 0.8초로 빠른 것은 LLM이 답을 만들기 전에 거절했기 때문입니다.
  • 미분류 접수 건 ✘: 접수 건 목록에 미분류가 하나도 없습니다. 위 케이스에서 아무것도 만들지 못했으니 당연합니다.
  • 1/3은 예상한 결과입니다. 예제 2의 재시도 · 키는 데이터 API의 실패만 다뤘고, LLM의 실패는 아직 아무도 다루지 않습니다.

고친 뒤

실행한 명령:

uv run tools/flow.py push my-work/intake.json
uv run tools/flow.py test cases/lab01-challenge.json -v
uv run tools/flow.py test cases/lab01.json

출력 (결과 줄만):

✔ 평소에는 분류됨 · 3.5s
✔ LLM이 멈추면 미분류로 접수 · 0.9s
✔ 미분류도 접수 건으로 · 0.0s
Passed 3/3
(lab01 6/6)

고친 뒤 이렇게 나온 까닭

케이스 고치기 전 고친 뒤 무엇이 바꿨나
LLM이 멈춤 500 · 접수 0 200 · created: 3, unclassified: 3 분류 노드가 멈추지 않고 오류를 다음으로 넘김
미분류 접수 건 없음 미분류 3건 Parse가 오류를 보고 미분류 · 보통으로 정함
  • 분류는 여전히 실패했습니다 (키가 틀렸으니까요). 달라진 것은 실패를 받은 뒤입니다. 분류 노드가 워크플로우를 멈추지 않고 error를 넘겼고, Parse가 그것을 보고 분류를 미분류, 우선순위를 보통으로 정해 접수했습니다. 문의는 잃지 않고, 사람이 나중에 분류하면 됩니다.
  • 0.9초로 오히려 빨랐습니다. LLM이 거절한 뒤 기다리지 않고 바로 접수로 갔기 때문입니다. 이것이 우아한 저하(전부 멈추는 대신 덜 좋은 결과라도 내기)입니다.
  • unclassified: 3이 결과에 따로 있으니, 사용자나 운영자는 "분류가 안 된 것이 3건"이라는 것을 압니다. 조용히 넘어간 것이 아닙니다.
  • llm up으로 진짜 키를 되돌린 뒤 lab01 6/6: 평소 분류와 예제 2의 규칙은 그대로입니다.

과제 2 · 요청 안의 규칙 (10분)

이 실습의 의도

한 줄로: 비싼 일(LLM 분류 · 접수)을 하기 전에 요청 전체를 보고, 규칙에 맞지 않는 것을 입구에서 막습니다.

생활 속 예 · 접수 창구 직원. 같은 서류를 두 번 내밀면 "방금 접수하셨는데요" 하고 그 자리에서 돌려주고, 서류를 수백 장 한꺼번에 내밀면 "한 번에 10장까지만 받아요"라고 합니다. 안쪽 사무실까지 들고 가서 처리하지 않습니다. 과제 2는 워크플로우 입구에 이 창구 직원을 세우는 실습입니다.

상황 둘.

상황 지금은 바라는 것
엑셀에서 복사하다 같은 줄(같은 id)이 두 번 둘 다 분류 · 접수 시도. 키 덕분에 접수 건은 하나지만 답은 "둘 다 접수" 두 번째 줄만 rejected (duplicate id in this request)
한 번에 수백 건 모두 처리, 그동안 n8n이 붙잡힘 10건이 넘으면 요청 전체를 400으로 거절, 하나도 처리하지 않음

예제 2와의 차이. 예제 2의 항목 확인은 "이 한 줄에 id · user · text가 있나"였습니다. 이번 규칙은 다른 줄과 비교해야(같은 id) 또는 요청 전체를 봐야(몇 건인가) 알 수 있습니다.

왜 멱등 키로 충분하지 않나. 교안 1단계 8절의 "멱등 키가 있어도 입구 규칙이 필요한 까닭"을 먼저 읽습니다. 키는 중복 접수 건이라는 피해를 막고, 입구 규칙은 쓸데없는 분류와 틀린 숫자라는 일을 막습니다.

시험하는 것. batch-dup-id(R-401 두 번 · R-402), batch-eleven(11건), 11건이 400일 때 접수 건이 정말 하나도 안 생겼는지, 평소 요청은 그대로인지.

끝나면 알게 되는 것. 항목 규칙과 요청 전체 규칙의 차이, 상한을 넘으면 일부가 아니라 전체를 거절하는 까닭, 입구에서 막으면 얼마나 빨라지는지 (11건을 다 처리할 때와 비교).

"어떤 팀은 엑셀에서 복사하다 같은 줄을 두 번 넣어 보내요. 그리고 한 번에 수백 건을 보내서 몇 분씩 걸린 적도 있어요. 같은 요청 안에 같은 번호가 또 있으면 두 번째는 받지 말고, 한 번에는 10건까지만 받아 주세요."

uv run tools/flow.py test cases/lab01-challenge2.json -v     # 고치기 전
조건 값
같은 id가 또 두 번째부터 rejected, 이유 duplicate id in this request
10건 넘음 HTTP 400 {"error": "too many requests (max 10)"}, 하나도 처리하지 않음
기대 결과 cases/lab01-challenge2.json, lab01 · lab01-challenge도 그대로

이해하기

입구에서 막기 (validate at the edge). 잘못된 요청은 비싼 일(LLM · API)을 하기 전에 막는 것이 가장 쌉니다. 예제 2의 항목 확인이 "항목 하나"의 규칙이었다면, 이번에는 요청 전체의 규칙입니다.

규칙 어느 수준 어기면
id · user · text가 있어야 항목 그 항목만 거절 (나머지는 처리)
같은 요청 안에 같은 id가 또 항목 (두 번째) 두 번째만 거절
한 번에 10건까지 요청 전체 요청 전체 400 (하나도 처리하지 않음)

왜 같은 id는 멱등 키로 충분하지 않나. 같은 id 두 개는 키가 같아 접수 건은 하나만 생깁니다. 하지만 결과에는 created 2건(하나는 duplicate: true)으로 나와, 사용자는 두 건이 접수된 줄 압니다. 그리고 LLM 분류를 두 번 합니다. 입구에서 두 번째를 거절하면 결과가 정직하고 비용도 덜 듭니다.

왜 상한인가. 상한이 없으면 한 요청이 몇 분씩 n8n을 붙잡고, 그동안 다른 요청이 기다립니다. 많은 API가 한 번에 받을 수 있는 수를 정해 둡니다 (예: AWS SQS의 묶음 보내기는 한 번에 10개). 상한을 넘으면 일부만 처리하지 않고 전체를 거절합니다. 일부만 처리하면 어디까지 됐는지 사용자가 알기 어렵기 때문입니다.

flowchart LR
  Q(["requests"]) --> L{"목록이고<br/>10건 이하?"}
  L -->|아니오| E(["400 · 이유"])
  L -->|예| V{"항목마다:<br/>빠진 것 · 앞에 나온 id?"}
  V -->|맞음| P["분류 · 접수"]
  V -->|아님| R["rejected · 이유"]

테스트 케이스.

케이스 보내는 것 기대
같은 id 두 번 batch-dup-id (R-401 둘 · R-402) created: 2, rejected: 1, duplicates: 0, 이유 duplicate id
11건 batch-eleven 400, error에 too many
400이면 아무것도 /tickets-created/list count: 2
평소 batch-ok created: 3, rejected: 0

기대 결과. 고치기 전 1/4, 고친 뒤 4/4. 11건은 LLM을 부르지 않아 빠르게 끝납니다.

▶ 1단계 · 어디에 넣나

"목록인가"를 보는 곳이 이미 있습니다 (예제 2의 400). 상한은 그 자리에, 같은 id는 항목을 확인하는 Code에서 앞에서 본 id를 기억해 둡니다. 400의 이유가 둘(목록이 아님 · 너무 많음)이 되므로 응답 글도 나눠야 합니다.

▶ 빈칸 채우기 · 요청문(프롬프트) 뼈대

요청문에 꼭 들어갈 것: ① 요청 전체 규칙과 응답 ② 항목 규칙과 이유 ③ 어느 것이 거절되나 ④ 이미 있는 규칙은 그대로 ⑤ 기대 결과. 빈칸 4개를 채웁니다.

my-work/intake.json 에서 requests 가 목록이어도 ____① 건이 넘으면 HTTP 400 {"error": "too many requests (max 10)"} 로, 하나도 처리하지 말아 줘.
목록이 아닐 때의 400 글은 그대로 "requests must be a list".
Check items 에서 같은 요청 안에 같은 id 가 또 나오면 ____② 번째부터 rejected, 이유 "____③".
앞에서 본 id 는 ____④ 에 기억해 둔다. 나머지 규칙(빠진 칸 거절 · 재시도 · 키 · 미분류)은 그대로.
기대 결과 cases/lab01-challenge2.json, cases/lab01.json · lab01-challenge.json 도 통과.

빈칸 힌트:

  • ① 팀장의 말에 있습니다. "넘으면"이므로 그 수 자체는 받습니다.
  • ② 처음 나온 것은 받습니다.
  • ③ 위 조건 표의 이유 글 그대로입니다 (테스트가 duplicate id를 찾음).
  • ④ 이미 본 것을 빨리 찾을 수 있는 JavaScript 모음입니다 (목록도 되지만 더 알맞은 것).
uv run tools/flow.py push my-work/intake.json
uv run tools/flow.py test cases/lab01-challenge2.json -v
uv run tools/flow.py test cases/lab01.json
uv run tools/flow.py test cases/lab01-challenge.json
uv run tools/flow.py ops --reset

실행 결과 · 요청 안의 규칙

수업 준비 때(10/29) 강사 노트북에서 실제로 나온 출력입니다.

고치기 전 · 과제 1까지 고친 my-intake

실행한 명령:

uv run tools/flow.py test cases/lab01-challenge2.json -v

출력 (결과 줄만):

✘ 같은 id가 두 번이면 두 번째는 거절 · 2.8s · created = 3 (expected 2); rejected = 0 (expected 1); duplicates = 1 (expected 0); results has none of ['duplicate id']
✘ 11건은 400 (한 번에 10건까지) · 11.0s · status 200 (expected 400); error has none of ['too many']
✘ 400이면 하나도 만들지 않음 · 0.0s · count = 13 (expected 2)
✔ 평소 요청은 그대로 · 3.2s
Passed 1/4

왜 이렇게 나왔나 (고치기 전)

  • 같은 id 두 번 ✘ created = 3, duplicates = 1: 요청 안에 R-401이 두 번 있습니다. 멱등 키 덕분에 접수 건은 둘(R-401 · R-402)만 생겼습니다. 하지만 결과는 "세 건을 접수했다"고 말하고, 두 번째 R-401도 LLM 분류를 한 번 더 했습니다. 보낸 쪽의 실수(같은 id를 두 번 넣음)를 아무도 알려 주지 않습니다.
  • 11건 ✘ 200 · 11.0초: 한 번에 받을 수 있는 수의 상한이 없어 11건을 모두 분류 · 접수했습니다. 11초 동안 n8n이 이 요청 하나에 붙잡혀 있었습니다. 100건, 1000건이 와도 그대로 받습니다.
  • 접수 건 ✘ count = 13: 2(같은 id 케이스) + 11(이번 케이스). 막아야 할 요청이 접수 건을 11개 만들었습니다.
  • 평소 요청 ✔: 정상 3건은 원래 잘 됩니다. 이 케이스는 고친 뒤에도 ✔여야 합니다.
  • 1/4는 예상한 결과입니다. 키는 "같은 건을 두 번 만들기"는 막지만, "이상한 요청을 받기"는 막지 않습니다 (교안 1단계 8절).

고친 뒤

실행한 명령:

uv run tools/flow.py push my-work/intake.json
uv run tools/flow.py test cases/lab01-challenge2.json -v
uv run tools/flow.py test cases/lab01.json
uv run tools/flow.py test cases/lab01-challenge.json
uv run tools/flow.py ops --reset

출력 (결과 줄만):

✔ 같은 id가 두 번이면 두 번째는 거절 · 3.4s
✔ 11건은 400 (한 번에 10건까지) · 0.2s
✔ 400이면 하나도 만들지 않음 · 0.0s
✔ 평소 요청은 그대로 · 3.7s
Passed 4/4
(lab01 6/6 · lab01-challenge 3/3)

고친 뒤 이렇게 나온 까닭

케이스 고치기 전 고친 뒤 무엇이 바꿨나
같은 id 두 번 created: 3, duplicates: 1 created: 2, rejected: 1 (duplicate id …) 입구에서 요청 안의 id를 셈
11건 200 · 11.0초 400 · 0.2초 (too many requests (max 10)) 입구에서 건수를 셈
접수 건 수 13 2 400이면 아무것도 하지 않음
평소 요청 ✔ ✔ 정상 요청은 막지 않음
  • 같은 id: 두 번째 R-401은 분류하지 않고 rejected로, 이유는 duplicate id …로 시작합니다. 결과가 실제와 같아졌습니다 (접수 건 2 = created: 2). 보낸 쪽은 자기 실수를 바로 압니다.
  • 11건 0.2초: LLM과 데이터 API를 부르기 전에 입구에서 세고 400을 돌려줬습니다. 11.0초 → 0.2초. 하나도 만들지 않았으니 접수 건은 2개 그대로입니다. 400이라 보낸 쪽은 "내가 나눠서 다시 보내야 한다"는 것을 압니다.
  • 키와 입구 규칙은 둘 다 필요합니다. 키는 같은 건이 두 번 생기는 피해를 막고, 입구 규칙은 하지 말아야 할 일을 시작도 하지 않게 막습니다.
  • lab01 6/6 · lab01-challenge 3/3: 앞의 규칙(항목 확인 · 재시도 · 키 · 미분류)은 그대로입니다.

막혔을 때

증상 할 일
거절한 항목이 결과에 없음 거절 갈래가 모으는 노드(Merge)의 두 번째 입력(index: 1)에 이어졌는지
flaky에서 실패 접수 건 만들기 노드에 재시도 설정이 있는지 (parameters 밖, 노드 바로 아래). check가 안에 쓴 것을 오류로 알려 줌
다시 보냈더니 duplicates가 0 key가 요청의 id인지 ($execution.id가 아님)
결과의 순서 · 이유가 엉킴 Code에서 앞 노드의 .all()과 같은 순서로 맞추는지
테스트 뒤에도 LLM이 모두 실패 uv run tools/flow.py llm up
slow 케이스가 너무 오래 정상 (약 12초). 접수 건 만들기의 timeout이 3초인지
11건이 400인데 이유가 must be a list 400 응답 글을 두 경우로 나눴는지
계속 안 됨 cp reference/lab01-example2-intake.json my-work/intake.json 후 push (예제 2까지)

확인

  • 예제 1: 기준의 500 · 이미 만들어진 건 · 중복을 RUNBOOK.md에 적었다.
  • 예제 2: cases/lab01.json 6/6 (고치기 전 0/6).
  • 예제 3: cases/lab01-faults.json 5/5, 고장마다 시간 · 늦게 생긴 접수 건을 적었다.
  • 과제 1: cases/lab01-challenge.json 3/3, lab01도 통과.
  • 과제 2: cases/lab01-challenge2.json 4/4, lab01 · lab01-challenge도 통과.
  • my-work/PROMPTS.md, 끝나면 ops --reset.