# 2026-08-21-01 — `Detach` 보존 주체 / `KeyGone` / `Owned` 확정, `attachSlot` 분해 **한 줄**: QA 4라운드 followup의 마지막 열린 항목(`F-3`)이 사용자 회신으로 전량 닫히면서, `:List`의 요소 소유권 모델(`slot._detached` + `KeyGone` + `Owned`)과 `attachSlot`의 책임 분해(`materializeSlotTree`/`mountSlotTree`)가 확정돼 `base/slot-plan.md`에 반영됐다. **소스 관계**: 지금 유효한 설계는 항상 `base/slot-plan.md`가 소스이고, 처리 경과 요약은 `qa-request/pre-implementation-qa-round4-followup.md`의 H절, 분해 근거는 `research/slot-attach-decomposition.md`. 이 파일은 그 결정들이 **어떤 논의를 거쳐 나왔는지**의 원문 기록이다. --- ## 1. 발단 — `F-3`을 제안하게 된 관측 3차 처리(G절)까지 오면서 `Detach`(옛 `PopOnly`) 경로에 두 개의 구멍이 남아 있었다. 1. **보존 주체가 `userdata`였다.** 기존 문서는 `updateFn`이 `Detach, { old = ..., source = ... }`를 반환하면 "보존은 반환한 userdata가 담당"한다고 확정해뒀다. 2. **키가 데이터에서 사라졌을 때의 처분이 미결**이었다(`question.md` 3번, `ROADMAP.md` M6에 ⚠️로 박혀 있었음). 2번을 파다가 1번이 함께 무너졌다. 결정적인 근거는 **gcconn 트릭**이다 — `base/lifecycle-pattern.md`가 확정해둔 대로 quad가 만든 모든 Instance는 `GetPropertyChangedSignal("ClassName")` 커넥션의 클로저가 자기 자신과 `gchold`를 캡처하므로 **참조를 놓는 것만으로는 회수되지 않는다.** 즉 detach된 요소를 `userdata` 안에 넣어두고 `:List`가 그 키를 잊어버리면 **GC 폴백이 아예 없이 영구 누수**다. `userdata`는 `:List`에게 opaque라 "이 안에 뭐가 들었으니 죽여라"를 알 수 없다. 그래서 세 가지를 묶어 제안했다: (1) 보존은 Slot 필드가 한다, (2) owner가 죽을 때의 정리는 `Effect`가 한다, (3) 키가 사라지면 `updateFn`에게 한 번 더 묻는다. ## 2. 사용자 회신 — 두 가지를 더 얹었다 > "Detach 요소는 slot 안에 보관하는게 내 생각이였어서 (2) 제안에 동의. ud > 에 넣는거로는 최종 처분이 불가하다에 동의함. 이미 detach 인데 또 detach 를 > 보내도록 하면 nop하게 두고, detach 를 다시 안 보내고 prev 를 사용하게 > 된다면 재마운트 해주는거 괜찮은 아이디어같음. base에 전부 반영해줘." 여기서 **"이미 detach인데 또 detach → nop"**과 **"`prev`를 그대로 반환 → 재마운트"**가 확정됐다. 이 둘은 원래 제안에 없던 것으로, 보존 주체가 `slot._detached`로 옮겨간 순간 **`ud`에 홀드할 필요 자체가 없어진다**는 따름정리를 사용자가 먼저 짚은 것이다 — reconcile이 `prev`로 그대로 돌려주니까 사용자 코드가 `old`를 `ud`에 담아 다니지 않아도 된다. ## 3. 반영 — `settle()`이 두 경로를 강제로 합친다 구현상 가장 중요한 판단은 **정상 사이클과 소멸 루프가 같은 처분 함수를 공유하게 만든 것**이다. `settle(key, result, detach, pos)` 하나가 `wasMounted`/`wasDetached`를 함께 보고 분기한다: - `detach`면 → `rawDetach`(언마운트하되 **소유권 유지**), `_detached`로 이동. 이미 `_detached`에 있으면 `wasMounted == nil`이라 자연히 nop. - `result == prev`인데 `wasDetached`면 → `_detached`에서 빼고 `rawAdd` = 재마운트. - `result == nil`이면 → `releaseElement(self, prev, wasDetached ~= nil)`. 두 경로가 갈라질 수 없게 만든 이유는 명확하다. 이 코퍼스에서 반복적으로 났던 버그(`RC-1`/`RC-3`/`RC-4`)가 전부 **"비슷한 두 경로 중 한쪽만 고쳤다"** 계열이었다. `rawRemove`(소유권 해제 + 파괴) / `rawUnmount`(소유권 해제 + 파괴 안 함) / **`rawDetach`(소유권 유지 + 파괴 안 함)** 3형제로 분화한 것도 같은 이유 — "파괴하는가"와 "소유권을 놓는가"가 원래 한 축에 뭉쳐 있었는데, `Detach`는 정확히 **파괴 안 하면서 소유권은 유지**하는 조합이라 기존 둘로는 표현이 안 됐다. ## 4. `Owned` — `Detach`와 직교하는 두 번째 축 `C-2`(`:List`의 "밀려난 prev는 dispose"가 `state` 의미론과 충돌)를 닫은 것이 이 축이다. 정리하면: - **`Detach`** = 사이클 단위. "내 건데 잠깐 빼둠." - **`Owned`** = 설치 단위. "애초에 내 게 아님." 이 둘이 직교한다는 걸 명시하고 나니 충돌이 사라졌다 — 값 교체 시 파괴는 `Owned = true`일 때 **맞다**(그 요소는 `updateFn`이 만들었고, `:List`가 안 지우면 아무도 못 지운다). 사용자가 `state`에 담아 넘긴 요소는 `Owned = false`로 설치되고 어떤 경로로도 파괴되지 않는다. ## 5. `attachSlot` 분해 — 사용자가 비용 논거로 확정 3차 처리에서 `F-4-3`(`setLength` 위치)을 물었을 때, 사용자가 문제를 더 크게 재정의했다: "attachSlot 의 기능이 너무 다양해진게 문제같음." 핵심 진단은 **C6과 C7이 한 함수 안에서 동시에 만족될 수 없다**는 것이었다: - **C6**: 부모에게 미는 길이(`setLength(ownerKey, position, ...)`)는 flush가 끝나야 정해지는 **최종값**이어야 한다. - **C7**: 부기는 물리 트리 조작보다 항상 먼저 끝나야 한다. 한 함수엔 `setLength` 슬롯이 하나뿐이라, 앞에 두면 C6 위반(부모가 1회 헛돎), 뒤에 두면 C7 위반이다. **함수를 둘로 쪼개면 각자 하나씩 만족**한다. 사용자가 분해안(B)을 검토하고 낸 판단이 결정적이었다: > "오히려 안 쪼갤 이유가 정당하지 않아보이는게, 안 쪼갰을 때 구현 상 실수가 > 많아질 위험이 보임." > "이게 하나의 큰 복잡한 복합 함수라 여러 session 간의 실수가 발생하던 > 부분이고, 지금 적절한 방향으로 이동하지 않으면 계속 실수에 의한 > 시간/기술비용이 축적될것 같음. 지금 의사코드를 건들이는 비용이, 추후 > 실수가 누적되는 비용보다 싸다고 생각함." 이건 `conventions.md`의 "드문 오용이나 가상의 미래 요구까지 방어/최적화하려고 구조를 복잡하게 만들지 않는다" 원칙과 **충돌하지 않는다** — 이 분해가 방어하는 건 가상의 미래 요구가 아니라 **이미 세 번 관측된 버그 클래스**(`RC-1`/`RC-3`/`RC-4`, 전부 "줄 순서를 잘못 잡아서")이기 때문이다. 분해하면 그 순서 제약이 줄 순서가 아니라 **함수 경계로 강제**된다. ### 사용자가 스스로 답한 우려 같은 회신에서 사용자가 유일한 우려를 제기하고 스스로 결론까지 냈다: > "다만 이렇게 된다면 얼마나 nested 든, 하나 마운트되고 하나 마운트되고... > 각 객체들이 Parent = inst 되어가며 피지컬에 붙어가는 일자 진행인데, 이건 > 관측 이후 일괄 등록이라는 차이가 있는듯. 하지만 이미 리스트 액티베이션이 > 자신의 길이를 정확히 알려면 재귀를 뛰어야하는 부분이고 … 이런 사소한, > 문제를 야기하기 어려운 동작을 원래와 동치시키기 위해 디버깅이 어려운 > 함수를 만드는건 옳지 않아보임." 확인해보니 **`Parent` 대입 순서 자체는 안 바뀐다** — 바뀌는 건 `ChildAdded` 핸들러가 볼 때 서브트리의 `Length`/`Offset`이 이미 최종값이라는 점뿐이고, 옛 코드는 거기서 **미완성 스냅샷**을 보여줬다. 즉 이 변화는 동치성 손실이 아니라 **엄밀히 더 정확해지는 방향**이다. 분석 원문은 `research/slot-attach-decomposition.md` 7절. ## 6. 이 세션이 안 한 것 - **5라운드 문항지를 만들지 않았다** — D절 회신의 "이후 stale 만 잡는것으로 끝낼 수 있어보임"에 따름. - **스파이크 두 건은 미실행**(`01` 재작성 = 단일 generalized `for`, `table.insert` 구멍 재사용 = `R-11`). 상태의 소스는 `luau-test/STATUS.md`. - 이 분해로 `Dispatch.drive`도 같은 모양(부기/물리 분리)으로 맞출지는 **일부러 미뤘다** — 지금 그게 아프다는 증거가 없다.