From b419d8c5e5ccc28c333235ae799e1ec1d49555b7 Mon Sep 17 00:00:00 2001 From: qwreey-agent-selene Date: Tue, 18 Aug 2026 22:23:32 +0900 Subject: [PATCH] =?UTF-8?q?qa:=20New()/Quad()=20=EB=8B=A4=EC=A4=91=20?= =?UTF-8?q?=EC=9D=B8=EC=8A=A4=ED=84=B4=EC=8A=A4=ED=99=94=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EC=A0=95=EC=A0=95=20=E2=80=94=20=EC=9D=B4=EC=A0=84?= =?UTF-8?q?=20=EC=BB=A4=EB=B0=8B=EC=9D=98=20=EC=98=A4=EC=97=AD=20=EC=A0=95?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 직전 커밋에서 New()를 전부 Quad()로 치환했던 게 사용자 의도를 잘못 읽은 것이었음 — 사용자가 직접 바로잡음. 실제 설계: Quad(require의 반환값)는 이미 만들어진 기본 싱글톤 인스턴스이고, 그 안의 New 필드를 명시적으로 호출해야만 별도의 새 Quad 네임스페이스가 생긴다. "그냥 Quad()를 부르면 매번 새 인스턴스"였다면, 컴포넌트를 여러 모듈로 쪼갠 앱에서 각자 인스턴스를 "얻는" 것 자체가 "새로 만들기"가 되어 서로 다른 인스턴스가 생기는 사고로 이어졌을 것. architecture.md/dispatch-core-plan.md/bind-system-plan.md/ module-lifecycle-plan.md의 New() 서술을 되돌리고, qa-request round1의 A-3 절에 후속 정정 문단을 추가했다. quad-doc-auditor로 두 라운드 감사해 새 발견 0건까지 수렴시켰다. Co-authored-by: qwreey --- .claude/base/architecture.md | 60 +++++++++++++++---- .claude/base/bind-system-plan.md | 12 ++-- .claude/base/dispatch-core-plan.md | 40 +++++++------ .claude/base/module-lifecycle-plan.md | 24 ++++---- .../pre-implementation-qa-round1.md | 15 +++++ 5 files changed, 105 insertions(+), 46 deletions(-) diff --git a/.claude/base/architecture.md b/.claude/base/architecture.md index 54480dd..42b32b8 100644 --- a/.claude/base/architecture.md +++ b/.claude/base/architecture.md @@ -98,15 +98,50 @@ quad는 이제 "스크립트"가 아니라 **라이브러리**다. DOMless Roblo 정확한 패키지 이름, 아래 "구현 착수" 절 참고) — base가 가상돔 없이도 프로바이더 패턴으로 백엔드를 받는 인터페이스만 정의하고, 실제 Roblox 구현은 `quad-roblox`가 담당. -13. **모듈은 기본 싱글톤, `Quad()`는 나중에.** 한 Lua 스레드에서 Roblox/비-Roblox - 프로바이더를 동시에 쓸 일이 거의 없을 거라 판단 — 필요해지면 그때 `Quad()` - 추가(**[정정, 2026-08-18 `/code-review high`] 이름은 아래 배너대로 - `New()`가 아니라 `Quad()`가 확정 — 이 문장도 그 이름으로 통일**). - **메커니즘도 이미 정해짐(2026-08-08 두 번째 세션, 새 설계 아니라 - 기존 패턴의 자연스러운 연장)**: v1처럼 `require`를 감싸 `Init(QuadId?)`로 - 격리 인스턴스를 만드는 방식은 안 씀 — 대신 지금 있는 "팩토리가 - `BaseModule`을 뮤테이션" 패턴(14번) 그대로, 매번 새 `BaseModule` - 테이블을 만들어 팩토리로 채우는 것뿐. 상세 근거는 +13. **모듈은 기본 싱글톤, `New()`는 추가 인스턴스가 필요할 때만.** + **[재정정, 2026-08-19 — 이전 정정(`New()`→`Quad()` 전면 치환)이 + 부정확했음, `/code-review high` 이후 사용자가 직접 바로잡음]** `New`와 + `Quad`가 이름이 다투는 게 아니다 — 이전 정정이 "미래 API 이름은 + `Quad()`"라고 단정하며 `New()`를 전부 `Quad()`로 바꿨던 게 틀렸다. + 사용자 원문: *"내가 말한건 Quad() 만 제공하면 항상 모든 Quad 요구처에서 + 각각의 Quad() 를 수행해서 새로운 모듈 스코프가 나온다는게 문제였어. + 그래서 Quad 는 기본적으로 생성된걸 리턴하긴 하는데, Quad.New() 도 + 제공하는거 어떻냐는거였음, 즉 New() 는 존재하고 기본 리턴은 New() 해서 + 주는건데, 리턴 안에 New 필드가 있고 그 함수를 쓰면 하나의 새로운 Quad + 네임스페이스가 만들어지는식. 안 그러면 모든 컴포넌트를 나눈 모델 + 등에서 Quad() 를 해서 새로운 모듈 스코프가 나와서, 래핑된 Quad() 를 + 수행하는 부분을 따로 작성해두어야한다던가 해질꺼임."* + - **`Quad`(`require`의 반환값, 호출 아님)는 이미 만들어진 기본 + 인스턴스 자체다** — 모듈 로드 시점에 그 모듈 자신이 내부적으로 + `New()`를 한 번 불러 만든 결과를 그대로 top-level 반환값으로 내보냄. + 그래서 평범한 소비자는 아무것도 호출할 필요 없이 `require(quad)`가 + 돌려준 걸 바로 `Quad.Dispatch`처럼 씀(지금 싱글톤 단계와 동일한 + 접근 방식 — 달라지는 게 없음). + - **`Quad.New()`가 명시적 opt-in으로 설계됨**(**구현은 아직 미착수** — + 지금은 싱글톤 단계라 `New` 필드 자체가 아직 안 노출됨, 바로 아래 + "M0 스캐폴딩에 주는 함의" 참고) — 다중 인스턴스화가 실제로 붙는 + 시점엔 반환된 기본 인스턴스 **안에** `New` 필드(함수)가 생기고, + 이걸 실제로 호출하면 완전히 **별도의 새 Quad 네임스페이스** + (자기만의 Dispatch 레지스트리 등)가 나오는 모양으로 짠다. Roblox + + 비-Roblox 프로바이더를 진짜로 동시에 써야 하는 드문 경우에만 이걸 + 쓰게 될 것. + - **왜 "그냥 `Quad()`를 부르면 새 인스턴스"로 안 하는가** — 컴포넌트를 + 여러 파일/모듈로 쪼갠 실제 앱에서는 각 파일이 독립적으로 "Quad + 인스턴스 하나 줘"를 요청하게 되는데, 그 요청 방법 자체가 "새로 + 만들기"라면 **파일마다 서로 다른, 서로 공유 안 되는 인스턴스**를 + 만들게 되는 사고가 난다(Dispatch 레지스트리가 안 공유되는 등) — + 그러면 앱 전체가 하나의 공유 부트스트랩을 따로 작성해서 그걸 통해서만 + Quad를 얻도록 강제해야 함. `require`가 이미 인스턴스화된 기본값을 + 주면 이 위험 자체가 원천적으로 없음 — 새 인스턴스가 필요한 그 드문 + 경우만 명시적으로 `.New()`를 부르면 되므로 실수로 스코프가 갈라질 + 일이 없다. + 한 Lua 스레드에서 Roblox/비-Roblox 프로바이더를 동시에 쓸 일이 거의 + 없을 거라 판단해 지금은 `New()` 없이 싱글톤만 두고, 필요해지면 그때 + `New()`를 노출한다. **메커니즘도 이미 정해짐(2026-08-08 두 번째 세션, + 새 설계 아니라 기존 패턴의 자연스러운 연장)**: v1처럼 `require`를 감싸 + `Init(QuadId?)`로 격리 인스턴스를 만드는 방식은 안 씀 — 대신 지금 있는 + "팩토리가 `BaseModule`을 뮤테이션" 패턴(14번) 그대로, 매번 새 + `BaseModule` 테이블을 만들어 팩토리로 채우는 것뿐. 상세 근거는 `base/dispatch-core-plan.md`의 "Dispatch는 프리미티브가 아니다" 절. **[정정, 2026-08-18 구현 전 QA]** 옛 서술은 그렇게만 하면 지금 module-level state로 사는 모든 것(`_initializedBy` 마커, Dispatch @@ -116,9 +151,10 @@ quad는 이제 "스크립트"가 아니라 **라이브러리**다. DOMless Roblo require 를 감싸지는 않고 단순히 InitModule(module) 등을 받도록 각 코드들을 약간 고쳐서 이것을 해결함."* 즉 **코드 변경 없이 자동으로** 되는 게 아니라, module-level state를 참조하는 코드들이 모듈 인스턴스를 - 인자로 받도록 **손을 대야** 한다. 미래 API 이름도 `New()`가 아니라 - **`Quad()`**(`Quad()`를 부르면 새 quad 인스턴스가 나오는 식)이고, - **지금은 단순 싱글톤이라 `Quad()` 없이 `Quad.Dispatch`로 바로 접근**한다. + 인자로 받도록 **손을 대야** 한다 — `New()`가 실제로 호출되면(위 opt-in + 경로) 그 순간 만들어지는 새 `BaseModule` 테이블에 대해 이 손질이 필요. + **지금은 `New()` 자체가 노출 안 된 싱글톤 단계라 `Quad.Dispatch`로 + 바로 접근**한다. - **M0 스캐폴딩에 주는 함의**: 레지스트리를 module-level upvalue로 직접 잡아두면 나중에 다중 인스턴스화할 때 전면 수정이 된다. 지금 싱글톤으로 가되, **그 참조 형태를 나중에 인자 하나 받는 걸로 바꾸기 diff --git a/.claude/base/bind-system-plan.md b/.claude/base/bind-system-plan.md index aa7cb5d..51d3b2c 100644 --- a/.claude/base/bind-system-plan.md +++ b/.claude/base/bind-system-plan.md @@ -112,11 +112,13 @@ RobloxFactory(QuadBase)` 세 줄 정도로 직접 조립하면 됨(별도 번들 아니라 **같은 팩토리 재호출(무시) vs 다른 팩토리로 유일 슬롯 충돌(에러)이라는 서로 다른 케이스를 각각 가리키고 있었음**. 구현은 모듈 테이블에 "누가 초기화했는지" 마커(`_initializedBy = "roblox"`류, 정확한 이름은 구현 단계)만 -두면 됨. 모듈 스코핑(`Quad()`, `base/architecture.md` 13번)과의 관계도 -실은 열려있던 게 아니라 자연히 풀림 — `Quad()`가 생기면 각 인스턴스가 -별도 테이블이 되므로 이 마커도 테이블별로 독립적으로 스코핑됨(**[한정, -2026-08-18 `/code-review high` — `architecture.md` 13번의 정정과 맞춤]** -단 "자동으로"는 아님 — module-level state를 참조하는 코드들이 모듈 +두면 됨. 모듈 스코핑(`New()`, `base/architecture.md` 13번)과의 관계도 +실은 열려있던 게 아니라 자연히 풀림 — `New()`가 실제로 호출되면 그 +호출이 만드는 인스턴스가 별도 테이블이 되므로 이 마커도 테이블별로 +독립적으로 스코핑됨(**[재정정, 2026-08-19 — `architecture.md` 13번의 +재정정과 맞춤]** `Quad`는 이미 만들어진 기본 인스턴스이고 `New()`는 그 +안의 opt-in 필드다, "`Quad()`를 부르면 매번 새 인스턴스"가 아님. 단 +"자동으로"는 아님 — module-level state를 참조하는 코드들이 모듈 인스턴스를 인자로 받도록 손을 봐야 하는 건 architecture.md 13번의 정정 그대로, 여기서 반복 안 함). diff --git a/.claude/base/dispatch-core-plan.md b/.claude/base/dispatch-core-plan.md index 79571cb..24cb368 100644 --- a/.claude/base/dispatch-core-plan.md +++ b/.claude/base/dispatch-core-plan.md @@ -150,7 +150,7 @@ src/schema/union.luau:48-68`) — 에러 메시지는 즉시 문자열로 만들 debug 모드일 때만. (Quad.debug: boolean = default false) 식이고, true 로 하면 디버깅 가능"*). `Quad.debug`는 **새 공개 API 표면**이라 `base/module-lifecycle-plan.md`(모듈 표면)에도 반영이 필요하고, - 다중 인스턴스화(`Quad()`, `base/architecture.md` "확정된 결정" 13번) 시 + 다중 인스턴스화(`New()`, `base/architecture.md` "확정된 결정" 13번) 시 이 플래그가 인스턴스별인지 전역인지는 그때 같이 정한다. `Dispatch.listHandlers()`도 같은 디버그 표면에 속하는지(=플래그와 무관하게 항상 호출 가능한지) 구현 시 정할 것. @@ -599,28 +599,30 @@ end 전부 하나의 우선순위 스캔을 공유. **[정정, 2026-08-10 세션]** Tween은 더 이상 별도로 등록되는 핸들러가 아님 — Property 핸들러 내부에서 소비되는 값-레벨 래퍼로 재설계됨(`base/tween-plan.md`). -- **모듈 재생성(`Quad()`)과의 관계 — 새 설계 불필요, 이미 있는 선례로 자연히 - 풀림.**(**[정정, 2026-08-18 `/code-review high`] 헤딩이 옛 이름 `New()`를 - 쓰고 있었음 — `architecture.md` 13번 항목과 같은 이유로 `Quad()`로 - 통일, 아래 "[한정]" 문단이 이미 정확한 이름을 씀**) v1처럼 `require`를 - 감싸 `Init(QuadId?)`로 격리 인스턴스를 만드는 +- **모듈 재생성(`New()`)과의 관계 — 새 설계 불필요, 이미 있는 선례로 자연히 + 풀림.** (**[재정정, 2026-08-19]** 이 헤딩을 한때 `Quad()`로 바꿨던 게 + 틀렸음 — `New()`가 맞는 이름, `architecture.md` "확정된 결정" 13번의 + 재정정이 소스. 요지: `Quad`(`require`의 반환값)는 이미 만들어진 기본 + 인스턴스이고, 그 안의 `New` 필드를 명시적으로 호출해야만 별도의 새 + Quad 네임스페이스가 생긴다 — "그냥 `Quad()`를 부르면 매번 새 인스턴스"가 + 아니다.) v1처럼 `require`를 감싸 `Init(QuadId?)`로 격리 인스턴스를 만드는 방식은 안 씀(위 "확정된 것" 절 — id 기반 조회 자체가 Ref로 대체되며 기각됨). 대신 이미 확정된 "base 유틸은 인터페이스, 실제 구현은 팩토리가 `BaseModule`을 뮤테이션해서 주입"(`RobloxFactory(BaseModule)`) 패턴을 그대로 따름 — Dispatch의 handler 레지스트리도 `BaseModule` 테이블에 딸린 state 중 하나일 뿐이라, `_initializedBy` 마커에 대해 이미 확정된 - 것과 완전히 같은 논리가 적용됨(위 "base 유틸은 인터페이스" 절, "`Quad()`가 - 생기면 각 인스턴스가 별도 테이블이 되므로 이 마커도 테이블별로 독립적으로 - 스코핑됨" — 단 아래 "[한정]" 문단대로 코드 손질은 필요, 재설계까지는 - 불필요). 다중 인스턴스화가 실제로 생기면 그 시점에 - BaseModule 전체를 인스턴스별 테이블로 만드는 메커니즘에 Dispatch도 자연히 - 같이 딸려가고, 호출부는 `module.Dispatch.process(...)`처럼 그 인스턴스 - 테이블을 통해 접근하게 됨 — 지금 미리 프리미티브화해둘 이유가 없음. - **[한정, 2026-08-18 구현 전 QA]** 다만 "재설계 불필요"가 **"코드 변경 - 불필요"는 아니다** — 사용자 판정에 따르면 그때는 module-level state를 - 참조하는 코드들이 모듈 인스턴스를 인자로 받도록(`InitModule(module)` 류) - 손을 봐야 하고, 미래 API 이름도 `New()`가 아니라 **`Quad()`**다 - (`base/architecture.md` "확정된 결정" 13번). 지금은 싱글톤이라 + 것과 완전히 같은 논리가 적용됨(위 "base 유틸은 인터페이스" 절, "`New()`가 + 실제로 호출되면 그 호출이 만드는 인스턴스가 별도 테이블이 되므로 이 + 마커도 테이블별로 독립적으로 스코핑됨" — 단 아래 "[한정]" 문단대로 코드 + 손질은 필요, 재설계까지는 불필요). 다중 인스턴스화가 실제로 생기면 그 + 시점에 BaseModule 전체를 인스턴스별 테이블로 만드는 메커니즘에 Dispatch도 + 자연히 같이 딸려가고, 호출부는 `module.Dispatch.process(...)`처럼 그 + 인스턴스 테이블을 통해 접근하게 됨 — 지금 미리 프리미티브화해둘 이유가 + 없음. **[한정, 2026-08-18 구현 전 QA]** 다만 "재설계 불필요"가 **"코드 + 변경 불필요"는 아니다** — 사용자 판정에 따르면 그때는 module-level + state를 참조하는 코드들이 모듈 인스턴스를 인자로 받도록 + (`InitModule(module)` 류) 손을 봐야 한다(`base/architecture.md` "확정된 + 결정" 13번). 지금은 `New()` 자체가 노출 안 된 싱글톤 단계라 `Quad.Dispatch`로 바로 접근한다. ### base가 소유하는 핸들러와 주입되는 엔진 op (2026-08-13 열네 번째 세션 신설) @@ -692,7 +694,7 @@ Fallback Handler들도 존재하지 않아**, 위 "매치 실패는 즉시 `erro 레지스트리**를 자기가 채우는 건 그 어느 쪽도 아니다 — 외부에 노출되는 init 표면이 늘지 않고, 순서 의존도 없고(레지스트리와 등록 코드가 같은 모듈), 사용자가 할 일도 없다. 백엔드가 나중에 자기 Handler를 등록해 이기는 구조도 -그대로다(Fallback 밴드는 항상 최하위). A-3의 다중 인스턴스화(`Quad()`)로 +그대로다(Fallback 밴드는 항상 최하위). A-3의 다중 인스턴스화(`New()`)로 가더라도 자리는 그대로 — 그때는 "모듈 로드 시"가 "인스턴스 생성 시"가 될 뿐이다. diff --git a/.claude/base/module-lifecycle-plan.md b/.claude/base/module-lifecycle-plan.md index 7f650e8..669cbe5 100644 --- a/.claude/base/module-lifecycle-plan.md +++ b/.claude/base/module-lifecycle-plan.md @@ -78,9 +78,12 @@ init하려 하면 오류, 없는데 뭔가 생성해서 bind하려 해도 오류 한 Lua 스레드에서 둘 이상의 모듈 분화체(Roblox+비Roblox 동시)를 쓸 일이 거의 없을 거라 판단, 지금은 싱글톤으로 두고 필요해지면 다중 인스턴스화를 -추가. **[정정, 2026-08-18]** 그때의 API 이름은 `New()`가 아니라 `Quad()`이고, -"코드 변경 없이 자동으로 스코핑"되는 게 아니라 module-level state를 참조하는 -코드들이 모듈 인스턴스를 인자로 받도록 손봐야 한다 — +추가. **[정정, 2026-08-18, 재정정 2026-08-19]** "코드 변경 없이 자동으로 +스코핑"되는 게 아니라 module-level state를 참조하는 코드들이 모듈 +인스턴스를 인자로 받도록 손봐야 한다. 이름은 `Quad()`가 아니라 +**`New()`가 맞음**(2026-08-18에 한 차례 `Quad()`로 잘못 정정됐다가 +바로잡힘) — `Quad`(`require`의 반환값)는 이미 만들어진 기본 인스턴스고, +`New()`는 그 안에서 명시적으로만 부르는 opt-in 필드다. 상세는 `base/architecture.md` "확정된 결정" 13번이 소스. ## 모듈 표면의 디버그 플래그 — `Quad.debug` (2026-08-18 신설, 사용자 요구) @@ -174,13 +177,14 @@ print**(`base/dispatch-core-plan.md`의 "핸들러 계약" 절)이고, 앞으로 백엔드는 opt-in으로 `HANDLER_PRIORITY_FALLBACK + 1`짜리 가로채기 Handler를 추가로 등록할 수 있음 — 상세는 `base/dispatch-core-plan.md`의 같은 절. **중복 호출 - 가드/`Quad()`와의 관계는 2026-08-04 3차 라운드에서 확정**: 같은 팩토리로 + 가드/`New()`와의 관계는 2026-08-04 3차 라운드에서 확정**: 같은 팩토리로 재호출하면 무시(no-op), 다른 팩토리로 재호출하면 에러(유일 슬롯 충돌 — - 바로 위 "Bind는 누가, 어떻게 구현하는가" 절의 원칙과 일치) — `Quad()`가 - 생기면 인스턴스별 테이블이 분리되므로 이 가드도 자연히 인스턴스별로 - 스코핑됨(**[한정, 2026-08-18 `/code-review high`]** 위 "모듈 스코핑" 절의 - 정정과 맞춰 — "자동으로"는 아니고 module-level state를 참조하는 코드는 - 손을 봐야 함, 그 손질까지 하고 나면 이 가드 자체는 재설계 불필요라는 - 뜻). **이 결론이 Dispatch의 handler 레지스트리에도 + 바로 위 "Bind는 누가, 어떻게 구현하는가" 절의 원칙과 일치) — `New()`가 + 실제로 호출되면 그 인스턴스별 테이블이 분리되므로 이 가드도 자연히 + 인스턴스별로 스코핑됨(**[한정, 2026-08-18 `/code-review high`, 이름 + 재정정 2026-08-19]** 위 "모듈 스코핑" 절의 정정과 맞춰 — "자동으로"는 + 아니고 module-level state를 참조하는 코드는 손을 봐야 함, 그 손질까지 + 하고 나면 이 가드 자체는 재설계 불필요라는 뜻). **이 결론이 Dispatch의 + handler 레지스트리에도 그대로 적용된다는 게 2026-08-08 두 번째 세션에서 재확인/일반화됨** — `base/dispatch-core-plan.md` "Dispatch는 프리미티브가 아니다" 절. diff --git a/.claude/qa-request/pre-implementation-qa-round1.md b/.claude/qa-request/pre-implementation-qa-round1.md index 484a74b..d996b40 100644 --- a/.claude/qa-request/pre-implementation-qa-round1.md +++ b/.claude/qa-request/pre-implementation-qa-round1.md @@ -100,6 +100,21 @@ D-7의 재역전 여부, N-4의 `NoneHandler`/`NilHandler` 역할 분담, ST-2 - **파급**: 실제 코드 배치에 영향 — module-level upvalue로 레지스트리를 잡아두면 나중 다중 인스턴스화 때 전면 수정이 되므로, 지금부터 그 참조 형태를 정해둘지 여부가 M0 스캐폴딩 결정이 됨. +- **⚠️ [후속 정정, 2026-08-19] 위 "어긋나는 지점"의 이름 해석이 부정확했음 + — `New()`를 통째로 `Quad()`로 바꾸는 건 사용자 의도가 아니었다.** 이 + 판정을 그대로 `base/architecture.md`에 반영했다가(2026-08-18) 여러 파일에 + `New()`→`Quad()` 전면 치환이 퍼졌었는데, 사용자가 직접 바로잡음 — + *"New 와 Quad 가 이름 모순은 아닐꺼야... Quad 는 기본적으로 생성된걸 + 리턴하긴 하는데, Quad.New() 도 제공하는거 어떻냐는거였음, 즉 New() 는 + 존재하고 기본 리턴은 New() 해서 주는건데, 리턴 안에 New 필드가 있고 + 그 함수를 쓰면 하나의 새로운 Quad 네임스페이스가 만들어지는식."* + 실제 의미: `Quad`(`require`의 반환값)는 이미 만들어진 기본 인스턴스이고, + `New()`는 그 안에 있는 **명시적 opt-in 필드**(호출하면 별도의 새 + 네임스페이스 생성) — "그냥 `Quad()`를 부르면 매번 새 인스턴스"가 아님. + 컴포넌트를 여러 모듈로 쪼갠 앱에서 각자 `Quad()`를 불러 인스턴스를 + "얻어야" 하는 모델이면 실수로 서로 다른 인스턴스가 생겨버리는 게 + 진짜 문제였음. 지금 유효한 서술은 `base/architecture.md` "확정된 결정" + 13번의 재정정이 소스. ---