State<Observer>/State<Effect>가 재-dispatch될 때 안쪽 값이 안 바뀌어도 Dispatch가 값 비교 없이 매번 재바인딩하던 것에 RefLeafHandler와 같은 old ~= v dedup을 추가(correctness 아니라 순수 성능 최적화 — == 비교가 매번 도는 Relate weak-table 쓰기보다 항상 쌈). 가장 큰 변경은 Tag/Attribute 핸들러 등록 모델 역전: 직전 커밋이 확정한 "TagHandler/AttributeKeyHandler/AttributeGroupHandler가 quad-base 모듈 로드 시점에 스스로 등록한다"는 결론 자체가 틀렸음이 드러남 — 이건 lifecycle-pattern.md가 이미 거부해둔 InitNamespace류 top-level 부작용 패턴과 같은 클래스였고, module-lifecycle-plan.md가 이미 확정해둔 "등록은 백엔드 팩토리가 BaseModule을 뮤테이션하는 시점" 원칙과 정면으로 어긋났음. 정정: 저 이름들은 참조 카운트/이름 claim 알고리즘 구현일 뿐이고, HANDLER_PRIORITY_FALLBACK에 실제로 꽂히는 건 이를 감싸는 별도 이름의 TagFallbackHandler/AttributeKeyFallbackHandler/AttributeGroupFallbackHandler — 등록 주체는 quad-base 모듈이 아니라 백엔드 팩토리. dispatch-core-plan.md/ tag-plan.md/attribute-plan.md/module-lifecycle-plan.md/architecture.md 전부 재반영, ROADMAP.md M10 체크리스트에 새 Fallback 파일 3개 추가(빠져 있으면 구현자가 만들 필요를 몰랐을 갭), 뒤집힌 원문은 archive/tag-attribute-load-time-registration-reversed.md. /code-review를 두 라운드 돌려 findings 15건 확정 반영 — 죽어있던 Observer/Effect FALLBACK 동적 경로 가드(k 타입 미체크), bindLifetime pseudocode의 canExecute/canBound 혼용, "결정 대기" 절이 삭제됐는데 비어 있다고 서술하던 3개 파일, "동적 경로 가드"가 볼드 텍스트뿐 실제 헤딩이 아니라 깨져있던 절 참조 6곳(###으로 승격), CLAUDE.md 11번째 세션 기록 ~103줄→~13줄 압축, Tag/Attribute 정정이 프로즈만 고치고 놓친 pseudocode 2곳. 별개로 RefLeafHandler.isHandlable이 PostRef 도입 이후 안 갱신돼 `and not isPostRef(v)`를 빠뜨렸던 사전 존재 버그도 같이 발견·정정. doc-check.py ERROR 0 유지. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
14 KiB
2026-08-14 열두 번째 세션 — Observer/Effect Leaf에 Ref와 같은 identical-value dedup 추가(성능 최적화)
배경
이전 대화에서 State/State/State가 전부 설계상
지원되는지(읽기만으로 판단) 확인한 뒤, 사용자가 이어서 "state의
state가 emit됐는데 내부 Observer 객체 자체는 안 바뀐 경우, unbind/bind가
안 일어나고 process/retract가 nop인지"를 물었다.
1차 조사 — Dispatch 계층엔 값 동일성 비교가 없음
base/dispatch-core-plan.md의 "Dispatch 체인" 절(Dispatch.process의
(A) 분기)을 추적한 결과: 핸들러 타입이 같으면 v가 이전 값과 같은
객체든 아니든 무조건 slot.retractor(v)→h.process(inst,k,v,index)가
다시 불림 — Dispatch 자신은 값 동일성을 전혀 비교하지 않고, 값 단위
dedup은 필요한 핸들러가 직접 Relate로 구현해야 하는 몫(RefLeafHandler가
old ~= v를 Relate로 체크하는 게 그 예, base/ref-plan.md "Ref의
retract" 절). Dispatch/Leaf.luau가 매치하는 Observer/Effect 쪽엔 이
dedup이 문서 어디에도 없다는 걸 확인하고 사용자에게 "확정된 설계는 아님,
잠재적 갭"으로 보고했다.
2차 — "메인에서 작업해도 됨, 문제 되는 부분 찾아 고쳐" 지시 후 재조사
사용자가 다른 에이전트는 쉬고 있으니 메인 브랜치에서 직접 작업해도
된다며 이 갭을 고치라고 지시. 처음엔 곧바로 RefLeafHandler와 같은
dedup을 추가하려 했으나, 그 전에 base/lifecycle-pattern.md의
bindLifetime/unbindLifetime 실제 구현 스케치를 다시 추적해보니
애초에 "버그"가 아니었음이 드러났다:
bindLifetime/unbindLifetime은Relateweak 테이블에 대한 필드 쓰기 몇 개뿐 — 실제 Roblox 커넥션(gcconn)은 Instance 생성 시점에 단 한 번만 만들어져 모든bindLifetime호출이 공유(위 문서 "(0)" 절). 같은 값에 unbind 직후 바로 rebind해도 커넥션을 만들거나 끊지 않음 — 저렴하고 안전.Ref가 dedup이 꼭 필요했던 이유는RefLeafHandler.process가v:Set(inst)를 호출해서 — 이건 사용자가 등록한:Callback()을 스퓨리어스하게 재통지하는 관측 가능한 부작용. Observer/Effect의 Leaf 바인딩(bindLifetime만 호출)엔 이런 부작용이 없음 —fn재실행은 leaf 바인딩이 아니라 Observer/Effect 자기 내부 구독이 따로 트리거함.
이 재조사를 근거로 처음엔 base/dispatch-core-plan.md에 "Observer/Effect
Leaf는 이 dedup이 불필요 — 확인 후 기각"이라는 노트를 추가하고, 사용자에게
"버그가 아니었다"고 보고했다(이 시점까지는 커밋 전).
3차 — 사용자 반론: 성능상 == 비교가 항상 더 쌈, 그래도 넣는다
사용자가 correctness 문제가 아니라는 판단엔 동의하면서도, 성능
관점에서 == 비교(바이트코드 1개 + 분기)가 매번 여러 Relate weak
테이블 읽기/쓰기(해싱 비용)를 도는 것보다 항상 더 싸다고 지적 — 이득이
공짜에 가까운데 안 넣을 이유가 없다는 판단으로, RefLeafHandler와 같은
패턴을 그냥 넣기로 확정.
최종 반영
base/dispatch-core-plan.md"핸들러 내부 상태 저장" 절(4번) — "불필요, 확인 후 기각" 노트를 "채택함(correctness 아니라 순수 성능 최적화)"으로 교체, 근거(gcconn 공유로 안전함 +==비교가 항상 더 쌈) 둘 다 명시.base/source-state-plan.md에 새 절 "Observer/Effect Leaf dedup" 신설(위 "bindLifetime이 이 게이트의 두 번째 진입점이다" 절 바로 뒤, "PA님 코드와의 교차검증" 절 앞) —RefLeafHandler와 완전히 같은 모양의 pseudocode(old ~= v체크, retractor 안에서만relate정리하는 기존 주의사항까지 그대로 재사용).doc-check.pyERROR 0 유지 확인(중간에 절 제목이 줄바꿈에 걸쳐 인용되면서 생긴 WARN 1건은 그 자리에서 한 줄로 재정렬해 해소).
교훈
- "문제로 보인다"와 "실제로 문제다"는 구현 스케치를 끝까지 추적해야
갈린다 —
bindLifetime/unbindLifetime의 실제 비용(weak table 쓰기 몇 개, 커넥션 재생성 없음)을 확인하기 전까진 이게 실제 버그인지 판단할 근거가 없었음. 처음 보고("잠재적 갭")는 근거 없이 과하게 신중했고, 재조사 후 "버그 아님"도 성능 관점을 놓쳐 성급했음 — 두 번 다 사용자가 다음 질문으로 바로잡음. - "correctness엔 불필요"와 "넣을 가치가 없다"는 다른 결론 — 안전하다고 최적화까지 자동으로 기각되는 건 아님, 비용 대비 이득을 따로 판단해야 함.
4차 — /code-review 실행, 확정 findings 10건 + 추가 발견 1건
사용자가 /code-review(effort: high)를 실행 — 이 코퍼스는 소스코드가
없는 설계 문서 저장소라 "버그"는 자기모순/dead pseudocode/깨진 절
참조로 정의됨. 결과 10건을 ReportFindings로 렌더한 뒤, 같은
task-id가 한 번 더(다른 표본의 finder 조합으로) notify하며 1건을
추가로 찾아냄(effect-plan.md/ref-plan.md/source-state-plan.md의
"동적 경로 가드"가 볼드 텍스트일 뿐 실제 마크다운 헤딩이 아닌데 6곳이
절 제목처럼 인용 — doc-check.py로 직접 재확인해 사전에 존재하던
WARN임을 검증, "이번 diff가 새로 만들었다"는 리뷰의 프레이밍만 부정확
했음). 사용자가 "전부 고쳐줘"로 확정.
5차 — 고치던 중 사용자 개입: Tag/Attribute 자기등록 모델 자체가 틀림
findings #5(dispatch-core-plan.md:402)/#6(:560)을 고치려던 참에
사용자가 끼어들어 "tag, attribute를 base가 스스로 등록한다는 사실이
아닌거 알지?"라고 지적 — 이건 열한 번째 세션이 네 라운드 정정 끝에
확정했던 "TagHandler/AttributeKeyHandler/AttributeGroupHandler가
quad-base 모듈 로드 시점에 스스로 등록"이라는 결론 그 자체였다.
사용자가 정정한 모델: (1) TagHandler 등 그 이름들은 참조 카운트/이름
claim 알고리즘 구현일 뿐 — 공유 코드라 base에 위치하는 것뿐,
스스로 등록되는 주체가 아님. (2) HANDLER_PRIORITY_FALLBACK에 실제로
꽂히는 건 이를 감싸는 별도 이름의 엔티티(TagFallbackHandler 등)여야
함 — 이름 자체로 "자동 안전망"임을 구분. (3) 등록 주체는 quad-base
모듈이 아니라 필요한 엔진(백엔드 팩토리) — BaseModule 뮤테이션
시점에 자기 전용 Handler들과 같이 등록. 사후 검증 결과 이건
base/lifecycle-pattern.md가 이미 명시적으로 거부해둔 InitNamespace류
top-level 부작용 패턴과 정확히 같은 클래스의 실수였고,
base/module-lifecycle-plan.md가 이미 일반화해둔 "base는 인터페이스만,
등록/구현은 팩토리 뮤테이션 시점"이라는 원칙을 이 Tag/Attribute
결론만 예외로 뒀던 것이었다.
반영: archive/tag-attribute-load-time-registration-reversed.md
신설(뒤집힌 원문+근거), base/dispatch-core-plan.md("base가 소유하는
핸들러와 주입되는 엔진 op" 절 전면 재작성 + addHandler 절 수정),
base/tag-plan.md, base/attribute-plan.md, base/architecture.md
(Tag.luau 파일 설명), base/module-lifecycle-plan.md 전부 새 이름
(TagFallbackHandler/AttributeKeyFallbackHandler/
AttributeGroupFallbackHandler)과 새 등록 주체 서술로 갱신.
CLAUDE.md의 11번째 세션 서술은 역사적 기록으로 그대로 둠(그
세션 시점엔 최선의 결론이었음) — 이 재역전만 짧게 링크로 남김.
최종 반영 (code-review findings 11건 전부)
source-state-plan.mdObserverEffectLeafHandler.isHandlable에type(k) == "number"체크 추가(FALLBACK 가드가 죽은 코드였던 것 수정).source-state-plan.md의bindLifetimepseudocodecanExecute→canBound로 정정.README.mdsource-state-plan.md 행의 "canExecute 하나로 통합" 서술을canBound/canExecute분리 모델로 갱신.lifecycle-pattern.md445줄 "bindLifetime + canExecute"에canBound추가. 5-6.dispatch-core-plan.md의 Tag/Attribute 등록 모델 전면 재작성(위 5차 참고, 원래 findings의 "stale 문장"/"미설명 배너" 둘 다 이걸로 해소).ref-plan.md:585/source-state-plan.md:910의 존재하지 않는"HANDLER_PRIORITY_FALLBACK"절 인용을 실제 헤딩 제목("base가 소유하는 핸들러와 주입되는 엔진 op")으로 정정.question.md/CLAUDE.md/HUMAN_TODO.md의 "결정 대기 절이 비어 있음" 서술을 "헤딩째로 삭제됨"으로 정정(헤딩 자체가 이미 없었음).CLAUDE.md의 Debounce/Throttle "열린 질문 4개"를 개수 언급 없이question.md/debounce-throttle-plan.md§12로 위임(실제는 6개 — "개수는 소스 하나만" 원칙 적용).CLAUDE.md의 11번째 세션 기록을 ~103줄에서 ~13줄로 압축(전문은 이미session/2026-08-14-11-...md에 보존돼 있어 손실 없음).effect-plan.md/ref-plan.md/source-state-plan.md의 볼드 "동적 경로 가드" 3곳을 전부 실제###헤딩으로 승격 — 6곳의 절 인용이 전부 자연히 해소됨.
doc-check.py ERROR 0 / WARN 93→85 유지 확인(마지막에 새 archive
파일을 README 색인에 추가하지 않아 ERROR 1건 잠깐 발생 — 바로 수정).
교훈
- 문서가 "네 라운드 정정 끝에 확정"했다고 자기 서술해도 그게 진짜
맞다는 보장은 아님 — 같은 결론이 그 코퍼스 안의 다른 확정 원칙
(
module-lifecycle-plan.md의 팩토리 뮤테이션 패턴)과 충돌하는지는 "여러 번 검토했다"는 사실과 별개로 매번 다시 확인해야 함. - 사용자가 "아닌 거 알지?"로 끼어들면 그 자리에서 멈추고 확인부터 — 이미 진행 중이던 수정 계획(findings #5/#6)을 그대로 밀어붙였으면 틀린 전제 위에 새 "정정"을 또 쌓을 뻔했음.
6차 — 같은 실수의 다른 잔존 여부 전수 확인
사용자가 "다른 부분도 이런 실수 나온 거 있는지 봐줘"라고 요청 —
"모듈 로드 시점/require 시점에 스스로 등록·실행"류 top-level 부작용
주장을 코퍼스 전체에서 grep. 두 개는 오탐으로 확인(bind-system-plan.md
133줄은 그냥 정적 lookup 테이블 채우기라 부작용 없음, research/ debug-tooling-plan.md362줄은 React DevTools 비교 서술이라 quad 자신의
설계가 아님). dispatch-core-plan.md의 일반 Handler 등록 절(492-516줄,
NoneHandler/StoreBind/Leaf)은 이미 "BaseModule 테이블에 딸린
state"로 정확히 서술돼 있어 문제 없음 확인.
진짜 갭 발견: ROADMAP.md M10 체크리스트가 여전히 TagHandler/
AttributeKeyHandler/AttributeGroupHandler 자체를
HANDLER_PRIORITY_FALLBACK으로 등록한다고 서술 중이었고, 새로 분리된
TagFallbackHandler/AttributeKeyFallbackHandler/
AttributeGroupFallbackHandler 파일 자체가 체크리스트에 아예 없었음
— 이대로면 구현자가 이 세 파일을 만들 필요를 몰랐을 것. M10 배너에
정정 노트 추가, 세 알고리즘 항목에서 "스스로 등록 안 함" 문구로
정정, 새 Fallback 파일 셋을 체크리스트 항목으로 추가.
base/architecture.md의 AttributeKey.luau/Attribute.luau 파일 트리
설명에도 같은 Fallback 언급 보강(Tag.luau는 5차에서 이미 반영됨).
doc-check.py ERROR 0 유지.
7차 — 두 번째 /code-review high, 5차 정정의 잔존 흔적 3건 + 별개 버그 1건
사용자가 /code-review high를 재실행 — 5차의 Tag/Attribute 정정이
프로즈만 고치고 실제 pseudocode/다른 서술은 놓친 곳들을 정확히 잡아냄:
tag-plan.md:155—TagHandler.priority = HANDLER_PRIORITY_FALLBACKpseudocode가 100줄 아래 프로즈 정정과 모순(실제 코드로 복붙될 블록이라 가장 심각).TagHandler는.priority없음으로 수정, "패키지 배치" 절에TagFallbackHandler = { priority = ..., isHandlable = TagHandler.isHandlable, process = TagHandler.process }래퍼 pseudocode 신설.dispatch-core-plan.md:612— opt-in 가로채기 예시가 "TagHandler자신(FALLBACK)"이라고 서술, 몇 줄 위 재정정 블록과 모순 — 실제 FALLBACK에 있는 건TagFallbackHandler로 정정.ref-plan.md:257—RefLeafHandler.isHandlable이and not isPostRef(v)를 빠뜨림(PostRef 도입 9차 세션 때 이 자리가 안 갱신됨) — 같은 파일 783줄의 최종 공식과 불일치했던 걸 발견, 이번 세션과 무관한 별개의 사전 존재 버그였음(Tag/Attribute 정정과 무관). 정정.architecture.md:170—Leaf.luau파일 트리가v=Ref/Observer/ PreRef/PostRef만 나열하고Effect가 빠져 있었음(이번 세션 1~3차가 만든ObserverEffectLeafHandler와 불일치) —Effect추가 + 결합 핸들러 언급 보강.
전부 직접 재확인 후 반영(finder가 인용한 줄 번호/문맥을 실제로 열어
확인). doc-check.py ERROR 0 유지.
교훈: 프로즈만 고치고 pseudocode 블록을 안 고치는 게 이 세션에서 반복된 패턴(5차의 근본 원인과 같은 클래스) — "핸들러 계약을 코드 블록으로 정의하는 절"은 그 코드 블록 자체를 grep 대상에 넣어야 한다는 게 이번에 다시 확인됨.