quad/.claude/session/2026-08-12-10-attribute-name-ownership.md
qwreey 6b7af54e0d
decide(attribute): per-name ownership via private rawNew keys, not a new registry
Group Attribute(...) delegating through the public memoized AttributeKey(name)
cache meant a direct [AttributeKey "name"]=v write and a group field (or two
independent groups) targeting the same name silently converged on the same
dispatch slot -- last write wins, no error. Fix: groups use rawNew(name) to
mint a private per-name key cached on their own (inst,index) relation instead
of the shared cache, so AttributeKeyHandler can detect a conflicting claimant
by simple object-identity comparison -- no separate ownership registry needed.
Persisting names reuse the same cached key across group value swaps, so the
existing diff (nil only what left, leave the rest alone) keeps working.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 15:23:24 +09:00

4.1 KiB

2026-08-12 열 번째 세션 — Attribute 이름 소유권, rawNew 전용 키로 그룹/직접 쓰기 충돌 방지

배경

앞선 세션들(여덟/아홉 번째, Ref/Slot의 store 재바인드 정정)의 흐름을 이어, 사용자가 Attribute도 같은 종류의 문제가 있는지 확인 요청 — 그룹 Attribute(...)State<Attribute>로 동적으로 바뀔 수 있는데, 이미 확정된 diff 메커니즘("사라진 이름만 nil, 남은/새 이름은 그대로 설정") 자체는 맞지만, 서로 다른 두 원본(직접 리터럴 [AttributeKey "name"]=v와 배열파트 Attribute(store), 또는 서로 다른 두 그룹)이 같은 이름을 동시에 관리하려 하면 어떻게 되는가가 빠져있다는 지적.

진단

AttributeKey(name)이 이름별 weak 캐시로 항상 같은 객체를 리턴하고, 그룹의 위임 메커니즘이 Dispatch.process(inst, AttributeKey(name), source)로 그 공개 캐시를 그대로 씀 — 즉 서로 다른 원본이 같은 이름을 다루면 정확히 같은 (inst, k) 디스패치 자리로 수렴함. Modifier 필드는 override로 정적 충돌이 이미 해소되지만(한 Modifier 안에 같은 해시 키 중복 불가), 그룹의 이름 집합은 런타임에 동적이라 이 해소망 밖에 있다는 게 확인됨 — 실제로 일어날 수 있는 구조적 충돌.

설계 논의

1차로 Claude가 별도 Relate 기반 AttributeManageMap(claimant 타입을 구분해 등록/체크)을 제안했으나, 사용자가 더 단순한 안을 제시: 그룹이 공개 AttributeKey(name) 캐시를 안 쓰고, 캐시를 우회하는 rawNew(name)로 이름마다 자기 전용 키 객체를 만들어 자기 자신(그룹의 릴레이션)에 캐싱 — 그러면 "이 이름에 지금 어느 키 객체가 적용돼 있는가"를 보는 것만으로 소유권 판정이 되고, AttributeKeyHandler에서 바로 처리 가능. Claude가 이게 별도 claimant 타입 없이 AttributeKey 객체 identity 자체를 재사용하는 더 적은 부품의 설계임을 확인, 채택.

사용자가 추가로 확인 요청한 지점: 그룹 값이 교체될 때 diff가 "진짜 새 셋과 비교해 사라진 것만 nil화, 남은/새 이름은 그대로 갱신"으로 동작해야 이 메커니즘이 충돌 없이 맞물림 — 이미 확정된 diff 로직이 정확히 이 모양이라는 걸 재확인, 단 전용 키 캐시가 그룹 값 교체를 넘어 계속 유지돼야만 성립함을 짚음(매 교체마다 키를 새로 만들면 남아있는 이름조차 옛 키와 새 키가 달라 자기 자신과 충돌하는 꼴이 됨).

결정

  • 그룹의 기존 "(inst, 배열 위치)별 마지막으로 쓴 attribute 이름 문자열 집합" 릴레이션을 **"이름 → 그 이름 전용 키 객체 맵"**으로 확장(새 릴레이션 아님, 저장 형태만 바뀜) — 이름이 살아있는 동안 캐싱된 같은 키 객체를 재사용, 사라지면 그 키로 retractUnder, 새로 생기면 rawNew로 새 키.
  • AttributeKeyHandler는 per-inst owners: Relate() -- {[inst]={[name]: 현재 키 객체}}를 들고, process에서 다른 키 객체가 이미 그 이름을 쓰고 있으면 즉시 error, retract/nil 귀결 시 소유권 반납.
  • 패키지 경계 문제 없음 — AttributeKey도, 그룹의 실제 위임 로직도 이미 quad-roblox 소속이라 rawNew 호출이 새 역의존을 안 만듦.

반영

  • base/attribute-plan.md — "이름 소유권" 새 절 신설(메커니즘 전문), "메커니즘 — per-name 전용 키로..." 절 갱신(공개 캐시→그룹 전용 키로 정정, 캐시 영속성 조건 명시), "retract 불필요" 문단에 정정 각주 추가.
  • base/bind-system-plan.md — 일반 retract 계약 절의 "Attribute는 해당 안 함" 서술이 단일 키 경로에만 해당함을 명확화, 그룹의 이름 이탈은 오히려 Tag→nil급 retract 케이스라고 추가.
  • 다른 stale 참조(onchange-plan.mdAttributeKey(name) 언급 등)는 이 변경과 무관해 손 안 댐 — 확인만 하고 넘어감.