23 KiB
📐 구현 계획서 Rev.2 — C-6: stop_session.sh 레거시 주석 및 구버전 사용법 정리
- Job ID:
32167a9d(Rev.1 =73b18819) - Planner: claude (session:
herdr:canary-projects-multi-agent-mux-creator-claude) - Role: Planner (
MULTI_AGENT_RULES.md§1 — 본 작업에서 저장소 코드 0건 수정) - 반영 대상 Challenge:
8b6b574f(agy, Worker / Plan Reviewer) —[VERDICT: PASS WITH CHALLENGE] - 기준 커밋:
5ed39f8(refactor, 작업 트리에 미추적VERSIONS.md1건) - 백로그 항목: C-6 / 로드맵 P2-3
0. 요약
Challenge 는 타당합니다. 전면 수용합니다. 격리 클론에서 실제 mam_sandbox 픽스처로 실행해 재현했습니다 — Rev.1 §4.2-(2) 는 4개 하위 케이스 중 3개가 rc=2 로 실패했을 것입니다.
다만 Rev.2 는 챌린저의 권고안을 그대로 채택하지 않고 두 가지를 더합니다.
- 챌린저 권고(
valid_session사용)는 증상을 해소하지만, 가드를 C-6 과 무관한 불변식(:91-100에이전트 접미사 명명 규칙)에 결합시킵니다.rc=2가 5가지 서로 다른 원인에 공유되고 있다는 것이 이 오탐의 근본 원인이므로, Rev.2 는 종료 코드 대신 stderr 메시지를 단언해 원인 결합 자체를 제거합니다. - 확정 가드를 뮤테이션으로 검증하는 과정에서, 챌린저도 저도 놓쳤던 구멍 1건을 찾았습니다 — Rev.1 이 §1.1 에 결함으로 등재한
usage():41의--agent claude|agy과소 표기를, Rev.1·챌린저 양쪽 가드 모두 탐지하지 못합니다(M3). Rev.2 에서 닫았습니다.
| 항목 | Rev.1 | Rev.2 |
|---|---|---|
| §4.2-(2) 세션명 | nosuch (오탐 — 3/4 rc=2) |
test-project-creator-claude |
| §4.2-(2) 단언 | rc != 2 단독 |
stderr 메시지 단언 + rc != 2 보조 |
usage() 에이전트 목록 검증 |
없음 (M3 구멍) | 추가 |
| 가드 뮤테이션 검증 | 계획만 제시 | 3종 실측 완료 |
| 나머지(§1~§3, §5, §7) | — | 변경 없음 |
1. Challenge 판정 — 수용 (실측 재현)
1.1 챌린저 지적의 사실 확인
챌린저가 인용한 블록은 실재합니다. 정확한 위치는 :91-100(챌린저 표기 :92-100), exit 2 는 :98 입니다.
# stop_session.sh:91-100
# --agent 미지정 시 이름 suffix 로 fallback (P1-F)
if [ -z "$AGENT" ]; then
case "$SESSION_NAME" in
*-creator-claude|*-planner-claude|*-reviewer-claude) AGENT=claude ;;
...
*) echo "ERROR: cannot infer agent from '$SESSION_NAME'; pass --agent" >&2; exit 2 ;; # :98
esac
fi
챌린저가 지적한 실행 순서도 정확합니다. YAML 존재 검사는 :80, 에이전트 추론은 :91 이므로 추론이 뒤에 옵니다. 그리고 tests/conftest.py:15-52 의 mam_sandbox 픽스처는 agent-sessions.yaml 을 실제로 생성합니다(herdr_sessions: []). 따라서 :80 은 통과하고 :98 에 도달합니다 — "샌드박스 상태에 따라 결과가 뒤바뀐다"는 챌린저의 우려가 아니라, 결정론적으로 항상 실패합니다.
1.2 실측 — 격리 클론 + 실제 mam_sandbox 픽스처
git clone --local --no-hardlinks 로 만든 클론에 프로브 테스트를 넣어 측정했습니다.
--session |
추가 인자 | rc | stderr 첫 줄 |
|---|---|---|---|
nosuch |
--reason x |
2 | cannot infer agent from 'nosuch' |
nosuch |
--purge-conversation |
2 | cannot infer agent from 'nosuch' |
nosuch |
--yes |
2 | cannot infer agent from 'nosuch' |
nosuch |
--agent hermes |
1 | session 'nosuch' not in …yaml |
test-project-creator-claude |
--reason x |
1 | session … not in …yaml |
test-project-creator-claude |
--purge-conversation |
1 | session … not in …yaml |
test-project-creator-claude |
--yes |
1 | session … not in …yaml |
test-project-creator-claude |
--agent hermes |
1 | session … not in …yaml |
test-project-creator-claude |
--purge-conversation --yes |
1 | session … not in …yaml |
Rev.1 의 assert r.returncode != 2 는 4개 중 3개에서 실패합니다(--agent 를 준 케이스만 추론을 건너뛰어 통과). Challenge 확정.
부수 확인: Rev.1 §9 한계에서 "--purge-conversation 을 --yes 없이 호출 시 rc=1 인지 rc=3 인지 구현 시 실측 필요"라고 남겼던 항목도 해소되었습니다 — rc=1(레지스트리 조회가 확인 프롬프트보다 먼저)입니다.
2. 챌린저 권고안 평가 — 채택하되 보강
2.1 권고안은 작동합니다
valid_session = "test-project-creator-claude" 는 *-creator-claude 에 접미사 매칭되어 AGENT=claude 로 추론되고, 4개 케이스 전부 rc=1 로 끝납니다(위 표 하단 5행). 측정으로 확인했습니다.
2.2 그러나 근본 원인은 세션명이 아니라 rc=2 의 과부하입니다
stop_session.sh 에서 exit 2 는 5곳에서 발생합니다.
| 행 | 원인 |
|---|---|
:67 |
폐지 플래그(--mode/--capture-id/--graceful) |
:70 |
unknown arg |
:76 |
invalid agent type |
:79 |
--session 누락 |
:98 |
cannot infer agent ← 이번 오탐의 원인 |
가드가 검증하려는 것은 오직 :70 하나("도움말이 광고하는 플래그를 파서가 unknown 으로 튕기지 않는다")인데, rc != 2 는 나머지 4개와 구별하지 못합니다. 챌린저의 valid_session 은 :98 만 회피할 뿐 :76·:79 는 여전히 구별하지 못하며, 더 나쁘게는 가드를 :91-100 의 에이전트 접미사 명명 규칙에 결합시킵니다. 훗날 역할명이 추가되거나 creator 가 개명되면, C-6 가드가 C-6 과 무관한 이유로 깨지고 실패 메시지도 C-6 을 가리키지 않습니다.
2.3 Rev.2 의 보강 — stderr 메시지 단언
assert "unknown arg" not in r.stderr # 파서가 이 플래그를 모른다고 하지 않았다
assert "deprecated" not in r.stderr # 폐지 플래그로 취급하지도 않았다
assert r.returncode != 2 # (보조) 위 둘을 빠져나간 rc=2 도 없다
이 단언은 5개 원인 중 정확히 검증 대상인 것만 지목합니다. 실측 표에서 확인되듯 nosuch 케이스의 stderr 는 cannot infer agent 이므로 메시지 단언만으로는 세션명이 무엇이든 통과합니다 — 즉 챌린저 권고보다 엄밀히 더 견고합니다.
두 가지를 모두 채택합니다: 챌린저의 valid_session(원인 제거) + 메시지 단언(결합 제거). 어느 한쪽이 미래에 무력화돼도 다른 쪽이 남습니다.
3. 🆕 Rev.2 신규 발견 — 가드가 usage():41 결함을 놓침 (M3)
확정 가드를 뮤테이션 검증하던 중 발견했습니다. 챌린저도 Rev.1 도 지적하지 못한 구멍입니다.
Rev.1 §1.1 은 usage():41 의 [--agent claude|agy] 가 검증기(:74-77)의 4종 수용과 어긋난다고 결함으로 등재했습니다. 그런데 Rev.1·챌린저 양쪽 가드 모두 이 결함을 탐지하지 못합니다.
뮤테이션 M3: 수정된 클론에서 usage() 의 에이전트 목록만 claude|agy 로 되돌림
결과: 1 passed ← 가드가 통과시킴 ❌
C-6 이 고치기로 한 결함 중 하나가 가드 밖에 있었던 셈입니다. Rev.2 에서 다음 3줄로 닫았습니다.
for agent in ("claude", "agy", "hermes", "cline"):
assert agent in res.stdout, f"usage() omits supported agent {agent}"
재검증: 강화 후 baseline 1 passed, M3 재적용 시 1 failed. 구멍이 닫혔음을 실측했습니다.
4. 확정 회귀 가드
4.1 설계 원칙 (Rev.1 §4.1 유지)
직전 리뷰 31730364 에서 뮤테이션으로 드러난 실패 사례 — test_delegate_agent_resolution_and_fallback 이 테스트 파일 안에 case 문을 복사해 실행한 탓에 생산 코드 결함을 완전히 되돌려도 통과 — 를 반복하지 않도록, 가드는 stop_session.sh 를 직접 실행하고 그 파일을 직접 읽습니다.
4.2 확정 코드 — tests/test_tier2_component.py 에 추가
def test_comp_stop_usage_matches_parser(mam_sandbox):
"""C-6: help text and parser must not drift apart."""
script = mam_sandbox / ".agents" / "skills" / "multi-agent-mux-stop" / "scripts" / "stop_session.sh"
# 에이전트 접미사 추론(:91-100)이 성립하는 이름 — rc=2 의 다섯 원인 중
# 'cannot infer agent'(:98)를 배제하기 위함 (Challenge 8b6b574f)
VALID = "test-project-creator-claude"
# 1) --help 는 성공하고, 폐지된 플래그를 광고하지 않는다
res = subprocess.run(["bash", str(script), "--help"], capture_output=True, text=True)
assert res.returncode == 0
for dead in ("--mode", "--capture-id", "--graceful"):
assert dead not in res.stdout, f"usage() still advertises {dead}"
# 1b) 검증기가 받는 에이전트는 전부 도움말에 나온다 (Rev.2 M3)
for agent in ("claude", "agy", "hermes", "cline"):
assert agent in res.stdout, f"usage() omits supported agent {agent}"
# 2) 도움말이 광고하는 플래그는 전부 파서가 받는다
# rc=2 는 5가지 원인을 공유하므로 stderr 메시지로 직접 지목한다
for flag, args in (("--reason", ["--reason", "x"]),
("--purge-conversation", ["--purge-conversation"]),
("--yes", ["--yes"]),
("--agent", ["--agent", "hermes"])):
r = subprocess.run(["bash", str(script), "--session", VALID] + args,
capture_output=True, text=True)
assert "unknown arg" not in r.stderr, f"usage() advertises {flag} but parser rejects it: {r.stderr}"
assert "deprecated" not in r.stderr, f"usage() advertises deprecated {flag}: {r.stderr}"
assert r.returncode != 2, f"{flag} -> rc=2: {r.stderr}"
# 3) 폐지된 플래그는 전용 메시지와 함께 rc=2 로 거부된다 (특별 취급 유지)
for dead in ("--mode", "--capture-id", "--graceful"):
r = subprocess.run(["bash", str(script), "--session", VALID, dead, "hard"],
capture_output=True, text=True)
assert r.returncode == 2
assert "deprecated" in r.stderr
# 4) 헤더 주석도 폐지 플래그를 사용법으로 광고하지 않는다
head = "".join(script.read_text().splitlines(keepends=True)[:35])
assert "--mode soft|hard" not in head
4.3 뮤테이션 검증 — Rev.2 에서 실측 완료
Rev.1 은 뮤테이션을 "구현자 필수 수행"으로 지시만 했으나, Rev.2 는 계획 단계에서 직접 수행했습니다. 격리 클론에 §3 단계 1~2 의 문서 수정을 적용한 뒤:
| # | 뮤테이션 | 기대 | 실측 |
|---|---|---|---|
| — | (baseline, 수정 적용 상태) | PASS | 1 passed ✅ |
| M1 | 파서에서 --reason) 분기 삭제 (도움말은 계속 광고) |
FAIL | 1 failed — :19 unknown arg 단언 ✅ |
| M2 | 헤더에 [--mode soft|hard] 행 복원 |
FAIL | 1 failed — :30 헤더 단언 ✅ |
| M3 | usage() 에이전트 목록을 claude|agy 로 축소 |
FAIL | 강화 전 1 passed ❌ → 강화 후 1 failed ✅ |
M1 이 가드의 핵심 가치를 증명합니다 — 도움말과 파서 중 한쪽만 바뀌면 즉시 실패하며, 이것이 C-6 을 애초에 만든 드리프트입니다.
구현자는 위 표를 재현만 하면 됩니다(신규 설계 불필요).
5. 구현 계획 (Rev.1 대비 변경 없음)
단계 1 — 헤더 주석 교체 (:2-30, 29줄)
# stop_session.sh — multi-agent-mux-stop 의 부속 스크립트
# Usage:
# bash stop_session.sh --session <name> [--agent claude|agy|hermes|cline] \
# [--reason <reason>] [--purge-conversation] [--yes]
#
# 동작: 항상 graceful stop 입니다. send-keys 로 정상 종료를 유도하고
# (미종료 시 SIGTERM → SIGKILL 폴백), kill 직전에 이 워크스페이스의
# conversation id 를 row 에 확정 기록해 다음 resume 이 tier-1(race-free)
# 으로 복원되게 합니다. status 는 running -> stopped 로 전이합니다.
# 멱등: 이미 stopped 면 no-op + exit 0.
#
# 옵션:
# --session <name> — 대상 세션 (필수)
# --agent <type> — claude | agy | hermes | cline
# (미지정 시 세션명 접미사로 추론; 추론 실패 시 exit 2)
# --reason <reason> — 상태 전이 사유 (stop_reason). 기본값 manual_stop
# --purge-conversation — 디스크의 conversation artifact 까지 삭제.
# status=terminated, resumable=false 로 전이하며
# resume 불가. --yes 없이는 확인 프롬프트(exit 3)
# --yes — --purge-conversation 의 확인 프롬프트 생략
#
# 폐지된 옵션: --mode / --capture-id / --graceful 는 각각 exit 2 로 거부됩니다.
# graceful 종료와 id 캡처는 이제 무조건 수행되며, soft/hard 모드
# 구분은 --purge-conversation 유무로 대체되었습니다.
#
# Exit codes:
# 0 = success (or already-stopped no-op) | 1 = YAML not found / not registered
# 2 = invalid args | 3 = interactive confirmation required (--yes 누락)
# 4 = purge aborted (herdr session survived the kill chain)
Rev.2 추가:
--agent항목에 접미사 추론 동작(:91-100)을 한 줄 명기합니다. Challenge 가 드러냈듯 이 동작은 문서화되어 있지 않아 계획자·리뷰어 양쪽이 놓쳤던 부분입니다. C-6 의 취지("문서가 실제 동작과 일치할 것")에 정확히 부합합니다.
단계 2 — usage() 보강 (:39-47)
usage() {
cat <<EOF
Usage: $0 --session <name> [--agent claude|agy|hermes|cline] [--reason <reason>]
[--purge-conversation] [--yes]
Arguments:
--session <name> — target session name (required)
--agent <type> — claude | agy | hermes | cline
(inferred from the session-name suffix when omitted)
--reason <reason> — stop_reason field (default: manual_stop)
--purge-conversation — also delete on-disk conversation artifacts;
status becomes terminated and resume is impossible
--yes — skip the --purge-conversation confirmation prompt
Stop is always graceful and always captures the conversation id.
(idempotent: stopping an already-stopped session is a no-op with exit 0)
EOF
}
단계 3 — 내부 주석 3곳 + 경고 문자열 1곳
| 위치 | 조치 |
|---|---|
:157 |
# --capture-id: kill 직전에 … → # 캡처: kill 직전에 … |
:166 |
WARN: --capture-id requested but no conversation id resolved → WARN: no conversation id resolved before stop (nothing on disk yet) |
:172 |
# --graceful: send-keys 로 … → # graceful 종료: send-keys 로 … |
:257 |
# --capture-id: 항상 captured UUID 기록 → # 항상 captured UUID 기록 (purge 가 아닐 때만) |
단계 4 — MESSAGING.md:346-348
| `stopped` | stopped via `multi-agent-mux-stop` (default); conversation preserved for resume | `stop` |
| `terminated` | stopped with `--purge-conversation`, or herdr-dead detected; conversation deleted / session gone | `stop --purge-conversation`, `monitor` reconcile |
| `archived` | legacy value — no producer since `--mode soft` was removed; kept in the validation whitelist for rows written by older versions | (none) |
6. 문서 동기화 (Rev.1 대비 변경 없음)
6.1 IMPROVEMENTS.md — 7곳
| 행 | 현재 | 변경 후 |
|---|---|---|
:3 |
최종 갱신일 2026-08-16 (P3-1/A-4 …) |
날짜·사유에 C-6 완료 반영 |
:5 |
미해결 6건 (… 레거시 1) | 미해결 5건 (… 레거시 0) |
:6 |
완료 19건 | 완료 20건, 목록에 C-6 추가 |
:107 |
## 4. … (Legacy Remnants — 1건) |
… (Legacy Remnants — 0건 — 전원 완료) (:103 §3 표기법과 동일) |
:109-110 |
C-6 항목 | 삭제 (§5 로 이동) |
:114 |
## 5. … (Completed Tasks — 19건) |
… (Completed Tasks — 20건) |
:253 |
| **P2-3** | **C-6** | 도움말 3줄 정정 | 극소 | — | |
… **(✅ 완료 — 가드 신설, 전체 263/263 PASS)** | |
§5 신규 항목:
### **C-6 (P2-3): `stop_session.sh` 레거시 주석 및 구버전 사용법 정리** — ✅ 완료
- 헤더 주석이 광고하던 `--mode soft|hard` / `--capture-id` / `--graceful` 3종은 파서가 `exit 2` 로
거부하는 폐지 플래그였습니다. 헤더 29줄을 현재 CLI 에 맞게 교체하고, `usage()` 에 누락돼 있던
옵션 설명과 `--agent` 접미사 추론 동작을 보강했으며, Option B 이후 무의미해진 "워크스페이스에
격리된" 표현과 내부 주석 3곳의 플래그 표기를 정리했습니다.
- `MESSAGING.md` 상태 표가 제거된 플래그로 `stopped`/`terminated` 를 정의하던 것을 교정하고,
생산자가 사라진 `archived` 를 레거시 값으로 명기했습니다.
- 도움말과 파서의 일치를 강제하는 회귀 가드를 신설하고 뮤테이션 3종(M1~M3)으로 방어력을
검증했습니다 — C-6 은 문서 과제라 기존 테스트가 전혀 잡지 못하던 영역입니다.
주의: :5 의 "레거시 잔재 0건"과 :107 §4 헤더는 반드시 함께 바꿉니다. 직전 3라운드 리뷰에서 이 쌍의 불일치가 매번 지적되었습니다.
6.2 LOG.md
## 📌 1. 금일 작업 내용 요약 아래 기존 ### 1) P3-1 … 앞에 신규 항목을 삽입하고 기존 P3-1 을 ### 2) 로 조정합니다. 머리말 - **최종 기록일시** · - **작업 상태** 도 갱신합니다.
### 1) **C-6 (P2-3): `stop_session.sh` 레거시 주석 및 구버전 사용법 정리** — **완료**
- **배경**: 헤더 주석이 폐지 플래그 3종을 사용법으로 광고했으나 파서는 전용 메시지와 함께
`exit 2` 로 거부하고 있었음(실측). 백로그에는 "도움말 3줄"로 등재돼 있었으나 실제 대상은
헤더 29줄 + `usage()` + 내부 주석 3곳 + `MESSAGING.md` 상태 표였음.
- **주요 구현**: (파일별 변경 요약)
- **검증**: `pytest` 263/263 PASS. 신규 가드에 대해 뮤테이션 M1~M3 전부 FAIL 확인.
7. archived 사문 상태값 — Option A 확정
Rev.1 §7 에서 판단을 요청했고 챌린저가 §4-3 에서 Option A 에 전적으로 동의했으므로 확정합니다.
- A. 현상 유지 + 문서 명기 —
atomic_yaml.py:18화이트리스트와reconcile.sh:474관용 목록은 손대지 않고,MESSAGING.md에 "레거시 값, 현재 생산자 없음"을 명기 (§5 단계 4 에 반영 완료). - B(완전 은퇴)는 기존 데이터에
archived행이 있으면 검증 실패로 전체 쓰기가 막히므로 마이그레이션이 필요합니다 — C-6("극소") 범위를 벗어납니다.
MESSAGING.md 를 C-6 범위에 포함하는 것도 챌린저가 §4-2 에서 동의했으므로 확정합니다.
8. 검증 절차
| # | 명령 / 확인 | 기대 |
|---|---|---|
| 1 | bash -n .../stop_session.sh |
OK |
| 2 | bash stop_session.sh --help; echo $? |
rc=0, 폐지 플래그 미노출, 4개 에이전트 전부 노출 |
| 3 | --mode / --capture-id / --graceful |
rc=2 + deprecated 메시지 유지 |
| 4 | --agent bogus |
rc=2 (invalid agent type) |
| 5 | grep -rn -- "--mode soft" .agents/ *.md |
0건 |
| 6 | 뮤테이션 M1 — 파서에서 --reason) 삭제 |
가드 FAIL |
| 7 | 뮤테이션 M2 — 헤더에 --mode soft|hard 복원 |
가드 FAIL |
| 8 | 뮤테이션 M3 — usage() 에이전트 목록 축소 |
가드 FAIL |
| 9 | pytest tests/ -q |
263 passed |
| 10 | env -u PYTHONPATH pytest tests/test_tier2_component.py -q |
전부 통과 (환경 비의존) |
| 11 | IMPROVEMENTS.md :5 ↔ :107 대조 |
레거시 카운트 일치 |
| 12 | IMPROVEMENTS.md :6 ↔ :114 대조 |
둘 다 20건 |
9번은 약 6분 30초 소요됩니다(직전 실측 262 passed / 381.58s). 백그라운드 실행 권장.
10번 근거: 직전 라운드에서 신규 테스트가 주변 셸의 PYTHONPATH 에 의존해 CI 를 적색으로 만든 사례(N1)가 있었습니다. 확정 가드는 subprocess.run(["bash", ...]) 만 쓰므로 해당 위험이 없으나 확인 절차는 유지합니다.
9. 변경 규모 및 리스크
| 파일 | 변경 |
|---|---|
stop_session.sh |
헤더 29줄 교체, usage() 약 +10줄, 내부 주석 3곳 + 경고 문자열 1곳 |
MESSAGING.md |
3줄 |
IMPROVEMENTS.md |
7곳 + §5 신규 항목 |
LOG.md |
1개 블록 + 머리말 |
tests/test_tier2_component.py |
+1 test |
| 테스트 총계 | 262 → 263 |
| 리스크 | 평가 |
|---|---|
| 동작 회귀 | 없음. 실행 경로 무변경. 유일한 예외 :166 경고 문자열은 단언하는 테스트 0건 확인 |
| 가드 오탐 | 해소. Challenge 원인(:98)을 세션명으로 제거하고, rc=2 과부하를 메시지 단언으로 우회 |
| 가드 무력화 | 해소. M1~M3 실측으로 방어력 증명 |
| 카운트 불일치 재발 | §8 의 11·12번으로 차단 |
권장 커밋 분할
docs(stop): rewrite stop_session.sh header and usage to match the current CLI (C-6)— 단계 1~3test(stop): guard help text against parser drift (C-6)— §4docs(messaging,improvements,log): sync status table and backlog for C-6— 단계 4 + §6
2번을 1번 뒤에 두면, 가드가 1번 없이 실패하고 1번과 함께 통과함을 커밋 순서로 증명할 수 있습니다.
10. 한계
- 확정 가드는 격리 클론에서 실행 검증했으나, 저장소 본체에는 적용하지 않았습니다(Planner 역할). 클론은 검증 후 삭제했고 작업 트리는 계획 수립 전후 동일(
?? VERSIONS.md1건)합니다. - 뮤테이션 M1
M3 은 §5 단계 12 의 문서 수정을 클론에 부분 적용한 상태에서 수행했습니다(헤더--mode행 삭제 +usage()확장). 단계 3·4 는 가드 대상이 아니므로 적용하지 않았습니다. - 전체 회귀(263)는 재실행하지 않았습니다. 262 passed / 381.58s 가 유효 기준이며 HEAD 가
5ed39f8로 진행되었으므로 구현 시 재측정이 필요합니다. MESSAGING.md는 폐지 플래그 3종 검색으로 걸린 3줄만 확인했고 나머지는 감사하지 않았습니다.:91-100접미사 추론의 역할 목록(creator/planner/reviewer)이 실제 사용되는 역할 전부를 덮는지는 확인하지 않았습니다. C-6 범위 밖이며, 가드는 이 목록에 의존하지 않도록(§2.3) 설계했습니다.