72일차 자료 추가(답 제외)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mcchaeandClaude Opus 5.5 committed 2026-10-25 22:57:12 +09:00
1 parent aaf77d4e18
commit febde9757c
63 files changed
+12260

No files matched your search

+441
View File
@@ -0,0 +1,441 @@
# 72일차 개요 · Workflow 실행 흐름 최적화와 운영 관점 개선
> 노코드 멀티 에이전트 워크플로우 설계 및 운영 · 2026-10-26
> 30장 · 각 장은 가로줄(`---`)로 나뉩니다. 1교시에는 1~6장을 보고, 나머지 장은 각 단계 강의(1 · 3 · 5교시) 때 함께 띄웁니다.
> 숫자는 모두 강사 노트북 실측입니다 (65일차 n8n 2.42.6, OpenAI `gpt-4.1-mini`, 10/18 수업 준비 · 10/23~24 다시 잼).
| 장 | 범위 | 띄우는 때 |
| --- | --- | --- |
| 1~6 | 표지 · 목표 · 시간표 · 오늘 고칠 것 · 일하는 순서 · 실습 15개 | 1교시 시작 |
| 7~14 | 1단계 · 재기와 병목 | 1교시 강의 |
| 15~21 | 2단계 · 실행 흐름 최적화 | 3교시 강의 |
| 22~28 | 3단계 · 운영 관점의 안정성 | 5교시 강의 |
| 29~30 | 바이브 코딩 · 정리와 예고 (8교시 35분 정리는 [04-STAGE4-WRAPUP](04-STAGE4-WRAPUP.md)) | 7교시 · 8교시 |
---
## 1 / 30 · 표지
```mermaid
flowchart LR
M["재기"] --> F["병목"] --> C["한 가지 바꾸기"] --> R["다시 재기"]
R --> M
classDef d fill:#0F3D3E,stroke:#0F3D3E,color:#fff
classDef o fill:#F2A541,stroke:#F2A541,color:#fff
class M,R d
class F,C o
```
**72일차**
# Workflow 실행 흐름 최적화와 운영 관점 개선
빠르게, 그리고 멈춰도 답하게 · 재고 나서 고친다
<sub>노코드 멀티 에이전트 워크플로우 설계 및 운영 · 2026-10-26</sub>
---
## 2 / 30 · 오늘 할 수 있게 될 세 가지
| | 목표 | 내용 |
| :-: | --- | --- |
| **①** | **재서 병목을 찾는다** | p50 · p95 · 첫 호출 · `bench` · 단계별 `timings` · 종류별 재기 · 시간 예산 |
| **②** | **품질은 그대로 빠르게 만든다** | 규칙 먼저 · 캐시 · 적중률 · 캐시 키 설계 · 검토를 검사로 · 잃은 것 |
| **③** | **멈춰도 정해진 답을 한다** | timeout · 재시도 · 오류 출력 · LLM 없이 답 · 부분 고장 · 차단기 · 상태 점검 |
---
## 3 / 30 · 오늘의 시간표
| 교시 | 시간 | 무엇 |
| --- | --- | --- |
| 1 | 09:00~09:50 | 개요 · **1단계 강의** 재기와 병목 |
| 2 | 10:00~10:50 | **Lab 01** 실습 5개 |
| 3 | 11:00~11:50 | **2단계 강의** 실행 흐름 최적화 |
| 4 | 12:00~12:50 | **Lab 02** 실습 5개 |
| - | 13:00~14:00 | 점심 |
| 5 | 14:00~14:50 | **3단계 강의** 운영 관점의 안정성 |
| 6 | 15:00~15:50 | **Lab 03** 실습 5개 |
| 7 | 16:00~16:50 | 바이브 코딩 `tbench` (요청서 → TODO → SPEC → IMP-PLAN → 만들기 → 설치) |
| 8 | 17:00~17:50 | 발표 · **4단계 정리** (의미 · 효과 · 실전 응용) · 73일차 예고 |
> 강의 한 시간, 실습 한 시간. 실습은 Lab마다 **예제 3개 + 과제 2개**.
---
## 4 / 30 · 오늘 고칠 것: 느린 실습 워크플로우 (IT 헬프데스크)
```mermaid
flowchart LR
W(["질문"]) --> C{{"분류 LLM"}} --> D["FAQ · 접수"] --> A{{"답 LLM"}} --> R{{"검토 LLM"}} --> S(["답"])
```
| `base-helpdesk` (VPN 질문) | 10/18 | 10/24 |
| --- | --- | --- |
| LLM 호출 | 3번 | 3번 |
| push 직후 첫 호출 | 6.5초 | 4.4초 |
| 그 뒤 5번 | p50 2.61초 · p95 2.83초 | p50 2.69초 · p95 2.98초 |
| FAQ가 8초 느릴 때 | 9.0초 기다림 | 11.5초 |
| FAQ · LLM 오류 | HTTP 500 | HTTP 500 |
---
## 5 / 30 · 일하는 순서 (오늘은 고치기)
```mermaid
sequenceDiagram
participant 나
participant 에이전트 as ① CLI 에이전트
participant 도구 as ② flow.py
나->>도구: bench · call -v (재기)
나->>에이전트: 요청문 (바꿀 것 하나 · 그대로 둘 것 · 기대 결과)
에이전트-->>나: my-work/helpdesk.json · check ✔
나->>도구: push · test -v · bench (다시 재기 + 품질)
나->>도구: fault · llm down (고장 연습)
```
---
## 6 / 30 · 오늘의 실습 15개
| | 예제 1 | 예제 2 | 예제 3 | 과제 1 | 과제 2 |
| --- | --- | --- | --- | --- | --- |
| **Lab 01** 재기 | bench 기준 | 단계별 `timings` | 종류별 재기 · LLM 몫 | FAQ · 접수 나누기 · `slowest_step` | 시간 예산 `over_budget` |
| **Lab 02** 빠르게 | 규칙 먼저 | 질문 캐시 | 적중률 재기 | 검토 → 검사 (LLM 한 번) | FAQ 번호 캐시 `cache_by` |
| **Lab 03** 안정성 | timeout · 재시도 | LLM 없이 답 | 접수 API만 멈춤 | 상태 점검 `my-health` | 차단기 `breaker-faq` |
> 예제 3은 대부분 **재기만** 합니다 (Lab 03 예제 3은 만들기). 과제는 팀장의 말로 옵니다.
---
## 7 / 30 · 1단계 · 재기 전에 고치지 않기
| 짐작 | 실측 (10/24) |
| --- | --- |
| "데이터 API가 느릴 것" | FAQ · 접수 조회 합쳐 **18~43ms** |
| "IT 밖 질문은 금방 끝날 것" | 점심 메뉴도 **2.48초** (LLM 세 번) |
| "push 하면 바로 평소 속도" | 첫 호출 **4.4초** |
> 재기 → 병목 → **한 가지** 바꾸기 → 다시 재기 (품질도)
---
## 8 / 30 · 1단계 · p50 · p95 손으로
```text
1.1 1.2 1.2 1.3 1.3 1.4 1.4 1.5 1.6 9.8 (초, 예시)
```
| 평균 | p50 | p95 | max |
| --- | --- | --- | --- |
| 2.18초 | 1.3초 | 9.8초 | 9.8초 |
> 평균 2초는 **아무에게도 맞지 않는다.** 보통(p50)과 느린 쪽(p95)을 함께.
---
## 9 / 30 · 1단계 · 첫 호출과 같은 날 비교
| 날 | 첫 호출 | p50 | p95 |
| --- | --- | --- | --- |
| 10/18 | 6.5초 | 2.61초 | 2.83초 |
| 10/23 | 6.3초 | 3.14초 | 3.22초 |
| 10/24 | 4.4초 | 2.69초 | 2.98초 |
> 같은 워크플로우도 날마다 0.5초 다르다 → **바꾸기 전 · 후는 같은 날**. 첫 호출은 따로 (워밍업).
---
## 10 / 30 · 1단계 · 어디서 재나
| 어디서 | 보이는 것 | 한계 |
| --- | --- | --- |
| `flow.py bench` | 사용자가 기다린 시간, p50 · p95, 실패 | 안은 모름 |
| Executions | 노드마다 시간 | 한 번씩 눈으로 |
| 결과의 `timings` | 단계마다 ms (매번, 테스트로도) | 직접 만듦 (2교시 Lab 01 예제 2) |
---
## 11 / 30 · 1단계 · 단계별 시간 (Lab 01 예제 2)
```mermaid
flowchart LR
S["t0"] --> C{{"classify"}} --> T1["t1"] --> D["data"] --> T2["t2"] --> A{{"answer"}} --> T3["t3"] --> R{{"review"}} --> T4["t4"]
```
| classify | data | answer | review | total |
| --- | --- | --- | --- | --- |
| 616 ms | 18 ms | 1124 ms | 920 ms | 2698 ms |
> 10/24 VPN 한 번. `steps` · `llm_calls`도 **워크플로우가** 남긴다.
---
## 12 / 30 · 1단계 · 종류별로 재기 (Lab 01 예제 3)
| 질문 | p50 | total | LLM 몫 |
| --- | --- | --- | --- |
| FAQ (VPN) | 2.91초 | 2698 ms | 98.6% |
| 접수 (T-1002) | 2.99초 | 2647 ms | 97.8% |
| IT 밖 (점심) | 2.48초 | 2482 ms | 98.7% |
> 세 종류 모두 **LLM이 98%**. 병목은 HTTP가 아니라 LLM 호출 수.
---
## 13 / 30 · 1단계 · 어림 계산과 가장 느린 단계 (Lab 01 과제 1)
| 없애면 | 남는 시간 | 줄어듦 |
| --- | --- | --- |
| 데이터 조회 | 2.68초 | 0.7% |
| 분류 LLM | 2.08초 | 23% |
| 검토 LLM | 1.78초 | 34% |
| 분류 + 검토 | 1.16초 | 57% |
`slowest_step`: 10/24 네 번 중 answer 2 · review 1 · classify 1 (첫 호출) — 실행마다 바뀐다.
---
## 14 / 30 · 1단계 · 시간 예산 (Lab 01 과제 2)
| 예산 | `total_ms` | `llm_ms` | `over_budget` |
| --- | --- | --- | --- |
| 500 | 2609 | 2563 | true |
| 3000 (기본) | 3023 | 2973 | **true** |
| 3000 (접수) | 2904 | 2858 | false |
> 예산은 **사용자 쪽에서** 정한다. 넘어도 답은 보내고 **기록**한다 → 모으면 목표 수준(SLO).
---
## 15 / 30 · 2단계 · LLM 호출 줄이기
```mermaid
flowchart LR
Q(["질문"]) --> K{"캐시?"} -->|있음| Z(["0.05초"])
K -->|없음| D["데이터"] --> R{"규칙?"} -->|예| A{{"답 1번"}}
R -->|아니오| C{{"분류"}} --> A
A --> V["검사 (Code)"]
```
| LLM | 꼭 LLM이어야 하나 |
| --- | --- |
| 분류 | 번호 · FAQ 결과로 **규칙이 대부분** |
| 답 | 예 |
| 검토 | 말투는 지시문, 빈 답 · 길이 · 한국어는 **코드** |
---
## 16 / 30 · 2단계 · 규칙 먼저 (Lab 02 예제 1)
```mermaid
flowchart LR
D["FAQ · 접수 조회"] --> R{"규칙"}
R -->|"T-1234"| T["ticket"]
R -->|"FAQ 결과"| F["faq"]
R -->|"모름"| L{{"분류 LLM"}}
```
> 규칙을 쓰려면 **데이터 조회가 먼저** (20~40ms라 손해 없음). 규칙도 테스트한다.
---
## 17 / 30 · 2단계 · 캐시해도 되나 (Lab 02 예제 2)
| 답 | 캐시 | 까닭 |
| --- | --- | --- |
| VPN FAQ 답 | 예 | 같은 질문이면 같은 답 |
| "T-1002 상태" | **아니오** | 상태가 바뀜 |
| 본인 접수 건 | **아니오** | 남의 답이 나갈 수 있음 |
> 같은 질문 두 번째 **0.05초**, `llm_calls: 0` (10/24). 캐시는 **테스트도** 바꾼다 → `before`에 비우기, `no_cache`.
---
## 18 / 30 · 2단계 · 적중률 (Lab 02 예제 3)
| 적중률 | 평균 (어림, 0.05초 · 2.0초) |
| --- | --- |
| 0% | 2.00초 |
| 10% | 1.81초 |
| 30% | 1.42초 |
| 60% | 0.83초 |
> 캐시가 빨라도 **적중률이 낮으면 평균은 그대로**. 사람들은 같은 것을 다른 말로 묻는다.
---
## 19 / 30 · 2단계 · 캐시 키 설계 (Lab 02 과제 2)
| 키 | 적중 | 위험 |
| --- | --- | --- |
| 질문을 고친 글자 | 낮음 | 거의 없음 |
| **FAQ 번호** `faq-FAQ-003` | 높음 | 답이 질문에 맞춰지지 않음 |
| 뜻 (임베딩) | 높음 | 다른 질문에 같은 답 (오늘 안 함) |
| 10/24 | 결과 |
| --- | --- |
| "VPN 연결이 계속 끊어지는데요" (다른 말) | `cache_by: faq`, LLM 0번, **0.1초** (전에는 1.2초) |
---
## 20 / 30 · 2단계 · 검토를 검사로 (Lab 02 과제 1)
| 날 | LLM 2번 | LLM 1번 |
| --- | --- | --- |
| 10/18 | 2.01초 | **1.15초** |
| 10/23 | 2.39초 | **1.32초** |
| 10/24 | 2.22초 | **1.12초** |
(캐시 없이 VPN p50) · 검토 LLM은 10/24에 "처리 중"을 "승인 대기 중"으로 바꿔 써서 테스트를 깼다.
---
## 21 / 30 · 2단계 · 빨라진 만큼 잃은 것
| 바꾼 것 | 잃은 것 |
| --- | --- |
| 규칙 먼저 | 규칙이 틀리면 엉뚱한 분류 |
| 질문 캐시 | 오래된 답 · 남의 답 → **접수 현황은 캐시 안 함** |
| FAQ 번호 캐시 | 질문에 맞춘 답 |
| 검토 → 검사 | 뜻을 보는 검토 |
> 품질 테스트는 **답의 내용**을 본다. 구조(`llm_calls: 3`)를 고정한 테스트는 최적화를 막는다.
---
## 22 / 30 · 3단계 · 멈추는 방식
| 고장 | 고치기 전 |
| --- | --- |
| FAQ 느림 (8초) | 9.0 · 13.2 · 11.5초 기다림 |
| FAQ 오류 (500) | 0.1~0.2초 만에 HTTP 500 |
| LLM 키 틀림 | HTTP 500 |
| 접수 API만 오류 | FAQ 질문까지 담당자 연결 |
> 느림 → timeout · 오류 → 정해진 답 · 일부만 → 그 부분만 · 계속 → 차단기
---
## 23 / 30 · 3단계 · timeout · 재시도 · 오류 출력 (Lab 03 예제 1)
```mermaid
flowchart LR
H["HTTP<br/>timeout 2초"] -->|실패| R["0.5초 뒤<br/>한 번 더"] -->|실패| E["오류 출력 →<br/>담당자 연결"]
```
| 설정 | 최악 |
| --- | --- |
| 2초 × 2번 + 0.5초 | **4.5초** (실측 4.6~4.7초) |
| 2초 × 3번 + 0.5초 × 2 | 7.0초 |
> 재시도는 "잠깐 끊긴" 고장에. "계속 느린" 고장에는 차단기.
---
## 24 / 30 · 3단계 · 옵션은 재 보고 쓴다
| 설정 | 404 | 500 |
| --- | --- | --- |
| 없음 | 재시도 → 오류 출력 | 재시도 → 오류 출력 |
| `neverError` | 재시도 → **보통** | 재시도 → **오류** 출력 |
| + `fullResponse` | 바로 보통 (`statusCode`) | 바로 보통 (`statusCode: 500`) |
> 10/24 시험 워크플로우로 잼. 수업 준비 때 적은 "neverError는 500도 성공"은 **틀렸다.** 짐작하지 않고 잰다.
---
## 25 / 30 · 3단계 · LLM 없이 답하기 (Lab 03 예제 2)
| `llm down` | 고치기 전 | 고친 뒤 |
| --- | --- | --- |
| VPN | 500 | `간단 답변` FAQ 원문 |
| T-1002 | 500 | `간단 답변` 상태 그대로 |
| 점심 메뉴 | 500 | `담당자 연결` |
> 데이터로 답할 수 있는 것은 데이터로. 평소 답과 **같은 척하지 않는다.**
---
## 26 / 30 · 3단계 · 부분 고장 (Lab 03 예제 3)
| 접수 API 500일 때 | Lab 03 예제 2까지 | Lab 03 예제 3 정답 |
| --- | --- | --- |
| T-1002 | 담당자 (`data_api_down`) | 담당자 (`tickets_down`) |
| **VPN** | **담당자** | **자동 답변** |
| T-9999 (404) | 자동 답변 | 자동 답변 |
> 멈춘 **부분만** 줄이고 나머지는 평소대로 (우아한 저하). `fullResponse`로 상태 코드를 보고 나눈다.
> 덤: Lab 03 예제 1의 재시도가 404에도 걸려 평소 VPN `data_ms`가 605ms였다 → Lab 03 예제 3 뒤 38ms.
---
## 27 / 30 · 3단계 · 차단기 (Lab 03 과제 2)
```mermaid
stateDiagram-v2
[*] --> 닫힘
닫힘 --> 열림: FAQ 실패 → breaker-faq 저장 (30초)
열림 --> 닫힘: 30초 뒤 저절로
```
| FAQ 느림 | 결과 |
| --- | --- |
| 처음 질문 | 4.8초 뒤 담당자 (`data_api_down`) |
| 다음 질문 | **0.1초** 담당자 (`breaker_open`) — 차단기 없으면 4.6초 |
---
## 28 / 30 · 3단계 · 상태 점검 (Lab 03 과제 1)
```mermaid
flowchart LR
W(["my-health"]) --> A["/health"] --> F["FAQ 검색"] --> L{{"LLM 'OK'"}} --> S["ok · degraded · down"]
```
| 원칙 | 10/23 |
| --- | --- |
| 점검마다 짧은 timeout | FAQ 느림에도 3.1초 |
| 하나가 실패해도 끝까지 | FAQ 오류 → `degraded` |
| 상태는 코드가 | LLM 멈춤 → `degraded` |
---
## 29 / 30 · 7교시 · 바이브 코딩 `tbench`
```mermaid
flowchart LR
T["Doc/TODO.md"] --> S["Doc/SPEC.md"] --> P["Doc/IMP-PLAN.md<br/>엔진 → TUI"] --> I["단계별 구현"] --> C["확인"] --> U["uv tool install"] --> O["다른 폴더에서"]
```
| 요구 14개 | |
| --- | --- |
| 엔진 | R1 N번 · p50 · p95 · 실패 / R3 저장 · 비교 / R4 동시 / R6 첫 호출 · warmup / R7 실패 종류 / R8 키 가리기 / R9 `timings` 평균 / R10 기준 · 종료 코드 / R11 JSON Lines / R12 설정 / R13 기록 목록 |
| TUI | R2 오는 대로 표 · 스파크라인 / R5 요청 고르기 |
| 설치 | R14 `uv tool install` · 어느 폴더에서나 |
> 50분 안: 엔진 1단계 + TUI 1단계 + 설치. 큰 일은 **다단계로**, 단계마다 확인 · 체크 · commit.
--- | --- |
| R1 | N번 부르고 p50 · p95 · max · 실패 |
| R2 | TUI · 스파크라인 · 오는 대로 표 |
| R3 | 저장 · 바꾸기 전후 비교 (조건이 다르면 경고) |
| R4 | 동시에 k개 |
| R5 | turl 요청 다시 쓰기 |
`요청서 → SPEC 10분 → 1 글자로 → 2 TUI → 3 저장 · 비교 · 동시 → 4 정리` · 테스트는 가짜 서버로
---
## 30 / 30 · 오늘의 정리 · 73일차 예고
| 단계 | 한 줄 |
| --- | --- |
| 1단계 | 고치기 전에 잰다. p95 · 첫 호출 · 단계별 · 종류별 · 시간 예산 |
| 2단계 | 병목(LLM)을 규칙 · 캐시 · 검사로 줄이고, 키를 설계하고, 품질을 다시 확인 |
| 3단계 | timeout · 재시도 · 대체 답 · 부분 고장 · 차단기로 정해진 시간에 정해진 답, 상태 점검 |
| 바이브 코딩 | 재는 도구도 테스트한다 (가짜 서버) |
- 73일차: **입력·출력 검증과 응답 필터링, Workflow 보안** — 들어오는 질문과 나가는 답을 검사하고, 지시문 공격 · 개인정보를 막기
- 바이브 코딩: `tscan` · 폴더 · git에 올리기 전 파일에서 API 키 · 개인정보를 찾아 가려 보여 주기
+91
View File
@@ -0,0 +1,91 @@
# 72일차 실습 준비 · 수업 전에 미리 해 두면 좋은 것
- 수업일: 2026-10-26 (72일차) · Workflow 실행 흐름 최적화와 운영 관점 개선
- 준비 시간: 약 10분
- 먼저 WSL에서 `cd ~/aio-01d4-stu && git pull`로 오늘 자료를 받습니다 (수강생 저장소: https://gl.isitsoft.kr/publicproj/aio-01d4-stu).
- **수업 전날까지** 1~2절을 끝내 주세요.
```mermaid
flowchart LR
A["1. n8n · 데이터 API 켜기<br/>(72일차 판)"] --> B["2. 기준 워크플로우 게시 ·<br/>doctor ready"] --> C["3. 미리 읽기"]
```
---
## 1. n8n과 데이터 API 켜기 (전날 필수, 3분)
오늘은 **72일차 판 데이터 API**(71일차 + 고장 스위치 + 캐시)를 씁니다. 같은 이름이라 이 폴더에서 다시 만들면 바뀝니다.
```bash
sudo systemctl disable --now ollama 2>/dev/null # WSL에 설치한 Ollama가 있으면 끄고 자동 시작도 끔
cd ~/aio-01d4-stu/Last3Mon/65/n8n
docker compose rm -sf ollama # Ollama 컨테이너가 있으면 지움 (없으면 그냥 지나감)
docker compose start
cd ~/aio-01d4-stu/Last3Mon/72/data-api
docker compose up -d --force-recreate
curl -s localhost:8800/admin/fault # {"faq": "ok", "tickets": "ok", "slow_seconds": 8}
```
- **66일차부터 LLM은 OpenAI API 키(`Last3Mon/.env`, Credential `OpenAI (.env)`)만 씁니다.** Ollama는 쓰지 않습니다.
- `network day65-n8n_default not found` → n8n이 켜져 있지 않음. 위 n8n 부분부터 다시.
- `.../day65-n8n/compose.yml: no such file or directory` → 홈이 아닌 곳에 설치함. `Last3Mon/65/n8n/.env`에 `N8N_HOME=<설치 폴더>` ([67일차 준비](../67/00-DAY67-PREP.md) 1절).
## 2. 기준 워크플로우 게시와 준비 확인 (전날 필수, 3분)
오늘 고칠 **느린 실습 워크플로우**(IT 헬프데스크)를 게시해 둡니다 (만들지 않고 게시만).
```bash
cd ~/aio-01d4-stu/Last3Mon/72
uv run tools/flow.py push reference/baseline-helpdesk.json
uv run tools/flow.py call base-helpdesk requests/faq-vpn.json -v # route 자동 답변, 처음 한 번은 느림
uv run tools/flow.py doctor
```
```text
✔ n8n container day65-n8n-n8n-1
✔ n8n http://localhost:5678
✔ data API http://localhost:8800
✔ fault switch /admin/fault (day 72 data API)
✔ n8n → http://data-api:8800
✔ credential 'OpenAI (.env)' (openAiCourse0001)
✔ node catalog (for check)
✔ CLI coding agent: codex, claude
ready
```
| ✘ 항목 | 할 일 |
| --- | --- |
| n8n container · data API | 1절 |
| fault switch | 데이터 API가 예전 판 → 1절 `--force-recreate` |
| `! fault switch is on` | `uv run tools/flow.py fault all ok` |
| credential | `cd ~/aio-01d4-stu/Last3Mon/65/n8n && bash register-credentials.sh` |
| CLI coding agent | [67일차 준비](../67/00-DAY67-PREP.md) 2절 |
---
## 3. 미리 읽어 오면 좋은 내용 (선택, 약 5분)
### 3.1 "느리다"는 몇 초인가
실습 워크플로우에 질문을 보내고 3초를 기다리는 것과 9초를 기다리는 것은 다릅니다. 그런데 10번 중 9번은 2초, 1번은 9초라면 "느리다"고 말해야 할까요? 오늘은 이것을 **p50 · p95**로 잽니다.
### 3.2 용어
| 용어 | 한 줄 설명 |
| --- | --- |
| p50 · p95 | 응답 시간을 줄 세웠을 때 가운데(50%) · 느린 쪽 95% 위치의 값 |
| 병목 | 전체 시간을 정하는 가장 느린 단계 |
| 캐시 | 한 번 만든 답을 저장해 두고 같은 질문에 다시 쓰기 |
| timeout · 재시도 | 이 시간 안에 답이 없으면 실패로 보기 · 실패하면 한 번 더 |
| 대체 경로 (fallback) | 한 단계가 실패했을 때 멈추지 않고 가는 다른 길 |
## 4. 당일 준비물
- 충전된 노트북과 충전기, WSL의 Docker, Windows Terminal(WSL) 창 **두 개**, 웹 브라우저
- 66일차 `~/vibe/turl` (7교시 `tbench`에서 저장한 요청을 다시 씀, 없어도 됨)
## 확인
- [ ] `curl -s localhost:8800/admin/fault`가 `ok`
- [ ] `call base-helpdesk`가 답을 돌려줌
- [ ] `uv run tools/flow.py doctor` → 마지막 줄 `ready`
+406
View File
@@ -0,0 +1,406 @@
# 1단계 · 재기와 병목
- 교시: 1교시 강의 (09:00~09:50) · 2교시 [Lab 01](../../Last3Mon/72/LAB-01-Q-MEASURE-BOTTLENECK.md) 실습 5개 (10:00~10:50)
- 이 단계가 끝나면: 고치기 전에 응답 시간을 p50 · p95로 재고, 워크플로우가 단계별 시간을 결과에 남기게 해 병목을 숫자로 찾고, 시간 예산을 정해 넘었는지 기록할 수 있다.
- 숫자: 모두 강사 노트북 실측입니다 (65일차 n8n 2.42.6, OpenAI `gpt-4.1-mini`). 10/18은 수업 준비, 10/24는 실습을 5개로 늘린 뒤 다시 잰 값입니다. 출력 전부는 수업 뒤 공개되는 `Last3Mon/72/answers/LAB-TEST-RESULT.md`에 있습니다.
- 실습 가리키기: 이 문서에서 "Lab 01 예제 1"은 2교시 [Lab 01 문서](../../Last3Mon/72/LAB-01-Q-MEASURE-BOTTLENECK.md)의 그 절입니다. 파일 경로(`cases/` · `requests/` · `my-work/` · `reference/`)는 실습 폴더 `~/aio-01d4-stu/Last3Mon/72` 기준입니다.
```mermaid
flowchart LR
A["1. 재기 전에<br/>고치지 않기"] --> B["2. 시간을 말하는 말<br/>p50 · p95 · max"] --> C["3. 첫 호출"] --> D["4. 어디서 재나"] --> E["5. 단계별 시간"]
E --> F["6. 종류별로 재기"] --> G["7. 어림 계산"] --> H["8. 가장 느린 단계"] --> I["9. 시간 예산"] --> J["10. 요청문으로"]
```
## 0. 이 시간 (50분)
| 시간 | 절 · 개요 | 하는 일 |
| --- | --- | --- |
| 09:00~09:06 | 개요 1~6장 | 오늘 하루, 실습 기준 워크플로우, 실습 15개 |
| 09:06~09:09 | 1 · 개요 7장 | 재고 고치는 순서, 병목, 짐작과 실측이 다른 예 |
| 09:09~09:19 | 2 · 개요 8장 | 꼬리 지연과 백분위, p50 · p95 손으로 계산, 날마다 다른 기준 |
| 09:19~09:23 | 3 · 개요 9장 | 콜드 스타트, 첫 호출 다루는 법 |
| 09:23~09:29 | 4 · 5 · 개요 10~11장 | 밖에서 · 안에서 재기, 관측 3기둥, 계측, `timings` 칸 계산 |
| 09:29~09:36 | 6 · 7 · 개요 12~13장 | 종류별 실측, 암달의 법칙, 어림 계산 |
| 09:36~09:44 | 8 · 9 · 개요 13~14장 | 임계 경로와 `slowest_step`, SLI · SLO · SLA · 오류 예산, 시간 예산 |
| 09:44~09:50 | 10 · 11 · 확인 질문 | 요청문, 자주 하는 실수, 질문 |
### 2교시 Lab 01에서 이어서 하는 것
| 2교시 실습 (시각) | 이 강의의 절 | 실습에서 하는 일 | 쓰는 파일 · 명령 |
| --- | --- | --- | --- |
| 시작 (10:00) | — | 보고서 서식 복사 | `cp templates/PERF-REPORT-TEMPLATE.md my-work/PERF.md` (오늘 표를 모두 여기에 적음) |
| Lab 01 예제 1 (10:00~10:08, 에이전트 없이) | 2 · 3 | 기준 워크플로우를 5번씩 재고, 첫 호출을 따로 적기 | `bench base-helpdesk requests/faq-vpn.json -n 5` 등 셋 → `my-work/PERF.md` 1절 |
| Lab 01 예제 2 (10:08~10:20) | 4 · 5 | 에이전트에게 `timings` · `steps` · `llm_calls`를 결과에 남기게 하기 | `my-work/helpdesk.json` (기준을 복사), 테스트 `cases/lab01.json` → `PERF.md` 2절 |
| Lab 01 예제 3 (10:20~10:28, 에이전트 없이) | 6 · 7 | 질문 세 종류를 재고 LLM 몫 계산 | `bench my-helpdesk requests/faq-vpn-nocache.json` · `ticket-own` · `off-topic` → `PERF.md` 2-1절 |
| Lab 01 과제 1 (10:28~10:38) | 8 | `faq_ms` · `ticket_ms`로 나누고 `slowest_step` | 테스트 `cases/lab01-challenge.json` |
| Lab 01 과제 2 (10:38~10:50) | 9 · 10 | `budget_ms` · `over_budget` · `llm_ms` | 테스트 `cases/lab01-challenge2.json` → `PERF.md` 2-2절 |
## 1. 재기 전에 고치지 않기
### 일반 설명
성능을 고치는 일은 어느 시스템이든 같은 고리를 돕니다: **재고 → 가장 큰 곳을 찾고 → 한 가지만 바꾸고 → 다시 잰다.** "섣부른 최적화는 모든 악의 뿌리"(도널드 커누스)라는 말이 흔히 인용되는 까닭은, 재지 않고 고친 곳이 대개 병목이 아니기 때문입니다.
- **병목(bottleneck)**: 전체 시간의 대부분을 차지하는 한 곳. 병 목이 좁으면 병을 아무리 키워도 빨리 따라지지 않는 것과 같습니다.
- 단계가 **차례로** 실행되면 전체 시간 = 단계 시간의 합입니다. 그래서 가장 큰 단계를 줄여야 전체가 줄어듭니다.
- 한 번에 **한 가지만** 바꿉니다. 둘을 함께 바꾸면 무엇이 효과였는지(또는 무엇이 망가뜨렸는지) 알 수 없습니다.
- 빨라졌는지만 보지 않고 **결과가 그대로인지**도 봅니다. 빨라지려고 일을 덜 하면 품질이 떨어질 수 있습니다.
### 오늘 실습에서
"느린 것 같으니 병렬로 바꾸자", "모델을 바꾸자"는 **짐작**입니다. 66일차에 일을 두 갈래로 나눴지만 빨라지지 않았던 것을 기억합니다 (n8n v1은 갈래를 하나씩 실행). 고치기 전에 잽니다.
```mermaid
flowchart LR
M["재기"] --> F["병목 찾기"] --> C["한 가지 바꾸기"] --> R["다시 재기"]
R -->|품질 확인| M
```
| 순서 | 질문 | 오늘 쓰는 것 |
| --- | --- | --- |
| 재기 | 지금 몇 초인가? 매번 같은가? | `flow.py bench` |
| 병목 찾기 | 그 시간 중 어느 단계가 대부분인가? | 결과의 `timings` |
| 바꾸기 | 그 단계를 줄이거나 건너뛸 수 있나? | 2단계 (규칙 · 캐시 · 검사 코드) |
| 다시 재기 | 빨라졌나? **답은 그대로인가?** | `bench` + 원래 테스트 |
짐작과 실측이 다른 예 (오늘 실습):
| 짐작 | 실측 (10/24) | 그래서 |
| --- | --- | --- |
| "사내 데이터 API가 느릴 것" | FAQ · 접수 조회를 합쳐 **18~43ms** | 데이터 API를 빠르게 해도 거의 그대로 |
| "IT와 상관없는 질문은 금방 끝날 것" | 점심 메뉴 질문도 **2.48초** (LLM 세 번) | 건너뛸 단계가 있다 (2단계) |
| "push 하면 바로 평소 속도" | push 직후 첫 호출 **4.4초** (10/18 6.5초) | 첫 호출은 따로 적는다 |
## 2. 시간을 말하는 말: 평균 · p50 · p95 · max
### 일반 설명
응답 시간은 거의 늘 **한쪽으로 긴 꼬리**를 가집니다. 대부분은 비슷하게 빠르고, 가끔 아주 느린 것이 섞입니다. 까닭은 여러 가지입니다: 네트워크가 잠깐 막힘, 서버가 다른 일로 바쁨(대기열), 메모리 정리(GC), LLM이 평소보다 긴 답을 씀.
- 이런 분포에서 **평균**은 꼬리에 끌려가 "보통"도 "느린 쪽"도 말해 주지 못합니다. 그래서 업계는 **백분위(percentile)** 로 말합니다: p50(중앙값), p90 · p95 · p99(느린 쪽).
- 꼬리가 중요한 까닭: 한 화면이 서비스를 여러 번 부르면 그중 하나만 느려도 화면이 느립니다. 예를 들어 한 번 부를 때 1%가 느린 서비스를 100번 부르는 화면은 1 − 0.99¹⁰⁰ ≈ **63%** 가 느린 호출을 하나 이상 만납니다 (계산 예). 큰 서비스가 p99 · p99.9까지 보는 까닭입니다.
- 백분위를 세는 방법은 도구마다 조금 다릅니다 (가장 가까운 자리를 고르는 nearest-rank, 두 자리 사이를 나누는 보간). 오늘은 nearest-rank: 작은 것부터 줄 세우고 **올림(비율 × 개수)번째**. 숫자를 비교할 때는 같은 도구 · 같은 방법으로 잰 것끼리.
- 개수가 적으면 백분위가 흔들립니다. p95를 믿으려면 적어도 수십 번, p99는 수백 번이 필요합니다.
### 오늘 실습에서
같은 요청을 여러 번 부르면 시간이 매번 조금씩 다릅니다. 그 목록을 한 숫자로 말하는 방법이 여럿입니다.
| 말 | 뜻 | 계산 (시간을 작은 것부터 줄 세움) |
| --- | --- | --- |
| 평균 | 다 더해서 개수로 나눔 | 한두 번 아주 느리면 크게 끌려감 |
| **p50** (가운데) | "보통 이 정도" | 줄의 가운데 |
| **p95** (느린 쪽) | 100번 중 95번은 이보다 빠름 | 앞에서 95% 자리 |
| max | 가장 느린 한 번 | 맨 끝 |
| 실패 수 | 답을 못 받은 횟수 | 시간 목록과 따로 |
**손으로 계산해 보기.** 10번 잰 시간이 아래와 같다고 합시다 (예시 숫자).
```text
1.1 1.2 1.2 1.3 1.3 1.4 1.4 1.5 1.6 9.8 (초, 작은 것부터)
```
| 말 | 값 | 어떻게 |
| --- | --- | --- |
| 평균 | 2.18초 | 합 21.8 ÷ 10 — 한 번의 9.8초 때문에 "보통 2초"처럼 보임 |
| p50 | 1.3초 | 가운데 (5번째) |
| p95 | 9.8초 | 10개 중 95% 자리 = 10번째 (자리 = 올림(0.95 × 10)) |
| max | 9.8초 | |
- 사용자 열 명 중 아홉은 1.6초 안에 받았고 한 명은 10초를 기다렸습니다. 평균 2초라는 말은 **아무에게도 맞지 않습니다.**
- 사용자가 "느리다"고 기억하는 것은 느린 쪽입니다. 그래서 p50과 p95를 **함께** 말합니다.
- 개수가 적으면 p95와 max가 같아집니다 (5번이면 p95 = 5번째 = max). 5번은 "대강", 20번 이상이면 p95가 의미를 갖기 시작합니다. 오늘 실습은 시간 때문에 5번입니다.
오늘 기준 워크플로우 (`base-helpdesk`, VPN 질문, push 직후 첫 호출 뒤 5번):
| 날 | 첫 호출 | p50 | p95 | 실패 |
| --- | --- | --- | --- | --- |
| 10/18 | 6.5초 | 2.61초 | 2.83초 | 0 |
| 10/23 | 6.3초 | 3.14초 | 3.22초 | 0 |
| 10/24 | 4.4초 | 2.69초 | 2.98초 | 0 |
같은 워크플로우 · 같은 질문인데 날마다 0.5초쯤 다릅니다 (OpenAI 쪽 응답 시간, 네트워크). 그래서 **바꾸기 전 · 후를 같은 날, 같은 조건으로** 잽니다. 어제 잰 숫자와 오늘 바꾼 숫자를 비교하면 안 됩니다.
## 3. 첫 호출 (콜드 스타트)
### 일반 설명
**콜드 스타트(cold start)** 는 "처음 한 번"만 드는 준비 비용입니다. 서버리스 함수(AWS Lambda 등)가 오랜만에 불릴 때, 자바 같은 언어가 처음 실행한 코드를 최적화(JIT)하기 전, 데이터베이스 연결 풀이 비어 있을 때 모두 같은 일이 생깁니다.
| 처음에만 드는 일 | 예 |
| --- | --- |
| 연결 만들기 | TCP 연결 + TLS 인증 주고받기 (왕복 몇 번), 그 뒤로는 연결을 다시 씀 |
| 불러오기 · 초기화 | 프로그램 · 설정 · 라이브러리를 메모리에 올림 |
| 비어 있는 캐시 | 처음에는 모든 것을 새로 계산 |
- 다루는 법은 셋입니다: **따로 적기**(평소 숫자와 섞지 않음), **미리 불러 두기**(워밍업), **아예 줄이기**(늘 켜 두기, 연결 재사용).
- 운영에서는 배포 직후, 서버가 늘어날 때(오토스케일), 밤새 아무도 안 쓴 뒤 아침 첫 요청이 콜드 스타트를 만납니다.
### 오늘 실습에서
n8n이 다시 켜진 뒤(오늘은 `push`할 때마다) 첫 호출은 눈에 띄게 느립니다.
| 까닭 | 설명 |
| --- | --- |
| 연결 열기 | OpenAI · 데이터 API와 처음 연결 (TLS 등) |
| 준비 | 워크플로우 · 노드를 처음 불러오고 Code 노드 실행기(runner)를 깨움 |
| 다루는 법 | 어디서 |
| --- | --- |
| 첫 호출을 **따로 적는다** | Lab 01 예제 1에서 `my-work/PERF.md` 1절 표의 "첫 호출" 칸 (`PERF.md`는 Lab 01을 시작할 때 `templates/PERF-REPORT-TEMPLATE.md`를 복사해 만든 내 보고서) |
| 재기 전에 한 번 불러 둔다 (워밍업) | 4교시 Lab 02 과제 1의 테스트 `cases/lab02-challenge.json`: 맨 위 `before`에 워밍업 호출이 있음 (과제 1에서 열어 봄) |
| 운영에서는 배포 직후 한 번 불러 둔다 | 74일차 배포에서 다시 |
> 수업 준비 때 첫 호출 때문에 시간 기대(`max_seconds`)가 있는 케이스가 흔들렸습니다. 테스트에 워밍업을 넣은 까닭입니다.
## 4. 어디서 재나
### 일반 설명
재는 자리는 크게 둘입니다.
| 재는 자리 | 무엇을 아나 | 예 |
| --- | --- | --- |
| **밖에서** (블랙박스) | 사용자가 실제로 기다린 시간, 성공 · 실패 | 부하 도구(k6 · Locust), 합성 점검(synthetic monitoring), 실제 사용자 측정(RUM) |
| **안에서** (화이트박스) | 그 시간이 어느 단계에서 쓰였나 | 로그, 단계별 시간, 추적(trace) |
업계에서 말하는 **관측 가능성(observability)** 의 세 기둥이 이것입니다.
- **지표(metrics)**: 모아서 센 숫자 (p95, 실패율, 분당 요청 수). 추세 · 알림에 씀.
- **로그(logs)**: 일이 하나 일어날 때마다 남기는 기록. 무슨 일이 있었는지 찾아볼 때.
- **추적(traces)**: 요청 **하나**가 지나간 길과 단계(span)별 시간. 어디가 느린지 볼 때.
밖에서 잰 숫자로 "느리다"를 알고, 안에서 잰 숫자로 "어디가"를 찾습니다. 둘 중 하나만으로는 모자랍니다.
### 오늘 실습에서
| 어디서 | 보이는 것 | 한계 |
| --- | --- | --- |
| `flow.py bench` (밖에서) | 사용자가 기다린 전체 시간, p50 · p95 · max, 실패 수 | 안에서 어디가 느린지는 모름 |
| n8n **Executions** 화면 | 노드마다 걸린 시간 | 한 번씩 눈으로, 여러 번 모으기 어려움 |
| 워크플로우가 남긴 `timings` | 단계마다 ms, 결과와 함께 | 직접 만들어야 함 (2교시 Lab 01 예제 2에서 만듦) |
```mermaid
flowchart LR
U["사용자 (bench)"] -->|전체 2.7초| W["n8n 웹훅"]
W --> S1["분류 LLM"] --> S2["데이터 조회"] --> S3["답 LLM"] --> S4["검토 LLM"]
S4 --> W
W -.->|timings 로 보임| T["classify · data · answer · review"]
```
- 밖에서 잰 시간(`bench`)과 안에서 잰 `total_ms`는 조금 다릅니다. 밖은 웹훅을 받고 돌려주는 시간까지 포함합니다. 오늘 VPN 한 번: `call` 2.7초, `total_ms` 2698ms (10/24) — 거의 같습니다.
## 5. 단계별 시간을 결과에 남기기
### 일반 설명
코드에 시간을 재는 줄을 넣는 일을 **계측(instrumentation)** 이라고 합니다. 원리는 간단합니다: 단계 앞뒤에서 시각을 읽고 차이를 남깁니다.
- 시각은 같은 시계로 읽어야 뺄 수 있습니다. 컴퓨터 시계는 시간 맞추기(NTP)로 가끔 앞뒤로 튈 수 있어서, 정밀하게 잴 때는 늘 앞으로만 가는 시계(monotonic: 자바스크립트 `performance.now()`, 파이썬 `time.monotonic()`)를 씁니다. 오늘처럼 ms 단위로 단계를 비교할 때는 `Date.now()`로 충분합니다.
- 계측에도 시간이 듭니다. 아주 짧은 단계를 아주 많이 재면 재는 비용이 커집니다. 오늘은 Code 노드 몇 개라 수 ms입니다.
- 업계 도구는 이것을 표준으로 만든 것입니다: **OpenTelemetry**는 단계(span)마다 시작 · 끝 시각과 이름을 남기고, 모아서 한 요청의 폭포 그림(waterfall)으로 보여 줍니다. 오늘 `timings`는 그 아주 작은 판입니다.
- 시간과 함께 **어느 길로 왔는지**(지나간 단계, 부른 외부 서비스 수)도 남깁니다. 시간은 길에 따라 다르기 때문입니다.
### 오늘 실습에서
단계가 끝날 때마다 Code 노드에서 `Date.now()`(지금 시각, ms)를 남기고, 마지막에 앞 시각과의 차이를 계산합니다.
```mermaid
flowchart LR
S["Start clock<br/>t0"] --> C{{"Classify"}} --> T1["After classify<br/>t1"] --> D["FAQ · 접수"] --> T2["After data<br/>t2"] --> A{{"Answer"}} --> T3["After answer<br/>t3"] --> R{{"Review"}} --> T4["After review<br/>t4"] --> B["Build result<br/>timings"]
```
| 칸 | 계산 | 10/24 VPN 한 번 |
| --- | --- | --- |
| `classify_ms` | t1 − t0 | 616 |
| `data_ms` | t2 − t1 | 18 |
| `answer_ms` | t3 − t2 | 1124 |
| `review_ms` | t4 − t3 | 920 |
| `total_ms` | 끝 − t0 | 2698 |
- 단계 칸을 더하면 2678ms. `total_ms`와 20ms 차이는 시각을 남기는 Code 노드들과 결과를 만드는 시간입니다.
- **시각 노드는 단계가 끝난 뒤에.** 앞에 두면 값이 0이나 음수가 됩니다 (Lab 01 문서 끝 "막혔을 때" 표 첫 줄).
- 지나가지 않은 단계(예: 규칙으로 분류해 Classify를 건너뜀)는 칸을 넣지 않습니다. `$('노드').isExecuted`로 먼저 확인합니다.
- `steps`(지나간 단계)와 `llm_calls`(부른 LLM 수)도 워크플로우가 정해진 값으로 남깁니다. 69일차 협업 기록과 같은 생각: **어느 길로 왔는지는 워크플로우가 안다.** LLM에게 "몇 번 불렀는지 적어"라고 하지 않습니다.
## 6. 요청 종류별로 재기 (2교시 Lab 01 예제 3)
### 일반 설명
한 서비스에 오는 요청은 종류가 섞여 있습니다(**작업 구성**, workload mix). 종류마다 지나가는 길이 다르면 시간도 다릅니다.
- 모두 섞어 잰 숫자는 "어느 종류가 느린지"를 숨깁니다. 빠른 종류가 많으면 느린 종류의 문제가 평균 · p50에 묻힙니다.
- 그래서 **종류별로 따로** 재고, 하루에 종류마다 몇 건이 오는지(비율)도 함께 봅니다. 고칠 순서는 "느림 × 건수"로 정합니다.
- 부하 시험(load test)도 실제 요청 비율을 흉내 낸 시나리오로 합니다. 한 가지 요청만 반복하면 실제와 다른 결론이 납니다.
### 오늘 실습에서
같은 워크플로우에 질문 세 종류를 5번씩 (10/24, Lab 01 예제 2에서 만든 `my-helpdesk`, 캐시 없음). Lab 01 예제 3에서 각자 같은 표를 `my-work/PERF.md` 2-1절에 채웁니다.
| 질문 종류 | p50 | p95 | classify | data | answer | review | total | LLM 몫 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| FAQ (VPN) | 2.91초 | 3.08초 | 616 | 18 | 1124 | 920 | 2698 | 98.6% |
| 접수 번호 (T-1002) | 2.99초 | 3.12초 | 818 | 43 | 877 | 895 | 2647 | 97.8% |
| IT 밖 (점심 메뉴) | 2.48초 | 2.84초 | 732 | 19 | 740 | 978 | 2482 | 98.7% |
(단계 칸은 ms, `uv run tools/flow.py call my-helpdesk <요청> -v` 한 번의 값. LLM 몫 = (classify + answer + review) ÷ total)
- **세 종류 모두 LLM이 98% 안팎.** 데이터 API는 20~40ms입니다.
- 점심 메뉴 질문은 데이터가 하나도 없는데도 LLM을 세 번 부르고 "찾지 못했습니다"류의 답을 씁니다. 2단계에서 줄일 수 있는 첫 후보입니다.
- 접수 번호 질문은 10/24 Lab 01 예제 2의 테스트(`cases/lab01.json`)에서 한 번 실패했습니다: 검토 LLM이 "처리 중"을 "승인 대기 중"으로 바꿔 썼습니다. **검토 LLM이 사실을 바꿔 쓸 수 있다**는 것도 4교시 Lab 02 과제 1(검토 LLM을 검사 코드로 바꾸기)의 근거입니다.
## 7. 어림 계산: 어디를 고치면 얼마나 빨라지나
### 일반 설명
어디를 고칠지 정하기 전에 "고치면 얼마나 줄어드나"를 계산해 봅니다. 이것을 정리한 것이 **암달의 법칙(Amdahl's law)** 입니다.
> 전체에서 비율 p를 차지하는 부분을 s배 빠르게 하면, 전체는 1 ÷ ((1 − p) + p ÷ s)배 빨라진다.
| 고치는 부분의 비율 p | 그 부분을 | 전체 |
| --- | --- | --- |
| 1% | 10배 빠르게 | 약 1.01배 (거의 그대로) |
| 50% | 2배 빠르게 | 약 1.33배 |
| 98% | 2배 빠르게 | 약 1.96배 |
| 98% | 없앰 | 약 50배 (남은 2%만) |
- 작은 부분을 아무리 빠르게 해도 전체는 거의 그대로입니다. **큰 덩어리부터.**
- 어림은 방향을 정하는 데 쓰고, 고친 뒤에는 반드시 다시 잽니다. 단계끼리 영향을 주기도 하기 때문입니다 (예: 앞 단계가 빨라지면 뒤 단계가 콜드 스타트를 떠안음).
### 오늘 실습에서
전체 2.7초(VPN) 중 각 단계를 **없앤다면** 얼마가 되는지 어림합니다.
| 없애는 것 | 줄어드는 시간 | 남는 시간 | 줄어드는 비율 |
| --- | --- | --- | --- |
| 데이터 조회 (18ms) | 0.02초 | 2.68초 | 0.7% |
| 분류 LLM (616ms) | 0.62초 | 2.08초 | 23% |
| 검토 LLM (920ms) | 0.92초 | 1.78초 | 34% |
| 분류 + 검토 | 1.54초 | 1.16초 | 57% |
- 데이터 조회를 열 배 빠르게 해도 사용자는 모릅니다. **큰 덩어리부터.**
- 2단계 실측과 맞춰 봅니다: 10/24 캐시 없이 VPN p50이 Lab 02 예제 2까지(규칙 + 캐시, LLM 2번) 2.22초 → Lab 02 과제 1에서 검토를 검사로 바꾼 뒤(LLM 1번) 1.12초. 어림한 "검토를 빼면 약 1초 줄어듦"과 맞습니다.
- 어림은 **방향**을 정하는 데 씁니다. 결과는 반드시 다시 잽니다.
## 8. 가장 느린 단계 (2교시 Lab 01 과제 1)
### 일반 설명
차례로 실행되는 흐름에서는 가장 오래 걸린 단계가 곧 가장 큰 병목입니다. 단계가 **동시에** 실행되는 흐름이라면, 전체 시간은 가장 긴 줄기(**임계 경로**, critical path)가 정합니다. 임계 경로 밖의 단계를 빠르게 해도 전체는 줄지 않습니다.
- 매번 사람이 화면을 열어 찾는 대신, 결과에 "가장 느린 단계"를 **계산해 남기면** 많은 요청을 모아 셀 수 있습니다 ("오늘 느린 요청의 70%는 답 단계").
- 가장 느린 단계는 실행마다 바뀔 수 있습니다. 비슷한 단계가 둘이면 1등이 자주 바뀌므로, 한 번이 아니라 **여러 번 모아서** 봅니다.
### 오늘 실습에서
`timings`에서 `total_ms`를 뺀 칸 중 가장 큰 칸의 이름을 `slowest_step`으로 결과에 남깁니다. 매번 Executions를 열지 않아도 결과만 보고 어디가 느린지 압니다.
10/24 Lab 01 과제 1 정답으로 과제 1 테스트 `cases/lab01-challenge.json`의 네 번 (ms):
| 질문 | classify | faq | ticket | answer | review | total | `slowest_step` |
| --- | --- | --- | --- | --- | --- | --- | --- |
| VPN (push 직후 첫 호출) | 1365 | 16 | 14 | 1196 | 993 | 3614 | classify |
| VPN | 774 | 17 | 13 | 971 | 815 | 2601 | answer |
| VPN | 680 | 17 | 10 | 1044 | 1007 | 2769 | answer |
| 접수 현황 | 723 | 13 | 11 | 1029 | 1162 | 2951 | review |
- 답 LLM과 검토 LLM이 비슷해서 실행마다 1등이 바뀝니다. push 직후 첫 호출에서는 분류(첫 LLM)가 1등이었습니다 (첫 호출의 준비 시간이 첫 LLM 단계에 붙음). 그래서 테스트는 `classify` · `answer` · `review` 중 하나면 통과로 봅니다.
- `slowest_step`은 LLM이 아니라 **Code가 계산**합니다. 숫자가 있으면 판단하지 않고 셉니다.
- 데이터 조회를 `faq_ms` · `ticket_ms`로 나눈 까닭: 3단계에서 "어느 쪽이 멈췄나"를 볼 때 둘을 따로 봐야 합니다.
## 9. 시간 예산 (2교시 Lab 01 과제 2)
### 일반 설명
"빠르다"는 기준이 있어야 말할 수 있습니다. 업계에서 쓰는 말은 셋입니다.
| 말 | 뜻 | 예 |
| --- | --- | --- |
| **SLI** (Service Level Indicator, 서비스 수준 지표) | 서비스가 잘 되고 있는지 **재는 숫자**. 무엇을 재나 | "3초 안에 답한 요청의 비율" |
| **SLO** (Service Level Objective, 서비스 수준 목표) | 그 지표가 지켜야 할 **목표 값**. 팀이 스스로 정함 | "한 달 동안 95% 이상" |
| **SLA** (Service Level Agreement, 서비스 수준 협약) | 고객과 맺은 **계약**. 목표를 못 지키면 어떻게 되나 (보상) | "95% 밑이면 요금 일부 돌려줌" |
- **오류 예산(error budget)** = 1 − SLO. 목표가 95%면 5%까지는 늦어도 됩니다. 예산이 남으면 새 기능을 내고, 다 쓰면 안정화에 집중합니다 (구글 SRE의 방식).
- **시간 예산 나누기**: 전체 목표(예: 3초)를 단계마다 나눠 줍니다 (분류 0.5초, 데이터 0.2초, 답 2초 …). 각 단계는 자기 몫을 넘었는지로 봅니다.
- 목표는 **사용자 쪽에서** 정합니다. "지금 2.7초니까 목표 3초"가 아니라 "사용자가 3초 넘으면 답답해하니 3초"입니다.
- 넘은 요청을 버리지 않고 **기록해서 모으는** 것이 먼저입니다. 모아야 어디를 고칠지 보입니다.
### 오늘 실습에서
"빠르다 · 느리다"는 기준이 있어야 말할 수 있습니다. 그 기준이 **시간 예산**(목표 시간)입니다.
| 말 | 뜻 | 오늘 |
| --- | --- | --- |
| 시간 예산 (`budget_ms`) | 한 요청이 이 안에 끝나야 함 | 기본 3000ms, 요청 본문으로 바꿀 수 있음 |
| 넘었나 (`over_budget`) | `total_ms > budget_ms` | 결과에 남김 (답은 그대로 보냄) |
| LLM 시간 (`llm_ms`) | 분류 · 답 · 검토의 합 | 넘었을 때 LLM 탓인지 바로 보임 |
| 목표 수준 (SLO) | "요청의 95%가 3초 안" 같은 약속 | 오늘은 씨앗만 (79일차 운영 대시보드) |
- 예산은 **사용자 쪽에서** 정합니다 ("3초 넘으면 느리다"). 워크플로우를 만든 사람이 "우리 워크플로우는 2.7초니까 예산 3초"라고 정하는 것이 아닙니다.
- 넘었다고 답을 멈추지 않습니다. 이미 거의 다 만든 답을 버리면 사용자는 더 오래 기다립니다. **기록하고, 모아서, 고칠 곳을 정합니다.**
- `llm_ms`를 `timings` 안에 넣으면 늘 가장 커서 `slowest_step`이 망가집니다. 그래서 결과 맨 바깥에 둡니다.
**세어 보기 (예시).** 하루 200건 중 `over_budget: true`가 24건이면 3초 안에 답한 비율은 (200 − 24) ÷ 200 = 88%. 목표가 95%라면 모자랍니다. 넘은 24건의 `slowest_step`을 세어 보면 무엇부터 고칠지 보입니다.
10/24 Lab 01 과제 2 정답으로 과제 2 테스트 `cases/lab01-challenge2.json` (VPN · 접수, 캐시 없이). Lab 01 과제 2에서 각자 `PERF.md` 2-2절에 같은 표를 채웁니다:
| 예산 | `total_ms` | `llm_ms` | `over_budget` |
| --- | --- | --- | --- |
| 60000 | 3357 (push 직후) | 3295 | false |
| 500 | 2609 | 2563 | true |
| 주지 않음 → 3000 | 3023 | 2973 | **true** |
| 주지 않음 → 3000 (접수 현황) | 2904 | 2858 | false |
- 기본 예산 3초에서 VPN이 3023ms로 **23ms 넘었습니다.** 지금 워크플로우는 3초 근처라 실행마다 `true` · `false`가 바뀝니다. 그래서 과제 2 테스트는 기본값일 때 `budget_ms: 3000`만 보고, 4교시 Lab 02에서 LLM을 줄이면 넉넉히 들어옵니다.
- `llm_ms`가 `total_ms`의 98%: 넘은 까닭이 LLM이라는 것이 결과만 봐도 보입니다.
## 10. 요청문으로
Lab과 다른 업무로 예를 듭니다 (주문 문의 워크플로우, **실습에는 없는 예**: `my-work/order.json`은 가상의 파일). Lab 01 예제 2 · 과제 1 · 과제 2의 요청문을 쓸 때 같은 칸을 채웁니다.
| 넣을 것 | 예 |
| --- | --- |
| 바탕 · 바꿀 것 | "`my-work/order.json`은 그대로 두고 결과에 필드만 더한다" |
| 시각을 남길 곳 | "주문 조회 뒤, 답 LLM 뒤" |
| 결과 모양 | `timings: {lookup_ms, answer_ms, total_ms}`, `steps`, `llm_calls` |
| 지나가지 않은 단계 | "지나가지 않은 노드는 `isExecuted`로 확인하고 빼기" |
| 예산 | "본문 `budget_ms`(없으면 2000), `over_budget`, `llm_ms`는 `timings` 밖" |
| 기대 결과 | 테스트 파일 |
> "빠르게 해 줘"로 시작하지 않습니다. **"재게 해 줘"** 가 먼저입니다.
## 11. 자주 하는 실수
| 실수 | 생기는 일 | 바로잡기 |
| --- | --- | --- |
| 평균 하나만 적음 | "가끔 10초"가 안 보임 | p50 · p95 · max |
| push 직후 바로 잼 | 첫 호출 4~6초가 섞임 | 첫 호출을 따로, 또는 워밍업 |
| 다른 날 숫자와 비교 | 날마다 0.5초쯤 다름 | 같은 날 전 · 후 |
| 시각 노드를 단계 앞에 | `timings`가 0 · 음수 | 단계가 끝난 뒤 |
| LLM에게 `llm_calls`를 쓰게 함 | 틀린 숫자 | 워크플로우가 `isExecuted`로 셈 |
| 빨라진 것만 봄 | 답이 바뀐 것을 놓침 | 원래 테스트도 다시 |
## 확인 질문
<details><summary>1. 5번 잰 시간이 2.4 · 2.5 · 2.6 · 2.6 · 7.9초입니다. p50 · p95 · 평균은? 사용자에게 무엇이라고 말하나요?</summary>
p50 2.6초, p95 7.9초(5개 중 5번째), 평균 3.6초. "보통 2.6초, 가끔 8초 가까이"라고 말합니다. 7.9초가 첫 호출이었는지 먼저 확인합니다.
</details>
<details><summary>2. 데이터 API를 2배 빠르게 하는 일과 검토 LLM을 없애는 일 중 무엇을 먼저 하나요?</summary>
검토 LLM. 데이터 API는 전체의 1%가 안 되어(18ms) 2배 빨라져도 0.01초입니다. 검토 LLM은 약 0.9초(34%)입니다. 대신 검토가 하던 일 중 무엇을 잃는지 적습니다 (2단계).
</details>
<details><summary>3. 시간 예산 3초를 넘은 요청에 답을 보내지 않으면 어떻게 되나요?</summary>
사용자는 3초를 기다리고도 답을 못 받습니다. 예산은 **기록하고 모아서** 고칠 곳을 정하는 데 씁니다. 답을 멈추는 것은 3단계의 timeout(외부 서비스를 기다리는 한도)이 하는 일이고, 그때도 "정해진 답"을 보냅니다.
</details>
## 정리
- [ ] 재기 → 병목 → 한 가지 바꾸기 → 다시 재기 (품질도)
- [ ] p50 · p95 · max를 함께, 첫 호출은 따로, 같은 날 전 · 후
- [ ] 단계별 시간 · 지나간 단계 · LLM 수는 워크플로우가 결과에 남긴다
- [ ] 질문 종류마다 재고, 큰 덩어리부터 어림한다 (오늘은 LLM이 98%)
- [ ] 시간 예산은 사용자 쪽에서 정하고, 넘었는지는 기록한다
+353
View File
@@ -0,0 +1,353 @@
# 2단계 · 실행 흐름 최적화
- 교시: 3교시 강의 (11:00~11:50) · 4교시 [Lab 02](../../Last3Mon/72/LAB-02-Q-OPTIMIZE-FLOW.md) 실습 5개 (12:00~12:50)
- 이 단계가 끝나면: LLM 호출을 규칙 · 캐시 · 검사 코드로 줄이고, 캐시 키를 설계해 적중률을 올리고, 바꿀 때마다 다시 재서 빨라진 만큼 품질이 그대로인지 확인할 수 있다.
- 숫자: 강사 노트북 실측 (10/18 수업 준비, 10/23~24 다시 잼). 같은 표 안의 전 · 후는 같은 날 잰 값입니다.
- 실습 가리키기: "Lab 02 예제 1"은 4교시 [Lab 02 문서](../../Last3Mon/72/LAB-02-Q-OPTIMIZE-FLOW.md)의 그 절, "Lab 01 …"은 2교시 실습입니다. 파일 경로는 실습 폴더 `~/aio-01d4-stu/Last3Mon/72` 기준이고, `PERF.md`는 Lab 01을 시작할 때 만든 내 보고서 `my-work/PERF.md`입니다.
```mermaid
flowchart LR
A["1. 줄일 수 있는<br/>LLM 호출"] --> B["2. 규칙 먼저"] --> C["3. 캐시"] --> D["4. 적중률"] --> E["5. 캐시 키 설계"]
E --> F["6. 검토를 검사로"] --> G["7. 잃은 것 적기"] --> H["8. 테스트도 바뀐다"] --> I["9. 요청문으로"]
```
## 0. 이 시간 (50분)
| 시간 | 절 · 개요 | 하는 일 |
| --- | --- | --- |
| 11:00~11:06 | 1 · 개요 15장 | LLM 호출의 시간 · 비용, 줄이는 방법, Lab 01 숫자: LLM 98% |
| 11:06~11:16 | 2 · 3 · 개요 16~17장 | 싼 것 먼저(규칙), 캐시 방식 · TTL · 무효화 · 키, 캐시해도 되는 것 |
| 11:16~11:26 | 4 · 5 · 개요 18~19장 | 적중률 공식 · 실측, 캐시 키 설계, FAQ 번호 캐시 |
| 11:26~11:36 | 6 · 7 · 개요 20~21장 | 코드 검사와 LLM 검토, 검토 → 검사 실측, 얻은 것 · 잃은 것 |
| 11:36~11:44 | 8 · 9 | 테스트 격리, 캐시가 테스트를 바꾼 이야기, 요청문 |
| 11:44~11:50 | 확인 질문 | 질문 |
### 4교시 Lab 02에서 이어서 하는 것
바탕은 Lab 01 과제 2까지 고친 `my-work/helpdesk.json`입니다 (없으면 `cp reference/lab01-example2-timings.json my-work/helpdesk.json` 후 push).
| 4교시 실습 (시각) | 이 강의의 절 | 실습에서 하는 일 | 쓰는 파일 · 명령 |
| --- | --- | --- | --- |
| Lab 02 예제 1 (12:00~12:10) | 1 · 2 | 데이터 조회를 앞으로, 규칙으로 되면 분류 LLM을 건너뜀 | `bench my-helpdesk requests/faq-printer.json -n 5`, 테스트 `cases/lab02.json` |
| Lab 02 예제 2 (12:10~12:20) | 3 · 8 | 질문 캐시 (데이터 API `/cache`) | 테스트 `cases/lab02.json` · `cases/lab01.json`, `requests/faq-vpn-again.json` |
| Lab 02 예제 3 (12:20~12:26, 에이전트 없이) | 4 | 캐시를 쓸 때 · 안 쓸 때를 재고 적중률 계산 | `bench my-helpdesk requests/faq-vpn.json` · `faq-vpn-nocache.json` -n 5, `curl localhost:8800/cache-stats` → `PERF.md` 3-1절 |
| Lab 02 과제 1 (12:26~12:38) | 6 · 7 | 검토 LLM을 검사 Code로 (LLM 한 번, 2.5초 안) | 테스트 `cases/lab02-challenge.json` (`--repeat 3`) → `PERF.md` 3절 |
| Lab 02 과제 2 (12:38~12:50) | 5 · 7 | FAQ 번호로 두 번째 캐시 (`cache_by`) | 테스트 `cases/lab02-challenge2.json` → `PERF.md` 3절 |
## 1. 줄일 수 있는 LLM 호출
### 일반 설명
LLM 호출은 보통 워크플로우에서 가장 느리고 비싼 단계입니다.
- **시간**: 첫 글자가 나오기까지의 시간(TTFT) + 답 글자 수 × 글자 하나 만드는 시간. 답을 한 토큰씩 차례로 만들기 때문에 **답이 길수록** 오래 걸립니다. 입력(지시문 · 자료)이 길어도 첫 글자가 늦어집니다.
- **비용**: 입력 토큰과 출력 토큰 수만큼 돈이 듭니다. 호출 수가 곧 비용입니다.
- **흔들림**: 같은 질문에도 시간과 답이 조금씩 다릅니다.
줄이는 방법은 쉬운 것부터 이 순서로 생각합니다.
| 방법 | 뜻 |
| --- | --- |
| 없애기 | 규칙 · 코드로 되는 일은 LLM에게 맡기지 않음 |
| 다시 쓰기 | 같은 일의 결과를 저장해 두고 씀 (캐시) |
| 줄이기 | 짧은 지시문, 짧은 답 ("3문장 이내"), 작은 모델 |
| 동시에 | 서로 기다릴 필요 없는 호출을 함께 (도구가 지원할 때) |
| 기다림을 덜 느끼게 | 답을 나오는 대로 보여 주기 (스트리밍) |
### 오늘 실습에서
2교시 Lab 01 예제 3에서 잰 대로 시간의 98%가 LLM 세 번이었습니다. 하나씩 "꼭 LLM이어야 하나"를 묻습니다.
| LLM 호출 | 하는 일 | 10/24 VPN 한 번 | 꼭 LLM이어야 하나 |
| --- | --- | --- | --- |
| 분류 | faq · ticket · other | 616ms | 번호가 있으면 ticket, FAQ가 찾아지면 faq — **규칙으로 대부분** |
| 답 | 데이터로 답 쓰기 | 1124ms | 예 (오늘 남기는 것) |
| 검토 | 말투 다듬기 | 920ms | 말투는 답 지시문으로, 빈 답 · 길이 · 한국어는 **코드로** |
같은 답이 다시 나오는 질문(같은 FAQ)은 아예 일을 하지 않아도 됩니다 → **캐시**.
```mermaid
flowchart LR
Q["질문"] --> C{"캐시에 있나"}
C -->|있음| R1["바로 답<br/>LLM 0번"]
C -->|없음| D["데이터 조회"] --> RU{"규칙으로<br/>분류되나"}
RU -->|예| A{{"답 LLM"}}
RU -->|아니오| L{{"분류 LLM"}} --> A
A --> K["검사 Code"] --> S["캐시에 저장 · 답"]
```
4교시 Lab 02가 하는 일이 이 그림입니다. Lab 01에서 고친 워크플로우(LLM 3번)에서 한 칸씩 바꿉니다: 규칙은 예제 1, 캐시는 예제 2, 검사 Code는 과제 1.
## 2. 규칙 먼저 (4교시 Lab 02 예제 1)
### 일반 설명
**싼 것 먼저, 모를 때만 비싼 것**: 이것을 단계적 처리(cascade) 또는 라우팅(routing)이라고 합니다. 고객센터가 자주 묻는 질문은 자동 응답으로, 나머지만 상담원에게 넘기는 것과 같습니다.
- 규칙(키워드, 번호 모양, 검색 결과 유무)은 빠르고 · 늘 같고 · 왜 그렇게 됐는지 설명할 수 있습니다. 대신 정해 둔 것밖에 모릅니다.
- 그래서 규칙은 **확실할 때만** 답하고, 모르면 LLM에게 넘깁니다. 규칙이 틀리게 답하는 비율(정밀도)이 낮아야 합니다.
- 규칙도 코드입니다. 맞는 예 · 틀리는 예 · 모르는 예를 테스트로 남겨 둡니다. 시간이 지나 질문이 바뀌면 규칙도 다시 봅니다.
### 오늘 실습에서
```mermaid
flowchart LR
D["FAQ · 접수 조회"] --> R{"규칙"}
R -->|"T-1234"| T["ticket"]
R -->|"FAQ 결과 있음"| F["faq"]
R -->|"모름"| L{{"분류 LLM"}}
T & F & L --> A{{"답"}}
```
- 규칙을 쓰려면 **데이터 조회가 먼저** 와야 합니다 (순서를 바꿈). 데이터 조회는 20~40ms라 먼저 해도 손해가 거의 없습니다 (1단계 6절, Lab 01 예제 3에서 잰 표).
- 두 길(규칙 · LLM)이 답 노드에서 만납니다. 두 길의 끝이 같은 모양(`{category}`)을 내놓게 합니다.
- 68일차 "나누기와 유형은 LLM, 담당과 빠진 입력은 규칙"과 같은 생각입니다. **셀 수 있는 것은 규칙.**
| 10/24 | Lab 01 예제 2 워크플로우 (LLM 3번) | Lab 02 예제 1 정답: 규칙 먼저 (LLM 2번) |
| --- | --- | --- |
| 프린터 질문 bench p50 | (재지 않음) | 2.45초 |
| 테스트 `cases/lab02.json` | — | 7/9 (예제 2에서 만들 캐시 케이스가 아직 실패, 접수 답에서 검토 LLM이 "처리 중"을 바꿔 씀) |
- 수업 준비 때(10/18): 접수 질문 2.4~2.8초 → 1.9~2.3초 (LLM 3번 → 2번).
- **규칙이 틀리면?** 예를 들어 "프린터 설치 담당자가 누구예요?"처럼 FAQ 낱말(`프린터` · `설치`)이 들어 있지만 FAQ가 답이 아닌 질문은 규칙으로 `faq`가 됩니다. 반대로 FAQ 낱말이 없는 질문("회의실 화면 공유가 안 돼요")은 LLM이 분류해야 합니다. `cases/lab02.json`에 "규칙으로 모르면 LLM이 분류" 케이스가 있는 까닭입니다. 규칙도 테스트합니다.
## 3. 캐시 (4교시 Lab 02 예제 2)
### 일반 설명
**캐시**는 한 번 만든 결과를 가까운 곳에 저장해 두고, 같은 요청이 오면 다시 만들지 않고 돌려주는 것입니다. 브라우저, CDN, 데이터베이스 앞의 Redis, LLM 응답 저장이 모두 캐시입니다.
| 방식 | 하는 일 | 오늘 |
| --- | --- | --- |
| **캐시 어사이드** (cache-aside) | 일하기 전에 캐시를 보고, 없으면 일한 뒤 저장 | ✔ (워크플로우가 직접) |
| 읽기 대행 (read-through) | 캐시가 알아서 원본을 읽어 옴 | |
| 쓰기 동시 (write-through) | 원본을 바꿀 때 캐시도 함께 | |
- **수명(TTL)**: 저장한 값이 저절로 사라지는 시간. 짧으면 적중이 줄고, 길면 오래된 답이 나갑니다.
- **무효화(invalidation)**: 원본이 바뀌었을 때 캐시를 지우는 일. "컴퓨터 과학에서 어려운 것은 캐시 무효화와 이름 짓기 두 가지"라는 농담이 있을 만큼 어렵습니다.
- **키**: 답을 바꾸는 것은 **모두** 키에 들어가야 합니다 (질문, 사람마다 다르면 사람, 자료가 바뀌면 자료 판번호). 하나라도 빠지면 다른 사람 · 다른 상황의 답이 나갑니다.
- 캐시해도 되는 것: **같은 키면 언제 · 누구에게나 같은 답**. 개인 정보가 든 답, 시시각각 바뀌는 상태는 캐시하지 않습니다.
### 오늘 실습에서
| 캐시해도 되나 | 이유 |
| --- | --- |
| "VPN이 끊겨요" 같은 FAQ 답 | 같은 질문이면 같은 답 |
| "T-1002 상태" | **안 됨.** 상태가 바뀌면 예전 답이 나감 |
| 사람마다 다른 답 (본인 접수 건) | **안 됨.** 남의 답이 나갈 수 있음 |
| `no_cache: true`를 보낸 요청 | 읽지도 쓰지도 않음 (재기 · 테스트용) |
```mermaid
flowchart LR
K["Cache key<br/>질문 → trim · 소문자 · 공백 하나"] --> G["GET /cache/키<br/>(없으면 404)"] --> I{"있나"}
I -->|예| H["Cached result<br/>llm_calls 0"]
I -->|아니오| W["원래 흐름"] --> P["POST /cache/키<br/>ttl 600"]
```
- 캐시 키: 질문을 앞뒤 공백 제거 · 소문자 · 연속 공백 하나로. `" vpn 접속이 자꾸 끊겨요. 어떻게 하나요? "`와 `"VPN 접속이 자꾸 끊겨요. 어떻게 하나요?"`가 같은 키가 됩니다 (Lab 02 예제 2 테스트가 보내는 `requests/faq-vpn-again.json`).
- 수명(ttl): 오늘은 600초. FAQ가 바뀌면 캐시를 비워야 합니다 (`/cache-clear`).
- 캐시 조회는 **일하기 전**, 저장은 답을 만든 **뒤**.
- 10/24 Lab 02 예제 2 정답으로 `cases/lab02.json` 9/9: 같은 VPN 질문 두 번째가 **0.1초**, `llm_calls: 0`.
## 4. 적중률 (4교시 Lab 02 예제 3)
### 일반 설명
캐시의 효과는 **적중률(hit rate)** 이 정합니다.
> 평균 시간 ≈ 적중률 × 캐시 시간 + (1 − 적중률) × 원래 시간
- 캐시가 아무리 빨라도 적중률이 낮으면 평균은 거의 그대로입니다.
- 적중률은 요청이 얼마나 **반복되는지**에 달렸습니다. 실제 서비스의 요청은 흔히 일부 인기 항목에 몰리고 나머지는 드문드문 옵니다 (롱테일). 인기 항목이 많을수록 캐시가 잘 맞습니다.
- 처음 켠 캐시는 비어 있어(cold cache) 적중률이 낮습니다. 잠시 지나야 오른 값이 나옵니다.
- 도구가 세는 hits · misses가 **무엇을 세는지** 먼저 확인합니다 (조회만 해도 세는지, 저장만 하는 요청도 세는지).
### 오늘 실습에서
캐시가 빨라도 **맞는(적중) 비율**이 낮으면 평균은 거의 그대로입니다.
10/24 실측 (같은 VPN 질문 5번, 캐시를 비운 뒤). Lab 02 예제 3에서 각자 같은 표를 `PERF.md` 3-1절에 채웁니다:
| 재기 | 첫 호출 | p50 | max |
| --- | --- | --- | --- |
| 캐시 씀 (`faq-vpn`) | 2.07초 | **0.05초** | 2.07초 (첫 호출) |
| 캐시 안 씀 (`faq-vpn-nocache`) | — | 1.99초 | 2.24초 |
- 같은 질문 5번 중 첫 번만 LLM(2.07초), 나머지 넷은 캐시(0.04~0.06초). 그래서 p50은 0.05초인데 p95 · max는 첫 호출 2.07초입니다.
- 그 뒤 `/cache-stats`는 `hits 9 · misses 1`이었습니다. `no_cache` 요청도 캐시를 **조회는** 하므로(쓰지는 않음) hits로 셉니다. 숫자가 무엇을 세는지 먼저 확인합니다.
**어림 계산.** 캐시에서 오면 0.05초, 아니면 2.0초(위 표)라고 하면:
| 적중률 | 평균 (어림) | 계산 |
| --- | --- | --- |
| 0% | 2.00초 | 1.0 × 2.0 |
| 10% | 1.81초 | 0.1 × 0.05 + 0.9 × 2.0 |
| 30% | 1.42초 | 0.3 × 0.05 + 0.7 × 2.0 |
| 60% | 0.83초 | 0.6 × 0.05 + 0.4 × 2.0 |
- 하루 질문이 모두 다른 글자라면 질문 캐시의 적중률은 0%에 가깝습니다. 사람들은 같은 것을 **다른 말로** 묻습니다.
- 그래서 다음 절의 질문: **무엇을 키로** 할까?
## 5. 캐시 키 설계 (4교시 Lab 02 과제 2)
### 일반 설명
키를 **넓게** 잡으면(여러 질문이 같은 키) 적중이 늘고, **좁게** 잡으면 잘못 맞을 위험이 줄어듭니다. 키 설계는 이 둘의 저울질입니다.
- 정규화(공백 · 대소문자 정리)는 거의 위험 없이 적중을 조금 올립니다.
- 중간 결과로 키를 잡을 수도 있습니다 (오늘: FAQ 번호). 다른 말로 물어도 같은 자료로 답하는 질문이 모입니다.
- **의미 캐시(semantic cache)**: 질문을 임베딩으로 바꿔 가까운 질문이면 같은 답을 씁니다. 적중은 크게 늘지만 "비슷하지만 다른 질문"에 같은 답이 나갈 수 있어, 얼마나 가까워야 같은 것으로 볼지(임계값)를 시험으로 정합니다.
- 잘못 맞은 답(false hit)의 비용이 클수록(돈 · 개인 정보 · 안전) 키를 좁게 잡습니다.
### 오늘 실습에서
같은 답을 받아야 할 질문들이 같은 키를 갖게 하는 것이 키 설계입니다.
| 키 | 같은 키가 되는 질문 | 적중 | 잘못 맞을 위험 | 오늘 |
| --- | --- | --- | --- | --- |
| 질문 글자 그대로 | 글자가 똑같아야 | 아주 낮음 | 없음 | — |
| 질문을 고친 글자 (trim · 소문자 · 공백) | 띄어쓰기 · 대소문자만 다른 질문 | 낮음 | 거의 없음 | Lab 02 예제 2 |
| **FAQ 번호** (`faq-FAQ-003`) | 같은 FAQ로 답하는 질문 모두 | 높음 | 답이 질문마다 다르지 않음 | Lab 02 과제 2 |
| 뜻 (임베딩으로 가까운 질문) | 비슷한 뜻의 질문 | 높음 | 비슷하지만 다른 질문에 같은 답 | 오늘 하지 않음 |
FAQ 번호 캐시는 FAQ 검색이 끝나야 번호를 알 수 있으므로 **두 번째 캐시**로 둡니다.
```mermaid
flowchart LR
Q["질문"] --> C1{"질문 캐시"}
C1 -->|있음| A1["답 · cache_by question"]
C1 -->|없음| D["FAQ · 접수 조회<br/>(수십 ms)"] --> R["규칙"] --> C2{"FAQ 번호 캐시<br/>faq-FAQ-003"}
C2 -->|있음| A2["답 · cache_by faq<br/>LLM 0번"]
C2 -->|없음| L{{"LLM"}} --> S["두 키에 저장"]
```
10/24 Lab 02 과제 2 정답으로 과제 2 테스트 `cases/lab02-challenge2.json` (캐시를 비운 뒤 차례로):
| 질문 | 결과 | 시간 |
| --- | --- | --- |
| "VPN 접속이 자꾸 끊겨요. 어떻게 하나요?" (처음) | `cached: false` | 2.5초 (push 직후 첫 호출) |
| "VPN 연결이 계속 끊어지는데요" (다른 말) | `cached: true`, `cache_by: "faq"`, `llm_calls: 0` | **0.1초** |
| 첫 질문을 띄어쓰기만 바꿔 | `cached: true`, `cache_by: "question"` | 0.0초 |
| 다른 말 + `no_cache` | `cached: false` | 1.2초 |
- Lab 02 과제 2를 하기 전 워크플로우(과제 1 정답)로는 "다른 말"이 `cached: false`, LLM 1번, 1.2초였습니다 (10/24, 4/6).
- 데이터 조회는 하고 LLM만 건너뜁니다. 그래서 질문 캐시(0.1초 이하)보다는 조금 느리지만 LLM보다는 훨씬 빠릅니다.
- **잃는 것**: FAQ 번호 캐시의 답은 **처음 물은 사람의 질문**에 맞춰 쓴 답입니다. "VPN이 끊겨요"에 맞춘 답이 "VPN 설치는 어떻게 해요?"에도 나갈 수 있습니다 (둘 다 FAQ-003). 답이 FAQ 원문 요약이면 괜찮고, 질문마다 달라야 하면 이 캐시는 맞지 않습니다.
- 접수 현황은 여기서도 캐시하지 않습니다. 사람마다 · 시간마다 다르니까요.
## 6. 검토 LLM을 검사 코드로 (4교시 Lab 02 과제 1)
### 일반 설명
LLM의 답을 검사하는 방법은 둘입니다.
| 방법 | 잘하는 것 | 약한 것 |
| --- | --- | --- |
| **코드 검사** (가드레일 · 검증기) | 빠름, 늘 같음, 설명 가능: 빈 답, 길이, 형식(JSON 모양), 금지어, 언어 | 뜻은 모름 |
| **LLM 검토** (LLM-as-judge) | 뜻을 봄: 사실과 맞나, 말투, 없는 약속 | 느리고 비쌈, 흔들림, 검토가 오히려 사실을 바꿔 쓸 수 있음 |
- 업계에서는 **코드 검사를 먼저** 두고, 뜻을 꼭 봐야 하는 업무에만 LLM 검토를 남기는 경우가 많습니다.
- 말투 같은 것은 검토로 고치기보다 처음 답을 쓰는 지시문으로 정하는 것이 싸고 빠릅니다.
### 오늘 실습에서
검토 LLM이 한 일을 Executions에서 비교하면 대부분 **말투만** 바꿨습니다. 그런데 10/24에는 접수 상태 "처리 중"을 "승인 대기 중"으로 바꿔 써서 Lab 01 예제 2의 테스트 `cases/lab01.json`이 한 번 실패했습니다. 말투는 답 LLM의 지시문("합니다체, 3문장 이내")으로 정하고, 셀 수 있는 것은 코드가 봅니다.
| 검사 | 코드 | 걸리면 |
| --- | --- | --- |
| 빈 답 | `answer.trim() === ''` | `review_problems`에 `empty` |
| 한국어인가 | `/[가-힣]/.test(answer)` | `not_korean` |
| 너무 김 | `answer.length > 400` | 400자로 자르고 `too_long` |
캐시 없이 같은 VPN 질문 5번:
| 날 | Lab 02 예제 2까지 (규칙 + 캐시, LLM 2번) | Lab 02 과제 1: 검토를 검사로 (LLM 1번) |
| --- | --- | --- |
| 10/18 | p50 2.01초 · p95 2.23초 | p50 **1.15초** · p95 1.16초 |
| 10/23 | p50 2.39초 · p95 2.84초 | p50 **1.32초** · p95 1.35초 |
| 10/24 | p50 2.22초 · p95 2.45초 | p50 **1.12초** · p95 1.32초 |
- 날마다 숫자는 다르지만 **LLM 하나를 빼면 약 1초 줄어든다**는 것은 같습니다 (1단계 7절 어림과 맞음).
- Lab 02 과제 1 테스트 `cases/lab02-challenge.json`은 2.5초 안을 기대합니다. 10/24 15번 모두 통과했습니다 (`--repeat 3`, 0.9~1.6초).
## 7. 잃은 것도 적는다
### 일반 설명
성능을 위한 변경은 거의 늘 무언가와 맞바꿉니다 (속도 ↔ 정확도, 속도 ↔ 최신성, 속도 ↔ 단순함). 개발 팀은 이것을 **설계 결정 기록(ADR, Architecture Decision Record)** 으로 남깁니다: 무엇을 정했나, 왜, 다른 선택지는, 무엇을 잃었나.
- 몇 달 뒤 누군가 "왜 검토가 없지?"라고 물을 때, 기록이 없으면 같은 논의를 처음부터 다시 합니다.
- 잃은 것을 적어 두면 상황이 바뀌었을 때(예: 규제 업무가 추가됨) 무엇을 되돌려야 할지 바로 압니다.
### 오늘 실습에서
| 바꾼 것 | 얻은 것 | 잃은 것 |
| --- | --- | --- |
| 규칙 먼저 | LLM 1번 줄임 | 규칙이 틀리면 엉뚱한 분류 (규칙을 테스트로) |
| 질문 캐시 | 같은 질문 0.1초 | 오래된 답 · 남의 답 위험, 테스트가 복잡해짐 |
| FAQ 번호 캐시 | 다른 말로 물어도 LLM 0번 | 답이 질문에 맞춰지지 않음 |
| 검토 → 검사 | LLM 1번 줄임 | 뜻을 보는 검토는 사라짐 (69일차의 "사실에 없는 약속" 검사) |
Lab 02 과제 1 · 과제 2를 마칠 때 `my-work/PERF.md` 3절에 바꾼 것마다 한 줄씩, **잃은 것까지** 적습니다. 운영에서 "왜 이렇게 했지?"라고 물을 때 답이 됩니다.
## 8. 캐시는 테스트도 바꾼다
### 일반 설명
테스트끼리 **상태를 나눠 쓰면** 순서에 따라 결과가 바뀝니다 (앞 테스트가 남긴 캐시 · 데이터). 이렇게 "가끔 실패하는" 테스트를 플래키(flaky) 테스트라고 하고, 쌓이면 아무도 테스트를 믿지 않게 됩니다.
- **격리**: 테스트 앞에서 상태를 비우거나(초기화), 테스트마다 다른 키를 씁니다.
- **겉으로 드러난 결과를 본다**: "LLM을 3번 불렀나" 같은 내부 구조를 기대하면, 정당하게 고친 코드도 실패합니다. 품질 테스트는 답의 내용을, 구조는 그 구조를 만든 단계의 테스트에서만 봅니다.
- LLM 문장은 여러 표현을 허용하는 기대로 씁니다 (여러 낱말 중 하나, 숫자 · 고유명사처럼 바뀌면 안 되는 것만 정확히).
### 오늘 실습에서
> 수업 준비 때 "규칙으로 모르는 질문"이 캐시에 들어가 다음 테스트에서 0.1초로 나왔습니다. 그래서 테스트에 `no_cache: true`, 테스트 전에 캐시 비우기(`before`)를 둡니다.
| 테스트를 흔드는 것 | 오늘 한 일 |
| --- | --- |
| 앞 테스트가 남긴 캐시 | `cases/lab02*.json` 맨 위 `before`에 `/cache-clear` |
| 재는 요청이 캐시에 맞음 | `no_cache: true` (Lab 02 예제 3 · 과제 1의 `bench`가 쓰는 `requests/faq-vpn-nocache.json`) |
| 구조를 보는 기대 (`llm_calls: 3`, `steps`에 `review`) | 품질 테스트는 **답의 내용**만, 구조는 그 단계 테스트에서만 |
| LLM 문장 | 여러 표현 중 하나 (`찾지 못` · `확인되지` · `없습니다` · `존재하지`) |
> 수업 준비 때 처음 쓴 품질 테스트에 `llm_calls: 3`, `steps`에 `review` 같은 **구조**가 들어 있어 정당하게 빨라진 워크플로우가 실패했습니다.
## 9. 요청문으로
Lab과 다른 업무로 예를 듭니다 (사내 식당 메뉴 안내 워크플로우, **실습에는 없는 예**). Lab 02 예제 1 · 2 · 과제 1 · 2의 요청문을 쓸 때 같은 칸을 채웁니다.
| 넣을 것 | 예 |
| --- | --- |
| 무엇을 줄이나 | "날짜가 있으면 규칙으로, 없을 때만 LLM이 날짜를 읽게" |
| 캐시 키 | "날짜 + 식당 이름. 질문 글자가 아니라" |
| 캐시하지 않는 것 | "오늘 남은 수량 (바뀜)" |
| 결과에 남길 것 | `cached`, `cache_by`, `llm_calls` |
| 품질 확인 | "빨라진 뒤에도 원래 테스트가 통과" |
> "빠르게 해 줘"가 아니라 **"이 LLM 호출을 이 조건일 때 건너뛰어 줘"** 처럼 한 가지씩.
## 확인 질문
<details><summary>1. 캐시를 붙였더니 같은 질문은 0.1초가 됐습니다. 하루 평균은 얼마나 줄까요?</summary>
적중률에 달렸습니다. 같은 질문이 하루 10%라면 평균 2.0초 → 약 1.81초 (4절 표). 적중률을 먼저 잽니다 (`/cache-stats`의 hits · misses).
</details>
<details><summary>2. "T-1002 상태"를 FAQ 번호 캐시처럼 접수 번호로 캐시하면 안 되나요?</summary>
안 됩니다. 접수 상태는 시간에 따라 바뀌고, 사람마다(본인 건) 다릅니다. 캐시해도 되는 것은 **같은 키면 언제 · 누구에게나 같은 답**인 것뿐입니다.
</details>
<details><summary>3. 검토 LLM을 지웠더니 1초 빨라지고 테스트도 통과했습니다. 끝인가요?</summary>
잃은 것을 적어야 끝입니다. 검토가 하던 "뜻을 보는" 일(사실에 없는 약속, 무례한 말투)은 이제 아무도 안 봅니다. 그것이 필요한 업무라면 검토를 남기고 다른 곳을 줄입니다.
</details>
## 정리
- [ ] 병목인 LLM 호출을 규칙 · 캐시 · 검사 코드로 줄인다
- [ ] 캐시하면 안 되는 답이 있다 (바뀌는 것, 사람마다 다른 것)
- [ ] 캐시의 효과는 적중률이 정한다. 적중률은 키 설계가 정한다
- [ ] 한 가지씩 바꾸고, 바꿀 때마다 같은 날 다시 잰다
- [ ] 빨라진 만큼 품질을 원래 테스트로, 잃은 것도 적는다
+376
View File
@@ -0,0 +1,376 @@
# 3단계 · 운영 관점의 안정성
- 교시: 5교시 강의 (14:00~14:50) · 6교시 [Lab 03](../../Last3Mon/72/LAB-03-Q-STABILITY.md) 실습 5개 (15:00~15:50)
- 이 단계가 끝나면: 데이터 API나 LLM이 느려지거나 멈췄을 때 정해진 시간 안에 정해진 답을 하도록 timeout · 재시도 · 대체 경로를 넣고, 일부만 멈춘 고장(부분 고장)을 그 부분만 넘기고, 차단기로 멈춘 곳을 잠깐 부르지 않고, 무엇이 멈췄는지 알려 주는 상태 점검을 만들 수 있다.
- 숫자: 강사 노트북 실측 (10/18 수업 준비, 10/23~24 다시 잼). 고장은 72일차 데이터 API의 고장 스위치(`flow.py fault`)와 `flow.py llm down`으로 일부러 냈습니다.
- 실습 가리키기: "Lab 03 예제 1"은 6교시 [Lab 03 문서](../../Last3Mon/72/LAB-03-Q-STABILITY.md)의 그 절, "Lab 02 …"는 4교시 실습입니다. 파일 경로는 실습 폴더 `~/aio-01d4-stu/Last3Mon/72` 기준이고, `PERF.md`는 Lab 01을 시작할 때 만든 내 보고서 `my-work/PERF.md`입니다.
```mermaid
flowchart LR
A["1. 멈추는 방식"] --> B["2. timeout ·<br/>재시도 · 오류 출력"] --> C["3. 최악의 시간"] --> D["4. 옵션을 재 보고 쓰기"]
D --> E["5. LLM 없이 답하기"] --> F["6. 부분 고장"] --> G["7. 차단기"] --> H["8. 상태 점검"] --> I["9. 고장 연습"]
```
## 0. 이 시간 (50분)
| 시간 | 절 · 개요 | 하는 일 |
| --- | --- | --- |
| 14:00~14:07 | 1 · 개요 22장 | 멈추는 모양 넷, 연쇄 장애, 고치기 전에 겪은 일 |
| 14:07~14:17 | 2 · 3 · 개요 23장 | timeout · 재시도 · backoff · 멱등성 · 오류 출력, 최악의 시간 |
| 14:17~14:21 | 4 · 개요 24장 | 옵션 직접 확인, `neverError` · `fullResponse` 실측 |
| 14:21~14:29 | 5 · 6 · 개요 25~26장 | 대안(fallback), LLM 없이 답하기, 우아한 저하 · 격벽, 접수 API만 멈췄을 때 |
| 14:29~14:40 | 7 · 8 · 개요 27~28장 | 차단기 3상태와 실측, 상태 점검 |
| 14:40~14:45 | 9 | 카오스 엔지니어링, 고장 연습 명령 · 규칙 |
| 14:45~14:50 | 확인 질문 | 질문 |
### 6교시 Lab 03에서 이어서 하는 것
바탕은 Lab 02까지 고친 `my-work/helpdesk.json`입니다 (없으면 `cp reference/lab02-example2-cache.json my-work/helpdesk.json` 후 push). 실습마다 **고치기 전에 고장을 먼저 겪고** 고칩니다.
| 6교시 실습 (시각) | 이 강의의 절 | 실습에서 하는 일 | 쓰는 파일 · 명령 |
| --- | --- | --- | --- |
| Lab 03 예제 1 (15:00~15:10) | 1 · 2 · 3 · 4 | FAQ 느림 · 오류를 겪고 timeout · 재시도 · 오류 출력 → `Data down` | `fault faq slow` · `error`, 테스트 `cases/lab03.json` → `PERF.md` 4절 |
| Lab 03 예제 2 (15:10~15:20) | 5 | LLM이 멈추면 데이터로 `간단 답변` | `llm down` · `up`, 테스트 `cases/lab03-llm-down.json` |
| Lab 03 예제 3 (15:20~15:30) | 4 · 6 | 접수 API만 멈출 때 그 부분만 넘기기 (`fullResponse` · `Ticket check`) | `fault tickets error`, 테스트 `cases/lab03-ticket.json` → `PERF.md` 4절 |
| Lab 03 과제 1 (15:30~15:40) | 8 | 상태 점검 워크플로우 `my-health` | 새 파일 `my-work/health.json`, 테스트 `cases/lab03-challenge.json` → `PERF.md` 5절 |
| Lab 03 과제 2 (15:40~15:50) | 7 · 9 | 차단기 `breaker-faq` (30초) | 테스트 `cases/lab03-challenge2.json` → `PERF.md` 4~6절, `my-work/TEST-REPORT.md` |
## 1. 멈추는 방식
### 일반 설명
외부 서비스(데이터베이스, API, LLM)는 반드시 언젠가 멈춥니다. 멈추는 모양은 크게 넷입니다.
| 모양 | 뜻 | 예 |
| --- | --- | --- |
| 오류 (fail-fast) | 바로 실패를 알려 줌 | HTTP 500, 연결 거부 |
| **느림** (fail-slow) | 답은 오는데 아주 늦게, 또는 끝없이 | 과부하, 잠금 대기 |
| 일부만 | 기능 하나만 멈춤 | 접수 API만 고장 |
| 계속 | 한동안 멈춘 상태가 이어짐 | 장애 30분 |
- **느림이 오류보다 위험한** 경우가 많습니다. 기다리는 동안 일꾼(스레드 · 실행)이 붙잡혀 다른 요청도 밀리고, 결국 멀쩡한 기능까지 느려집니다. 이것이 번지는 것을 **연쇄 장애(cascading failure)** 라고 합니다.
- 그래서 안정성 설계의 첫 원칙은 **빨리 실패하기(fail fast)**: 오래 기다리지 말고, 정해진 시간 안에 정해진 답을 합니다.
### 오늘 실습에서
일부러 고장을 내 본 결과 (고치기 전, 4교시 Lab 02 예제 2 정답 `reference/lab02-example2-cache.json`). Lab 03의 각 실습 첫머리에서 각자 같은 고장을 먼저 겪습니다:
| 고장 | 사용자가 겪은 것 | 날 |
| --- | --- | --- |
| FAQ가 느림 (8초 뒤 답) | **9.0초** · **13.2초** · **11.5초** 기다린 뒤 평소 답 (기다리는 한도가 없음) | 10/18 · 10/23 · 10/24 |
| FAQ가 오류 (500) | 0.1~0.2초 만에 **HTTP 500** `Error in workflow` | 10/18 · 10/23 |
| LLM 키가 틀림 | 1.3초 만에 **HTTP 500** | 10/23 |
| 접수 API만 오류 (500) | 접수 질문도, **FAQ 질문도** `담당자 연결` (Lab 03 예제 2까지 고친 워크플로우, 6절) | 10/24 |
| 멈추는 방식 | 무엇이 나쁜가 |
| --- | --- |
| 느림 | 사용자는 기다리고, 그동안 n8n도 일을 붙잡고 있음. 오류보다 나쁠 수 있음 |
| 오류 (500) | 사용자에게 아무것도 알려 주지 않음 |
| 일부만 멈춤 | 멀쩡한 기능까지 같이 멈추면 고장이 커 보임 |
| 계속 멈춤 | 사람마다 같은 시간을 기다리고, 멈춘 서비스에 요청이 계속 감 |
오늘 3단계는 이 넷을 차례로 다룹니다: timeout(느림) → 오류 출력과 정해진 답(오류) → 부분 고장 → 차단기(계속 멈춤).
## 2. timeout · 재시도 · 오류 출력 (6교시 Lab 03 예제 1)
### 일반 설명
밖을 부르는 모든 곳에 세 가지를 정합니다.
- **timeout**: 얼마나 기다릴지. 없으면 끝없이 기다릴 수 있습니다. 보통 그 서비스의 평소 p99보다 조금 길게 잡습니다.
- **재시도(retry)**: 잠깐 끊긴(일시적) 고장에만 효과가 있습니다. 계속 고장 난 곳에 재시도하면 기다림만 늘고, 힘든 서비스에 요청을 더 보내 회복을 늦춥니다(재시도 폭풍).
- 재시도 사이 기다림은 **지수적으로 늘리고**(0.5초 → 1초 → 2초, exponential backoff) **조금씩 무작위로 흩어**(jitter) 모두가 같은 순간에 다시 부르지 않게 합니다.
- **같은 요청을 두 번 보내도 결과가 같은가(멱등성, idempotency)** 를 먼저 봅니다. 조회(GET)는 괜찮지만 "주문 만들기"(POST)를 재시도하면 주문이 두 번 생길 수 있습니다. 이럴 때는 요청마다 고유 키(idempotency key)를 붙여 서버가 중복을 알아보게 합니다.
- **실패했을 때 갈 곳**: 오류를 그대로 사용자에게 던지지 않고, 정해진 답 · 담당자 연결 · 간단한 답으로 보냅니다. 그리고 왜 그 길로 갔는지(`reason`)를 남겨 나중에 셀 수 있게 합니다.
### 오늘 실습에서
```mermaid
flowchart LR
H["HTTP Request<br/>timeout 2초"] -->|실패| R["0.5초 뒤<br/>한 번 더"]
R -->|성공| N["다음 단계"]
R -->|실패| E["오류 출력 →<br/>담당자 연결"]
H -->|성공| N
```
| 설정 | n8n | 오늘 | 어디에 쓰나 |
| --- | --- | --- | --- |
| 기다리는 한도 | `options.timeout` (ms) | 2000 | `parameters.options` 안 |
| 다시 해 보기 | `retryOnFail` · `maxTries` · `waitBetweenTries` | 2번, 0.5초 | 노드 바로 아래 (`parameters` 밖) |
| 실패해도 멈추지 않기 | `onError: continueErrorOutput` | 오류 출력 → `Data down` | 노드 바로 아래, 연결은 `main`의 1번 칸 |
- timeout이 없으면 n8n은 **끝없이** 기다립니다. 외부를 부르는 노드에는 모두 timeout을 둡니다.
- 정해진 답에는 `reason`(`data_api_down`)을 둡니다. 운영자가 나중에 "오늘 데이터 고장으로 넘긴 것이 몇 건"인지 셀 수 있게.
- 캐시를 FAQ 조회 **앞에** 둔 덕에, FAQ가 멈춰도 캐시에 있는 질문은 0.1초에 답했습니다 (10/23 Lab 03 예제 1 테스트 `cases/lab03.json` 4/4).
## 3. 최악의 시간 계산
### 일반 설명
재시도까지 넣었으면 **최악의 시간**을 미리 계산합니다.
> 최악 ≈ 시도 횟수 × timeout + (시도 횟수 − 1) × 재시도 사이 기다림
- 이 값이 사용자 쪽 시간 예산(1단계 9절)보다 길면 설정이 틀린 것입니다.
- 여러 층이 겹치면 **바깥 timeout이 안쪽보다 길어야** 합니다. 안쪽이 아직 재시도 중인데 바깥이 먼저 포기하면, 안쪽 일은 헛수고가 되고 결과도 버려집니다.
- 큰 시스템은 요청에 "남은 시간"(deadline)을 실어 보내, 안쪽 단계가 남은 시간보다 오래 기다리지 않게 합니다.
### 오늘 실습에서
| 설정 | 계산 | 최악 |
| --- | --- | --- |
| timeout 2초, 2번, 기다림 0.5초 | 2 + 0.5 + 2 | **4.5초** |
| timeout 2초, 3번, 기다림 0.5초 | 2 + 0.5 + 2 + 0.5 + 2 | 7.0초 |
| timeout 5초, 2번, 기다림 1초 | 5 + 1 + 5 | 11초 |
- 실측: FAQ 느림에서 **4.6~4.7초** 뒤 `담당자 연결` (10/18 4.6초, 10/23 4.7초). 계산 4.5초 + 다른 단계.
- Lab 03 예제 1 테스트 `cases/lab03.json`의 FAQ 느림 케이스는 7초 안을 기대합니다. retry를 3번으로 늘리면 7.0초라 아슬아슬합니다.
- **재시도는 공짜가 아닙니다.** 느린 서비스에 재시도를 늘리면 기다림도 늘고, 이미 힘든 서비스에 요청을 더 보냅니다. 재시도는 "잠깐 끊긴" 고장에 쓰는 것이지 "계속 느린" 고장에 쓰는 것이 아닙니다 (그것은 7절 차단기).
## 4. 옵션은 재 보고 쓴다: `neverError` · `fullResponse`
### 일반 설명
라이브러리 · 도구의 옵션은 **이름이 주는 느낌과 실제 동작이 다를** 수 있고, 판(version)마다 바뀌기도 합니다. 안정성에 쓰는 옵션은 특히 그렇습니다. 고장이 났을 때만 동작하므로 평소에는 틀린 줄 모릅니다.
- 문서를 읽고, **작은 시험으로 직접 확인**합니다: 일부러 404 · 500 · 느림을 만들어 어디로 가는지, 몇 초 걸리는지.
- 확인한 판번호와 결과를 적어 둡니다. 판을 올리면 다시 확인합니다.
### 오늘 실습에서
캐시 조회는 "없음"을 404로 받습니다. 404에서 워크플로우가 멈추지 않게 `neverError`를 켰습니다. 그런데 이 옵션은 정확히 무엇을 하나요? 문서를 읽는 것만으로는 헷갈려서 **작은 시험 워크플로우로 재 봤습니다** (10/24, `retryOnFail` 3번 · 기다림 1초, 접수 API. 강사가 따로 잰 것으로 실습에는 없습니다. 이 결과를 Lab 03 예제 3에서 씁니다).
| 설정 | 404 (없는 번호) | 500 (접수 API 고장) |
| --- | --- | --- |
| 없음 | 재시도 뒤 **오류 출력** (2.1초) | 재시도 뒤 오류 출력 (2.1초) |
| `neverError` | 재시도 뒤 **보통 출력** (2.2초) | 재시도 뒤 **오류 출력** (2.2초) |
| `neverError` + `fullResponse` | 바로 보통 출력, `statusCode: 404` (0.1초) | 바로 보통 출력, `statusCode: 500` (0.1초) |
배운 것:
- 이 n8n(2.42.6)에서 `neverError`는 4xx만 보통 결과로 받고, **500은 여전히 실패**입니다. 수업 준비 때(10/18) 교안에는 "500도 성공으로 본다"고 적었는데, 재 보니 틀렸습니다. **옵션은 이름이나 기억으로 짐작하지 않고 잽니다.**
- `neverError`라도 `retryOnFail`이 켜져 있으면 **404도 재시도를 다 하고** 보통 결과로 받습니다. 시간을 버립니다.
- `fullResponse`를 함께 켜면 상태 코드를 받아 **워크플로우가 직접** 나눌 수 있습니다. 대신 결과 모양이 `{statusCode, body}`로 바뀌어 뒤에서 읽는 곳도 바꿔야 합니다.
## 5. LLM 없이 답하기 (6교시 Lab 03 예제 2)
### 일반 설명
핵심 서비스가 멈췄을 때 쓸 **대안(fallback)** 을 미리 정합니다. 흔한 순서는 이렇습니다.
| 순서 | 대안 | 대가 |
| --- | --- | --- |
| 1 | 다른 모델 · 다른 회사 LLM | 키 · 비용 · 품질을 하나 더 관리 |
| 2 | 저장해 둔 답 (캐시) | 있는 질문만 |
| 3 | 데이터 원문 그대로 · 정해진 답 | 덜 친절함 |
| 4 | 사람에게 넘기기 | 사람 시간 |
- 대안으로 답할 때는 **평소와 같은 척하지 않습니다.** "지금은 간단히 안내합니다"를 알리고, 결과에도 구분(`route`)을 남깁니다.
- 대안도 테스트합니다. 쓰지 않던 대안은 막상 필요할 때 고장 나 있기 쉽습니다.
### 오늘 실습에서
LLM이 실패하는 시점에는 데이터(FAQ · 접수 건)를 이미 조회했습니다.
| 질문 | LLM이 멈췄을 때 | `route` | 10/23 |
| --- | --- | --- | --- |
| FAQ가 있음 | FAQ 원문 + "자동 답변 기능을 잠시 쓸 수 없어 FAQ 원문으로 안내합니다." | `간단 답변` | 3.6초 (분류 LLM이 실패할 때까지) |
| 접수 번호 | "T-1002 접수 건은 현재 '처리 중' 상태입니다. …" | `간단 답변` | 0.4초 |
| 그 밖 | 담당자에게 | `담당자 연결` | 1.2초 |
- `route`를 다르게 두면 운영자가 "오늘 간단 답변이 몇 건이었나"를 셀 수 있습니다 (77일차 로그 분석).
- 다른 모델 · 다른 회사의 LLM으로 바꾸는 방법도 있지만, 키 · 비용 · 품질을 또 관리해야 합니다. **데이터로 답할 수 있는 것은 데이터로.**
- Lab 03 예제 2 테스트 `cases/lab03-llm-down.json`: 고치기 전 1/4 (빈 질문만), 고친 뒤 4/4.
- 사용자에게 **평소 답과 같은 척하지 않습니다.** "잠시 쓸 수 없다"를 알립니다.
## 6. 부분 고장: 멈춘 부분만 넘기기 (6교시 Lab 03 예제 3)
### 일반 설명
**우아한 저하(graceful degradation)** 는 일부가 고장 나도 전체를 멈추지 않고, 고장 난 기능만 줄여서 계속 서비스하는 것입니다. 쇼핑몰의 추천 기능이 멈춰도 검색 · 결제는 되게 하는 것이 예입니다.
- 이것을 위한 설계를 **격벽(bulkhead)** 이라고 부릅니다. 배의 칸막이처럼, 한 칸에 물이 들어도 다른 칸은 멀쩡하게 의존 서비스마다 실패를 따로 다룹니다.
- 흔한 실수는 모든 실패를 **한 군데로** 모으는 것입니다. 그러면 어느 부분이 멈췄든 모든 요청이 같은 실패 길로 갑니다.
- 실패를 "어느 서비스의, 어떤 실패인지"(상태 코드, timeout)로 구분해 받고, 그 서비스가 필요한 요청만 대안으로 보냅니다.
### 오늘 실습에서
접수 API만 멈추면 어떻게 될까요? Lab 03 예제 2까지 만든 워크플로우로 겪어 봤습니다 (10/24). Lab 03 예제 3 첫머리에서 각자 `fault tickets error`로 같은 것을 겪습니다.
| 질문 | Lab 03 예제 2까지 | Lab 03 예제 3 정답 |
| --- | --- | --- |
| 접수 번호 (T-1002) | `담당자 연결`, `reason: data_api_down` (0.6초) | `담당자 연결`, `reason: tickets_down` (0.1초) |
| **FAQ (VPN)** | `담당자 연결` (0.6초) — FAQ는 멀쩡한데 | **`자동 답변`**, 답에 "인증서" (1.1초) |
| 없는 번호 (T-9999, 평소) | `자동 답변` | `자동 답변` (404는 고장이 아님) |
(Lab 03 예제 3 테스트 `cases/lab03-ticket.json`: 예제 2까지의 워크플로우 2/4 → 예제 3 정답 4/4)
```mermaid
flowchart LR
G["Get ticket<br/>fullResponse"] --> C["Ticket check<br/>statusCode 보기"] --> D{"500 이상이고<br/>접수 번호 질문?"}
D -->|예| T["담당자 연결<br/>reason tickets_down"]
D -->|아니오| N["평소 흐름<br/>(FAQ 질문은 그대로 답)"]
```
- 왜 FAQ 질문까지 넘어갔나: 모든 질문이 접수 조회를 지나가고, 오류 출력은 "어느 질문인지"를 모르고 한 길로 보냅니다.
- 고치는 법: 실패를 **보통 결과로 받아**(`fullResponse`) 상태 코드와 질문을 함께 보고 나눕니다.
- 원칙: **고장 난 부분만 줄이고, 나머지는 평소대로.** 이것을 "우아한 저하(graceful degradation)"라고 부릅니다.
- 덤: Lab 03 예제 1에서 `Get ticket`에 재시도를 켠 뒤로, 접수 번호가 없는 질문(`/tickets/NONE` → 404)도 0.5초 기다렸다가 한 번 더 부르고 있었습니다 (4절 표: `neverError`에서도 404를 재시도). `fullResponse`로 바꾸면 404는 바로 옵니다.
| 10/24 VPN 질문의 `data_ms` | 값 |
| --- | --- |
| Lab 02 예제 2 (재시도 없음) | 32~76ms |
| Lab 03 예제 1 정답 (재시도 켬) | **605ms** |
| Lab 03 예제 3 정답 (`fullResponse`) | 38ms |
안정성을 위해 넣은 설정이 **평소 요청을 0.5초 느리게** 만들고 있었습니다. 고친 뒤에는 평소 시간도 다시 잽니다.
## 7. 차단기: 멈춘 곳은 잠깐 부르지 않기 (6교시 Lab 03 과제 2)
### 일반 설명
**차단기(circuit breaker)** 는 전기 차단기에서 온 이름입니다. 계속 실패하는 서비스를 잠시 **부르지 않아서** 사용자의 기다림을 없애고, 힘든 서비스가 회복할 틈을 줍니다 (마이클 나이가드 『Release It!』에서 널리 알려짐).
```mermaid
stateDiagram-v2
[*] --> 닫힘
닫힘 --> 열림: 실패가 기준 이상
열림 --> 반열림: 정한 시간이 지남
반열림 --> 닫힘: 시험 호출 성공
반열림 --> 열림: 시험 호출 실패
```
| 상태 | 하는 일 |
| --- | --- |
| 닫힘 (closed) | 평소처럼 부르고 실패를 셈 |
| 열림 (open) | 부르지 않고 바로 대안 |
| 반열림 (half-open) | 몇 번만 시험 삼아 불러 보고 결과로 닫거나 다시 엶 |
- 정할 것: 몇 번(또는 몇 %) 실패하면 열지, 얼마나 열어 둘지, 반열림에서 몇 번 시험할지.
- 차단기 상태는 요청들이 **함께 보는 곳**에 둡니다 (같은 프로그램의 메모리, 여러 서버라면 Redis 같은 저장소).
- 자바의 resilience4j, 서비스 메시 Envoy · Istio의 outlier detection 같은 도구가 이 일을 합니다.
### 오늘 실습에서
Lab 03 예제 1 뒤에도 FAQ가 계속 느리면, **사람마다** 4.6초씩 기다린 뒤 담당자로 넘어갑니다. 멈춘 것을 이미 알았는데도요.
```mermaid
stateDiagram-v2
[*] --> 닫힘
닫힘 --> 열림: FAQ 실패 (breaker-faq 저장, 30초)
열림 --> 닫힘: 30초 지나 데이터 API가 지움
닫힘: 닫힘 · FAQ를 부름
열림: 열림 · FAQ를 부르지 않고 바로 담당자 연결
```
| 차단기 | 오늘 |
| --- | --- |
| 기억하는 곳 | 데이터 API 캐시 `breaker-faq` (워크플로우는 요청마다 새로 시작하므로, 기억은 바깥에) |
| 여는 때 | FAQ 검색의 오류 출력 (timeout · 500) |
| 열려 있으면 | FAQ를 부르지 않고 바로 `담당자 연결`, `reason: breaker_open` |
| 닫히는 때 | ttl 30초. 그 뒤 첫 질문이 다시 FAQ를 불러 보고, 아직 멈춰 있으면 또 엶 |
10/24 Lab 03 과제 2 정답으로 과제 2 테스트 `cases/lab03-challenge2.json` (FAQ 느림 8초):
| 차례 | 질문 | 결과 | 시간 |
| --- | --- | --- | --- |
| 1 | 평소 VPN | `자동 답변` | 2.7초 (push 직후) |
| 2 | FAQ 느림 · 처음 | `담당자 연결`, `reason: data_api_down` | 4.8초 |
| 3 | FAQ 느림 · 다음 질문 | `담당자 연결`, `reason: breaker_open` | **0.1초** |
| 4 | FAQ가 돌아오고 캐시를 비운 뒤 | `자동 답변` | 1.1초 |
(Lab 03 예제 3 정답으로는 3번이 4.6초 · `data_api_down`이라 3/4)
- 차단기는 사용자를 기다리게 하지 않는 것과 함께, **멈춘 서비스에 요청을 더 보내지 않는** 일도 합니다. 되살아나는 중인 서비스에 요청이 몰리면 다시 넘어집니다.
- 30초는 정한 값입니다. 짧으면 자주 다시 불러 보고(기다리는 사람이 늘고), 길면 되살아난 뒤에도 한동안 담당자로 넘깁니다.
- 질문 캐시에 있는 질문은 차단기를 보기 **전에** 답하므로, 차단기가 열려 있어도 답합니다.
## 8. 상태 점검 (6교시 Lab 03 과제 1: `my-work/health.json`, 웹훅 `my-health`)
### 일반 설명
상태 점검(health check)은 "지금 이 서비스를 믿고 일을 맡겨도 되나"를 묻는 작은 요청입니다. 쿠버네티스는 이것을 세 가지로 나눕니다.
| 점검 | 묻는 것 | 실패하면 |
| --- | --- | --- |
| liveness | 프로그램이 살아 있나 | 다시 시작 |
| readiness | 지금 요청을 받을 준비가 됐나 (의존 서비스 포함) | 요청을 보내지 않음 |
| startup | 처음 켜지는 중인가 | 다른 점검을 잠시 미룸 |
- 얕은 점검(살아만 있나)은 싸지만 "살아 있는데 일은 못 하는" 상태를 놓칩니다. 깊은 점검(실제 일과 같은 길)은 정확하지만 비싸므로 짧은 timeout과 적당한 간격으로.
- 점검 결과는 사람과 기계가 함께 읽습니다: 알림 시스템, 부하 분산기, 대시보드. 그래서 **코드가 정한 모양**(`ok` · `degraded` · `down`)으로 돌려줍니다.
- 밖에서 주기적으로 실제 요청을 흉내 내 보내는 것을 합성 점검(synthetic monitoring)이라고 합니다.
### 오늘 실습에서
```mermaid
flowchart LR
W(["my-health"]) --> A["데이터 API<br/>/health"] --> F["FAQ 검색"] --> L{{"LLM 'OK'"}} --> S["Code: ok ·<br/>degraded · down"]
```
| 원칙 | 이유 | 10/23 |
| --- | --- | --- |
| 점검마다 짧은 timeout | 점검이 8초를 기다리면 점검이 아님 | FAQ 느림에서도 점검 3.1초 |
| 하나가 실패해도 다음 점검 | 무엇이 멀쩡한지까지 알아야 함 (`continueRegularOutput`) | FAQ 오류 → `degraded` 1.0초 |
| 상태는 코드가 | LLM이 멈췄을 수도 있음 | LLM 멈춤 → `degraded` 0.9초 |
| 실제 일과 같은 길로 | `/health`만 보면 FAQ 고장을 모름 | `/health`는 고장 스위치와 상관없이 ok |
| `status` | 기준 (Lab 03 과제 1 정답) |
| --- | --- |
| `ok` | 셋 모두 참 |
| `degraded` | 데이터 API나 LLM 중 하나라도 참 (일부는 됨) |
| `down` | 둘 다 거짓 |
- 운영에서는 Schedule Trigger로 5분마다 점검하고 `degraded`면 알립니다 (76일차 에러 처리).
- 점검 결과와 차단기는 함께 씁니다: 점검이 FAQ를 `false`로 보면 차단기를 미리 열어 둘 수도 있습니다 (오늘은 하지 않음).
## 9. 고장 연습
### 일반 설명
**카오스 엔지니어링(chaos engineering)** 은 일부러 고장을 내서 시스템이 예상대로 버티는지 확인하는 방법입니다. 넷플릭스의 Chaos Monkey(운영 서버를 무작위로 끔)가 유명하고, 클라우드에는 AWS Fault Injection Service 같은 도구도 있습니다. 팀이 날을 잡아 함께 하는 연습을 게임 데이(game day)라고 부릅니다.
- 순서: **가설**("FAQ가 멈춰도 5초 안에 담당자 연결") → 작은 범위로 고장 내기 → 관찰 · 기록 → 되돌리기 → 고치기.
- 고장의 범위(blast radius)를 작게 시작하고, **언제든 멈출 수 있는 스위치**를 먼저 준비합니다.
- 연습이 끝나면 반드시 원래대로 돌리고 확인합니다. 되돌리지 않은 연습 고장이 진짜 장애가 됩니다.
### 오늘 실습에서
| 명령 | 하는 일 |
| --- | --- |
| `flow.py fault faq slow` · `error` · `fault tickets error` · `fault all ok` | 데이터 API 고장 스위치 |
| `flow.py llm down` · `llm up` | OpenAI Credential을 틀린 키로 · 되돌리기 (재시작 없이 바로) |
| `curl -s -XPOST localhost:8800/cache-clear` | 캐시 · 차단기 비우기 |
- Lab 03 테스트 파일(`cases/lab03*.json`)은 `before`로 고장을 켜고 `after`로 **반드시** 끕니다 (실패해도). 손으로 고장을 켰다면 실습이 끝날 때 `fault all ok` · `llm up`.
- 운영에서 처음 겪는 고장은 비쌉니다. 일부러, 미리, 정해진 시간에 연습합니다.
- 연습 순서: **고치기 전에 먼저 겪는다** → 고친다 → 같은 고장으로 다시 겪는다 → 평소 테스트도 다시.
- 끝나면 `doctor` → `ready` (고장 스위치 · OpenAI 키가 평소대로).
## 확인 질문
<details><summary>1. timeout 3초, 재시도 3번, 기다림 1초면 최악은 몇 초인가요? Lab 03 예제 1 테스트 `cases/lab03.json`의 7초 안 기대를 지키나요?</summary>
3 + 1 + 3 + 1 + 3 = 11초. 지키지 못합니다. 느린 고장에 재시도를 늘리면 기다림이 늘어납니다. 계속 느리면 차단기(7절)를 씁니다.
</details>
<details><summary>2. 접수 API만 멈췄는데 VPN 질문도 담당자로 갔습니다. 왜, 그리고 어떻게 고치나요?</summary>
모든 질문이 접수 조회를 지나가고, `Get ticket`의 오류 출력이 질문을 가리지 않고 `Data down`으로 보냈기 때문입니다. `fullResponse`로 상태 코드를 보통 결과로 받아, "500이고 접수 번호 질문"일 때만 담당자로 보냅니다.
</details>
<details><summary>3. 차단기를 n8n 워크플로우 안의 변수로 기억하면 안 되나요?</summary>
웹훅 워크플로우는 요청마다 새로 시작해 앞 요청의 값을 갖고 있지 않습니다. 그래서 바깥(오늘은 데이터 API 캐시, 운영에서는 Redis 같은 저장소)에 둡니다. 저장할 때 ttl을 주면 닫는 일도 저절로 됩니다.
</details>
## 정리
- [ ] 느림 · 오류 · LLM 멈춤 · 일부만 멈춤을 일부러 겪어 본다
- [ ] timeout · 재시도 · 오류 출력으로 정해진 시간 안에 정해진 답, 최악의 시간을 계산한다
- [ ] 옵션은 짐작하지 않고 재 본다 (`neverError`는 500을 막지 않는다)
- [ ] 데이터로 답할 수 있으면 LLM 없이, 멈춘 부분만 넘기고, `route` · `reason`으로 구분
- [ ] 계속 멈추면 차단기로 잠깐 부르지 않는다
- [ ] 상태 점검은 짧게, 끝까지, 코드로
> 다음: 8교시 [4단계 정리](04-STAGE4-WRAPUP.md) — 1~3단계 전체의 의미 · 효과 · 회사에서 쓰는 법.
+210
View File
@@ -0,0 +1,210 @@
# 4단계 · 정리: 오늘 한 일의 의미 · 효과 · 실전 응용
- 교시: 8교시 17:10~17:45 (35분, 바이브 코딩 발표 10분 뒤) · 자료: 이 문서 + [개요](00-DAY72-OVERVIEW.md) 30장
- 이 시간이 끝나면: 오늘 1~3단계에서 한 일이 **무엇을 위한 것이었는지**, 숫자로 **얼마나 효과가 있었는지**, 회사에서는 같은 생각을 **어떤 이름과 도구로** 쓰는지 말할 수 있고, 최종 프로젝트에 무엇을 가져갈지 정할 수 있다.
- 숫자: 오늘 교안의 실측(강사 노트북, 10/18 · 10/23~24)입니다. "예시"라고 적은 계산은 가정을 둔 어림입니다.
```mermaid
flowchart LR
A["1. 오늘 한 장"] --> B["2. 왜 '운영'인가"] --> C["3. 효과를 숫자로"] --> D["4. 회사에서 부르는 이름"]
D --> E["5. 산업별 응용"] --> F["6. 운영 대시보드"] --> G["7. 하지 말아야 할 때"] --> H["8. 직무 · 면접"] --> I["9. 최종 프로젝트로"]
```
## 0. 이 시간 (35분)
| 시간 | 절 | 하는 일 |
| --- | --- | --- |
| 17:10~17:15 | 1 · 2 | 오늘 한 장, 만들기와 운영하기 |
| 17:15~17:20 | 3 | 오늘 숫자로 본 효과, 하루 1,000건이면 (예시) |
| 17:20~17:28 | 4 · 5 | 같은 생각의 업계 이름 · 도구, 산업별 응용 예 |
| 17:28~17:33 | 6 · 7 | 운영 지표 여섯 개, 하지 말아야 할 때 |
| 17:33~17:40 | 8 · 9 | 직무와 면접 질문, 최종 프로젝트 체크리스트 |
| 17:40~17:45 | 10 · 토론 | 다음 일차와의 연결, 질문 둘 |
## 1. 오늘 한 장
| 단계 | 한 일 | 오늘 숫자 |
| --- | --- | --- |
| 1단계 · 재기 | p50 · p95 · 첫 호출, 단계별 `timings`, 종류별 재기, 시간 예산 | 기준 VPN p50 2.69초 · 첫 호출 4.4초, **LLM이 시간의 98%** |
| 2단계 · 빠르게 | 규칙 먼저, 질문 캐시, 적중률, FAQ 번호 캐시, 검토를 검사로 | 캐시 없이 VPN p50 2.22 → **1.12초**, 캐시 적중 **0.05초**, 다른 말 질문 1.2 → **0.1초** |
| 3단계 · 멈춰도 답하게 | timeout · 재시도 · 오류 출력, LLM 없이 답, 부분 고장, 차단기, 상태 점검 | FAQ 느림 11.5초 → **4.6초** 담당자 연결 → 다음 사람 **0.1초**(차단기), LLM 멈춤 HTTP 500 → `간단 답변` |
| 바이브 코딩 | 재는 도구 `tbench` (TODO → SPEC → IMP-PLAN → 설치) | 요구 14개 |
한 문장으로: **재고 나서 고치고, 고친 만큼 다시 재고, 멈출 때를 미리 정해 둔다.**
## 2. 왜 "운영"인가: 만들기와 운영하기
64~71일차는 **동작하는 워크플로우를 만드는 일**이었습니다. 오늘은 그것을 **날마다 쓰는 사람들 앞에 계속 두는 일**입니다.
| | 만들기 (64~71일차) | 운영하기 (오늘 · 72일차부터) |
| --- | --- | --- |
| 질문 | "답이 맞나?" | "얼마나 빨리, 얼마나 자주 맞나? 멈추면?" |
| 기준 | 테스트 케이스 통과 | 시간 예산 · 실패율 · 비용 · 복구 시간 |
| 실패 | 고치면 끝 | 언젠가 반드시 옴 → **어떻게 실패할지**를 정함 |
| 보는 사람 | 만든 사람 | 쓰는 사람, 운영자, 비용을 내는 사람 |
- 회사에서 AI 서비스가 "시범(PoC)은 좋았는데 실제로 못 쓰는" 까닭은 대개 정확도보다 **느림 · 비용 · 장애 · 설명할 수 없음**입니다. 오늘 한 일은 이 넷을 다룹니다.
- LLM은 같은 질문에도 매번 다른 시간 · 다른 문장으로 답합니다 (오늘 검토 LLM이 "처리 중"을 바꿔 쓴 일). 그래서 일반 서비스보다 **재기 · 기록 · 대체 경로**가 더 중요합니다.
## 3. 효과를 숫자로
### 3.1 오늘 실측 전 · 후
| 무엇 | 전 | 후 | 바꾼 것 |
| --- | --- | --- | --- |
| VPN 질문 p50 (캐시 없이) | 2.69초 (LLM 3번, 기준) | 1.12초 (LLM 1번) | 규칙 먼저 + 검토를 검사로 |
| 같은 질문 다시 | 2.07초 | 0.05초 | 질문 캐시 |
| 다른 말로 같은 FAQ | 1.2초 | 0.1초 | FAQ 번호 캐시 |
| FAQ가 8초 느릴 때 | 11.5초 기다린 뒤 평소 답 | 4.6초 → 담당자 연결 | timeout 2초 · 재시도 1번 |
| FAQ가 계속 느릴 때, 다음 사람 | 4.6초 | 0.1초 | 차단기 30초 |
| 접수 API만 멈췄을 때 VPN 질문 | 담당자 연결 | 평소 답 | 부분 고장 처리 |
| LLM 키가 틀렸을 때 | HTTP 500 | FAQ 원문 · 상태 그대로 (`간단 답변`) | 데이터로 대체 |
### 3.2 하루 1,000건이면 (예시)
가정: 하루 질문 1,000건, 그중 규칙으로 분류되는 FAQ · 접수 질문 80%, 나머지 20%.
| 단계 | 질문당 LLM 호출 | 하루 LLM 호출 |
| --- | --- | --- |
| 기준 (분류 · 답 · 검토) | 3 | 3,000 |
| 규칙 먼저 + 검토를 검사로 | 규칙 1 · 나머지 2 | 0.8 × 1,000 × 1 + 0.2 × 1,000 × 2 = **1,200** |
| + 캐시 (FAQ 질문의 30%가 캐시에서 온다고 하면) | 캐시 0 | 1,200 − (FAQ 질문 수 × 0.3)만큼 더 줄어듦 |
- LLM 호출 수는 **비용과 거의 비례**합니다 (75일차에 토큰과 가격으로 다시 계산).
- 기다리는 시간: 사용자 1,000명이 질문마다 1.5초씩 덜 기다리면 하루 **약 25분**(1,500초)입니다. 사내 도구에서 "빨라졌다"는 사람들의 일하는 시간입니다.
- 이 어림은 **가정**에 달려 있습니다. 실제 비율은 운영 기록(77일차 로그)에서 세어 다시 계산합니다.
## 4. 회사에서는 같은 생각을 이렇게 부른다
오늘 한 일에는 업계에서 쓰는 이름이 있습니다. 이름을 알면 문서 · 도구 · 채용 공고를 읽을 수 있습니다.
| 오늘 한 일 | 업계에서 부르는 이름 | 자주 보는 도구 · 자료 |
| --- | --- | --- |
| p50 · p95 · max | 지연 시간 백분위 (latency percentiles), 꼬리 지연 (tail latency) | Prometheus 히스토그램, Grafana |
| 시간 예산 · `over_budget` | SLI · **SLO** (목표 수준) · 에러 예산 (error budget) | Google SRE 책의 SLO 장 |
| 단계별 `timings` · `steps` | 분산 추적 (tracing), 스팬 (span) | OpenTelemetry, Datadog · New Relic 같은 APM |
| LLM 호출마다 시간 · 입출력 기록 | LLM 관측 (LLM observability) | LangSmith, Langfuse, Arize Phoenix |
| `bench` · `tbench` | 부하 · 성능 시험 (load testing), 성능 기준 검사 (CI performance gate) | k6, Locust, JMeter |
| 규칙 먼저, 모를 때만 LLM | 라우팅 · 캐스케이드 (싼 방법부터) | 연구 예: FrugalGPT (LLM 캐스케이드) |
| 질문 캐시 · FAQ 번호 캐시 | 응답 캐시, 의미 캐시 (semantic cache) | Redis, GPTCache. 모델 쪽에는 프롬프트 캐싱 (OpenAI · Anthropic 등) |
| 검토 LLM을 검사 코드로 | 가드레일, 출력 검증 (validator) | Guardrails AI, NVIDIA NeMo Guardrails (73일차) |
| timeout · 재시도 | 복원력 패턴: 시간 제한, 지수 백오프 + 지터 재시도 | 각 언어의 재시도 라이브러리, AWS 아키텍처 글 |
| 오류 출력 → 정해진 답, LLM 없이 답 | 대체 경로 (fallback), 우아한 저하 (graceful degradation) | |
| 접수 API만 넘기기 | 격벽 (bulkhead), 부분 장애 격리 | |
| 차단기 `breaker-faq` | **서킷 브레이커** (circuit breaker) | resilience4j (Java), Polly (.NET), Envoy · Istio의 이상 감지 |
| 다른 회사 모델로 바꾸기 (오늘은 안 함) | 모델 게이트웨이 · 멀티 프로바이더 대체 | LiteLLM, OpenRouter |
| `my-health` | 헬스 체크, liveness · readiness, 합성 모니터링 | Kubernetes probe, 외부 감시 서비스 |
| `fault` · `llm down` 고장 연습 | **카오스 엔지니어링**, 게임 데이 | Netflix Chaos Monkey, Gremlin, AWS Fault Injection Service |
- 이름은 달라도 순서는 같습니다: **재고 → 목표를 정하고 → 넘으면 알리고 → 멈출 때를 정해 둔다.**
- 오늘은 n8n 노드와 데이터 API 캐시로 직접 만들었습니다. 회사에서는 이것을 플랫폼(게이트웨이 · 서비스 메시 · 관측 도구)이 대신해 주는 경우가 많습니다. 그래도 **무엇을 설정해야 하는지**는 오늘 만든 사람이 압니다.
## 5. 산업별 응용 (예)
아래는 오늘 한 일을 다른 업무에 옮긴 예입니다 (실제 특정 회사의 사례가 아니라 흔한 모양).
| 업무 | 오늘의 무엇을 | 이렇게 |
| --- | --- | --- |
| 은행 · 통신사 고객 상담 챗봇 | 규칙 먼저 · 캐시 · 시간 예산 · 담당자 연결 | "요금제 변경 방법" 같은 FAQ는 캐시, 본인 계좌 · 요금은 **캐시 금지**, 3초 예산을 넘는 비율을 날마다 보고, LLM 장애 때 상담원 연결 |
| 쇼핑몰 주문 문의 | 부분 고장 · 차단기 | 배송 조회 API가 멈추면 배송 질문만 "잠시 뒤"로, 환불 규정 질문은 평소대로 |
| 사내 IT · 인사 헬프데스크 (오늘) | 전부 | 같은 FAQ로 묶어 캐시, 접수 현황은 캐시 금지, 상태 점검을 5분마다 |
| 제조 설비 알림 요약 | timeout · 데이터로 대체 · 상태 점검 | LLM이 늦으면 원래 알림 문장을 그대로 보내고, 설비 데이터 API 점검을 따로 |
| 병원 · 금융처럼 규제가 있는 곳 | **검토를 지우지 않는 판단** | 빨라지는 것보다 "사실에 없는 약속"을 막는 검토가 중요하면 검토 LLM을 남기고 다른 곳을 줄임 (7절) |
| 사내 문서 RAG (71일차) | 캐시 키 설계 · 적중률 | 같은 규정 섹션으로 답하는 질문은 섹션 번호로 캐시, 규정이 바뀌면 캐시 비우기 |
**토론 거리**: 내 회사(또는 가고 싶은 회사)의 AI 기능 하나를 골라, 위 표의 한 줄을 채워 봅니다.
## 6. 운영 대시보드에 올릴 숫자 여섯
오늘 결과에 남긴 칸만으로 운영 지표가 됩니다 (79일차 `tops` · 운영 대시보드에서 실제로 모읍니다).
| 지표 | 결과의 어느 칸에서 | 무엇을 알 수 있나 |
| --- | --- | --- |
| 종류별 p50 · p95 | `timings.total_ms`, `category` | 어느 종류가 느린가 |
| 예산을 넘은 비율 | `over_budget` | SLO를 지키나 |
| LLM 호출 수 (요청당) | `llm_calls` | 비용 (75일차) |
| 캐시 적중률 | `cached`, `cache_by` | 키 설계가 맞나 |
| 대체 경로 비율 (까닭별) | `route`, `reason` | 무엇이 자주 멈추나 (`data_api_down` · `tickets_down` · `breaker_open` · `llm_down`) |
| 상태 점검 | `my-health`의 `status` | 지금 무엇이 멀쩡한가 |
- 오늘 `reason`을 꼭 넣게 한 까닭이 이것입니다. **까닭이 기록되지 않은 실패는 고칠 수 없습니다.**
## 7. 하지 말아야 할 때 (흔한 실수)
| 하기 쉬운 일 | 왜 위험한가 | 대신 |
| --- | --- | --- |
| 재기 전에 최적화 | 데이터 API(1%)를 고치느라 하루를 씀 | 1단계 어림 계산부터 |
| 사람마다 다른 답을 캐시 | 남의 계좌 · 접수 건이 나감 (사고) | 캐시 키에 사람이 들어가면 캐시하지 않음 |
| 무엇이든 재시도 | 같은 접수를 두 번 만들 수 있음 (POST) | "다시 해도 같은 결과"인 요청만 재시도, 아니면 중복 방지 키(idempotency key) |
| 재시도 · timeout을 크게 | 최악의 시간이 11초를 넘음 | 최악의 시간을 계산하고 예산 안으로 |
| 차단기를 오래 열어 둠 | 되살아났는데도 계속 담당자로 넘김 | 짧게(오늘 30초), 열리고 닫힌 횟수를 기록 |
| 규제 업무에서 검토를 지움 | 빨라졌지만 틀린 약속이 나감 | 품질 기준이 먼저, 그다음 속도 |
| 옵션을 기억으로 씀 | 오늘 `neverError`처럼 생각과 다르게 동작 | 작은 시험으로 재 보고 씀 |
## 8. 직무와 면접
오늘 한 일은 다음 직무들이 날마다 하는 일입니다: **AI · LLM 애플리케이션 엔지니어, MLOps · LLMOps 엔지니어, SRE(사이트 신뢰성 엔지니어), 백엔드 · 플랫폼 엔지니어.** 채용 공고의 "성능 최적화", "장애 대응", "관측성(observability)", "SLO 운영"이 오늘 내용입니다.
면접에서 흔히 받는 질문과 오늘 숫자로 답하는 법:
<details><summary>1. "응답이 느리다는 불만이 들어오면 무엇부터 하나요?"</summary>
평균이 아니라 p50 · p95를 종류별로 재고, 단계별 시간으로 병목을 찾습니다. 오늘 실측: LLM이 98%, 데이터 API 1% 미만이라 LLM 호출 수를 줄였고, VPN p50이 2.22초에서 1.12초가 됐습니다. 고친 뒤에는 원래 품질 테스트를 다시 돌렸습니다.
</details>
<details><summary>2. "LLM 제공사가 장애일 때 서비스는 어떻게 되나요?"</summary>
LLM 노드의 실패를 오류 출력으로 받아, 이미 조회한 데이터(FAQ 원문 · 접수 상태)로 답하고 `route`를 `간단 답변`으로 구분해 셉니다. 다른 제공사로 바꾸는 방법도 있지만 키 · 비용 · 품질을 또 관리해야 해서, 데이터로 답할 수 있는 것은 데이터로 했습니다.
</details>
<details><summary>3. "캐시를 넣으면 무엇이 위험한가요?"</summary>
바뀌는 답(접수 상태)과 사람마다 다른 답이 다른 사람에게 나가는 것. 그리고 테스트가 앞 테스트의 캐시 때문에 흔들리는 것. 그래서 캐시 금지 대상을 정하고, 테스트 앞에 캐시를 비웠습니다. 효과는 적중률로 재고, 키를 FAQ 번호로 바꿔 다른 말 질문도 0.1초로 만들었습니다 (잃는 것: 질문에 맞춘 답).
</details>
<details><summary>4. "서킷 브레이커는 왜 필요하고 무엇을 정해야 하나요?"</summary>
멈춘 서비스를 사람마다 기다리지 않게 하고, 되살아나는 서비스에 요청을 몰지 않으려고. 여는 조건(실패), 열려 있을 때의 답, 닫히는 때(오늘은 30초 ttl)를 정합니다. 오늘 다음 질문이 4.6초에서 0.1초가 됐습니다.
</details>
<details><summary>5. "재시도는 몇 번이 좋나요?"</summary>
정답은 없고 **최악의 시간**으로 정합니다. timeout 2초 · 2번 · 기다림 0.5초면 4.5초, 3번이면 7초. 시간 예산 안에 들어오는 만큼만, "잠깐 끊긴" 고장에만. 계속 느리면 재시도 대신 차단기입니다.
</details>
## 9. 최종 프로젝트로 가져갈 것
최종 프로젝트(85일차~)의 워크플로우에 아래를 넣으면 "운영을 생각한 프로젝트"가 됩니다.
- [ ] 결과에 `timings` · `steps` · `llm_calls` · `route` · `reason` (설명할 수 있는 결과)
- [ ] 주요 요청 종류별 p50 · p95를 **바꾸기 전 · 후 같은 날** 재서 보고서에 (`tbench compare`)
- [ ] 시간 예산 하나 (예: 3초)와 넘은 비율
- [ ] 캐시해도 되는 것 · 안 되는 것 표
- [ ] 외부 호출마다 timeout, 실패하면 정해진 답, 최악의 시간 계산
- [ ] LLM이 멈췄을 때의 답 (데이터로, 또는 담당자 연결)
- [ ] 상태 점검 워크플로우 하나
- [ ] 고장 연습 한 번 (켜고 → 겪고 → 고치고 → 되돌리기)과 그 기록
## 10. 다음 일차와의 연결
| 일차 | 오늘의 무엇을 잇나 |
| --- | --- |
| 73 | 입력 · 출력 검증과 보안 — 오늘 검토를 검사로 바꾼 자리에 "지시문 공격 · 개인정보" 검사 |
| 74 | 배포 — `tbench --max-p95`로 배포 전 성능 검사, 배포 직후 워밍업 |
| 75 | 비용 — `llm_calls`를 토큰 · 가격으로 |
| 76 | 에러 처리 — 상태 점검을 Schedule로, `degraded`면 알림 |
| 77 | 로그 분석 — `route` · `reason` · `over_budget`을 모아 세기 |
| 79 | 운영 대시보드 · `tops` — 6절의 지표 여섯 |
## 토론 (5분)
1. 오늘 바꾼 것 중 **내 업무 · 관심 분야에서 가장 먼저 쓸 것** 하나와 그 까닭.
2. 오늘 "빨라진 것"과 "잃은 것" 중, 내가 운영자라면 되돌리고 싶은 것이 있나요?
## 정리
- [ ] 오늘 한 일은 **운영**: 느림 · 비용 · 장애 · 설명할 수 없음을 다룬다
- [ ] 효과는 같은 날 전 · 후 숫자로, 큰 그림은 가정을 밝힌 어림으로
- [ ] 같은 생각을 업계에서는 SLO · 추적 · 캐시 · 가드레일 · 서킷 브레이커 · 카오스 엔지니어링이라고 부른다
- [ ] 결과에 남긴 칸이 곧 운영 지표가 된다 (`reason`까지)
- [ ] 최종 프로젝트에 체크리스트 8개를 가져간다
+87
View File
@@ -0,0 +1,87 @@
# 7·8교시 · 바이브 코딩 (`tbench`)
- 교시: 7교시 16:00~16:50 (각자 만들기), 8교시 17:00~17:50 (발표 10분 · [4단계 정리](04-STAGE4-WRAPUP.md) 35분 · 예고 5분)
- 과제: [VIBE-REQ-TBENCH](../../Last3Mon/72/VIBE-REQ-TBENCH.md)(요청서, 요청의 말 ①~⑫) → [VIBE-Q-TBENCH](../../Last3Mon/72/VIBE-Q-TBENCH.md)의 8단계: `Doc/TODO.md` → `Doc/SPEC.md` → `Doc/IMP-PLAN.md`(엔진 · TUI 단계) → 단계별 구현 → 확인 → `uv tool install` → 다른 폴더에서 사용 → 수업 뒤 `VIBE-A-TBENCH.md` 공개
- 요구 14개(R1~R14). 50분 안에는 **엔진 1단계(R1 · R6 · R7 · R8) + TUI 1단계(R2) + 설치(R14)**, 나머지는 IMP-PLAN의 다음 단계로 집에서.
- 68 · 69 · 71일차와 같은 흐름입니다: 사람의 말(요청서)을 **확인할 수 있는 요구**로 바꾸는 일을 학생이 먼저 하고, VIBE-Q는 그 다음 단계(비교 대상)로 씁니다.
- 오늘은 `tbench`: URL · 웹훅을 N번 불러 응답 시간 p50 · p95 · max, 실패 수를 표와 스파크라인으로 보여 주고, 바꾸기 전 · 후를 비교하는 TUI. 오늘 `flow.py bench`로 한 일을 내 도구로.
```mermaid
flowchart LR
R["요청서 → TODO · SPEC ·<br/>IMP-PLAN 20분"] --> A["7교시<br/>엔진 · TUI · 설치 30분"] --> B["발표<br/>10분"] --> C["4단계 정리<br/>35분"] --> F["73일차 예고<br/>5분"]
```
## 1. 7교시 · 각자 만들기 (50분)
| 시간 | 단계 | 확인 |
| --- | --- | --- |
| 16:00~16:09 | 0 준비 · 1 요청서 → `Doc/TODO.md` (에이전트 없이) | 할 일 ①~⑫ · 꼭 / 나중 |
| 16:09~16:15 | 2 `Doc/SPEC.md` → VIBE-Q 요구 표(R1~R14)와 비교 · commit | 요구마다 확인 방법 |
| 16:15~16:20 | 3 `Doc/IMP-PLAN.md` (엔진 1~3단계 · TUI 1~2단계 · 설치) · commit | 단계마다 체크리스트 |
| 16:20~16:44 | 4 구현: 엔진 1단계(R1 · R6 · R7 · R8) → TUI 1단계(R2), 막히면 5 문제 해결 팁 | 단계마다 `pytest` · 직접 실행 · commit |
| 16:44~16:50 | 6 확인 · 7 `uv tool install --editable .` · 8 다른 폴더에서 `tbench` | `cd ~/aio-01d4-stu/Last3Mon/72 && tbench ...` |
16:00에 화면에 띄울 것: 요청서의 ①~⑫와 예 하나(① → TODO 한 줄 → R1). "평균 하나로는 '가끔 느리다'가 안 보입니다. 몇 번째 값 · 실패 수로 바꿉니다." 16:09 "SPEC으로", 16:15 "IMP-PLAN으로 · 엔진과 TUI를 나눠서", 16:20 "에이전트 켜기 · 엔진 1단계만", 16:44 "설치하고 다른 폴더에서"를 알립니다.
| 볼 것 | 막혔을 때 한마디 |
| --- | --- |
| TODO · SPEC 없이 VIBE-Q를 통째로 붙여 넣었나 | "요청서로 Doc/TODO.md부터. 확인 방법이 없으면 됐는지 모릅니다" |
| IMP-PLAN이 한 덩어리 · 첫 요청에 14개 | "엔진 1단계만. 단계마다 확인 · 체크 · commit" |
| `tbench`를 다른 폴더에서 못 찾음 | "`uv tool list`, `uv tool update-shell` 뒤 새 터미널" |
| SPEC에 부탁만 ("빠르게 재", "보기 좋게") | "1~10초 열 개면 p95가 몇이어야 하나요? 숫자로" |
| 다 끝난 뒤에야 화면이 나옴 | "worker로 부르고 결과마다 표에 더하게" |
| p95 계산이 들쭉날쭉 | "계산 방법 하나를 정해 테스트로 고정" |
| 테스트가 진짜 n8n을 부름 | "pytest 안에서 가짜 서버 (일부러 느리게 · 가끔 500)" |
| 다른 요청끼리 비교 | "저장할 때 url · 본문 해시를 같이" |
| 실행해 보지 않고 다음 요청 | "에이전트가 됐다고 해도 직접 `uv run tbench`" |
수업 준비 때 tbench를 미리 만들어 보지는 않았습니다. 강사 바이브 코딩(4절)은 8교시가 4단계 정리로 바뀌어 **시간이 남으면 · 수업 뒤** 하고, 걸린 시간을 정답본(`VIBE-A-TBENCH.md`)에 남깁니다.
## 2. 8교시 · 발표 (10분)
- 1~2명. **자기 실습 워크플로우의 바꾸기 전 · 후를 tbench로 비교한 사람**을 먼저 고릅니다.
- 3분 발표 · 2분 질문: **내 SPEC과 VIBE-Q가 달랐던 곳** / IMP-PLAN을 어떻게 나눴나 / 잘 먹힌 요청문 / 안 먹힌 요청문 / 에이전트가 틀린 곳 / 비교 표 시연.
## 3. 강사 정리 (8교시 4단계 정리 안에서, 1절 표와 함께)
| | n8n 실습 | tbench |
| --- | --- | --- |
| 재기 | `flow.py bench`, 결과의 `timings` | 직접 만든 p50 · p95 |
| 첫 호출 | push 뒤 느림 → 워밍업 (Lab 01 예제 1 · Lab 02 과제 1) | 첫 호출 표시 · 빼고 계산 |
| 비교 | `my-work/PERF.md` 3절에 한 줄씩 (4교시 Lab 02) | `tbench compare` |
| 고장 | `fault` · `llm down` (6교시 Lab 03) | 가짜 서버 (느림 · 500) |
| 같은 조건 | 같은 요청 파일, `no_cache` | url · 본문 해시 · n 확인 |
## 4. 강사 바이브 코딩 (시간이 남으면 · 수업 뒤, 15분)
| 주제 | 요청문 요지 | 보여 줄 것 |
| --- | --- | --- |
| 단계별 시간 그래프 | "응답 JSON에 timings가 있으면 단계별 평균을 막대로" | 오늘 Lab의 `timings`를 tbench가 읽음 |
| 목표 시간 검사 | "--max-p95 2.5 를 넘으면 종료 코드 1 (CI에서 쓰게)" | 74일차 배포 전 검사(릴리스 게이트)의 씨앗 |
| tops 연결 | "결과 저장 형식을 JSON Lines로, 79일차 tops가 읽게" | 도구끼리 같은 형식 |
걸린 시간과 결과를 `Last3Mon/72/VIBE-A-TBENCH.md`에 적어 둡니다.
## 5. 보충 · 팁 (읽을거리, 4단계 정리 7절과 함께)
| 팁 | 설명 |
| --- | --- |
| 요청서 → 요구가 먼저 | 확인 방법(몇 번째 값 · 실패 수 · 같은 칸)이 있는 요구가 있어야 에이전트의 "완료"를 판단할 수 있음 |
| 재는 도구도 테스트 | 가짜 서버로 "3번째마다 500"이 실패 2개로 세지는지 |
| 같은 조건에서 비교 | 요청 · 횟수 · 캐시 상태 · 첫 호출 |
| 평균 대신 p95 | 사용자는 느린 쪽을 기억한다 |
| 화면은 굳지 않게 | 오래 걸리는 일은 worker |
| 단계마다 `git commit` | 에이전트가 망가뜨려도 돌아갈 곳 |
## 6. 73일차 예고 (5분)
- 오늘: 빠르게, 멈춰도 답하게. 내일: **입력·출력 검증과 응답 필터링, Workflow 보안** — 질문에 숨은 지시문 공격, 답에 섞인 개인정보 · 내부 정보를 막습니다.
- 바이브 코딩: `tscan` (폴더 · git staged 파일에서 API 키 · `.env` 값 · 주민번호 · 전화번호를 찾아 가린 채로 보여 주고, pre-commit 훅으로 등록). 오늘 `tbench`는 지우지 마세요. 79일차 `tops`에서 씁니다.
## 확인 (강사)
- [ ] 16:00 요청서 → TODO(에이전트 없이) → SPEC → IMP-PLAN 20분을 지켰다, 16:20부터 엔진 1단계.
- [ ] 7교시 시작 전 가짜 서버로 테스트하라는 말을 화면에 띄웠다.
- [ ] 수업 뒤: `VIBE-A-TBENCH.md`의 "자주 빠뜨리는 것"·강사 바이브 코딩 시간을 채우고 **학생 저장소에 VIBE-A만** 올린다 (LAB-A · `answers/`는 요청할 때).
- [ ] 발표자 1~2명, 강사 정리 표, 강사 바이브 코딩 1개(시간 기록), 팁, 예고.
- [ ] 수업이 끝날 때 학생들의 고장 스위치 · OpenAI 키가 평소대로인지 (`doctor`).
+40
View File
@@ -0,0 +1,40 @@
# 72일차 · Workflow 실행 흐름 최적화와 운영 관점 개선 (바이브 코딩)
- 일자: 2026-10-26 (72일차)
- 교과목: 노코드 멀티 에이전트 워크플로우 설계 및 운영
- 단원: 운영·확장 관점의 AI 워크플로우 설계 고도화
- 학습 내용: Workflow 실행 흐름 최적화, 운영 관점 개선 전략 적용
- 실습 폴더: [Last3Mon/72](../../Last3Mon/72/README.md)
- 실습 환경: 65일차 n8n 2.42.6 + OpenAI API(`gpt-4.1-mini`) + 72일차 데이터 API(고장 스위치 · 캐시) + **CLI 코딩 에이전트**
- **실습 준비: [00-DAY72-PREP.md](00-DAY72-PREP.md) (수업 전날까지 1~2절 완료)**
## 오늘의 시간표
| 교시 | 시간 | 내용 | 자료 |
| --- | --- | --- | --- |
| 1 | 09:00~09:50 | 오늘의 개요 · **1단계 강의** · 재기와 병목 | [개요](00-DAY72-OVERVIEW.md) 1~14장, [1단계](01-STAGE1-MEASURE-BOTTLENECK.md) |
| 2 | 10:00~10:50 | **Lab 01** 실습 5개 (bench · 단계별 시간 · 종류별 재기 · 가장 느린 단계 · 시간 예산) | [Lab 01](../../Last3Mon/72/LAB-01-Q-MEASURE-BOTTLENECK.md) |
| 3 | 11:00~11:50 | **2단계 강의** · 실행 흐름 최적화 | 개요 15~21장, [2단계](02-STAGE2-OPTIMIZE-FLOW.md) |
| 4 | 12:00~12:50 | **Lab 02** 실습 5개 (규칙 먼저 · 캐시 · 적중률 · LLM 한 번 · FAQ 번호 캐시) | [Lab 02](../../Last3Mon/72/LAB-02-Q-OPTIMIZE-FLOW.md) |
| - | 13:00~14:00 | 점심 | |
| 5 | 14:00~14:50 | **3단계 강의** · 운영 관점의 안정성 | 개요 22~28장, [3단계](03-STAGE3-STABILITY.md) |
| 6 | 15:00~15:50 | **Lab 03** 실습 5개 (timeout · LLM 없이 · 접수 API만 고장 · 상태 점검 · 차단기) | [Lab 03](../../Last3Mon/72/LAB-03-Q-STABILITY.md) |
| 7 | 16:00~16:50 | 바이브 코딩 · `tbench` 만들기 (각자, 요청서 → TODO → SPEC → IMP-PLAN → 구현 → 설치) | [VIBE-REQ-TBENCH](../../Last3Mon/72/VIBE-REQ-TBENCH.md) → [VIBE-Q-TBENCH](../../Last3Mon/72/VIBE-Q-TBENCH.md), [진행](05-VIBE-CODING.md) |
| 8 | 17:00~17:50 | 바이브 코딩 발표 10분 · **4단계 정리 35분** (의미 · 효과 · 실전 응용) · 73일차 예고 5분 | [진행](05-VIBE-CODING.md), [4단계 정리](04-STAGE4-WRAPUP.md) |
```mermaid
flowchart LR
P["개요"] --> S1["1단계<br/>bench · 단계별 시간 · 병목 · 예산"] --> S2["2단계<br/>규칙 먼저 · 캐시 · 키 설계 · LLM 줄이기"] --> S3["3단계<br/>timeout · 대체 경로 · 부분 고장 · 차단기 · 상태 점검"] --> V["7교시<br/>바이브 코딩 tbench"] --> W["8교시<br/>발표 · 4단계 정리"]
```
## 자료 목록
| 파일 | 내용 |
| --- | --- |
| `00-DAY72-PREP.md` | 실습 준비: n8n · 72일차 판 데이터 API 켜기, 기준 워크플로우 게시, `flow.py doctor` (수업 전) |
| `00-DAY72-OVERVIEW.md` | 개요 30장 (PPT 대신 Markdown, 가로줄로 장 구분, 실습 15개 한눈에) |
| `01-STAGE1-MEASURE-BOTTLENECK.md` | 재기 전에 고치지 않기, p50 · p95 손 계산, 첫 호출, 어디서 재나, 단계별 시간, 종류별 실측, 어림 계산, 가장 느린 단계, 시간 예산(SLO), 확인 질문 |
| `02-STAGE2-OPTIMIZE-FLOW.md` | 줄일 수 있는 LLM, 규칙 먼저, 캐시(무엇을 캐시하면 안 되나), 적중률 어림, 캐시 키 설계(FAQ 번호), 검토를 검사로, 잃은 것, 테스트도 바뀐다, 확인 질문 |
| `03-STAGE3-STABILITY.md` | 멈추는 방식, timeout · 재시도 · 오류 출력, 최악의 시간, `neverError` · `fullResponse` 실측, LLM 없이 답, 부분 고장, 차단기, 상태 점검, 고장 연습, 확인 질문 |
| `04-STAGE4-WRAPUP.md` | 8교시 35분 · 정리: 오늘 한 장, 왜 운영인가, 효과를 숫자로, 업계 이름 · 도구 (SLO · 추적 · 캐시 · 가드레일 · 서킷 브레이커 · 카오스), 산업별 응용, 운영 지표 여섯, 하지 말아야 할 때, 직무 · 면접 질문, 최종 프로젝트 체크리스트 |
| `05-VIBE-CODING.md` | 7교시 바이브 코딩 진행 (tbench), 8교시 발표, 강사 바이브 코딩(시간이 남으면), 팁 |