design: RunInit 재설계(함수를 릴레이션 키로) + darklua 경계 실측 정밀화
New()의 멱등 Init 가드를 파일마다 Relate+센티널을 두던 방식에서
module:RunInit(initFn) 하나로 통합 — 함수 자체를 릴레이션 키로 써서
"이 함수, 이 모듈에 실행했는가"를 (module, initFn) -> boolean? 하나로
표현(사용자 제안). Debug/init.luau는 가드 없이 순수 뮤테이션만 하도록
단순화. smoke.init.luau로 재호출 무시/인스턴스별 독립/함수별 독립 3개
시나리오 검증(luau/luau-analyze/selene 클린).
darklua process를 실제로 돌려 @self/@game은 안 건드리고 커스텀 .luaurc
alias(@pkg)만 script.Parent류로 치환한다는 걸 확인 — project-setup-plan.md의
darklua 기각 근거를 "지금은 커스텀 alias를 안 쓰니 불필요, 나중에 도입하면
그때 필요해짐"으로 정밀화.
⚠️ 미결: RunInit을 backend 설치 진입점(QuadRoblox(Quad))에도 재사용할지
— 함수 identity 추적으로는 "다른 팩토리 재호출은 에러" 계약을 못 만족.
module-lifecycle-plan.md에 반영, M2/M5 착수 전 확인 필요.
Co-authored-by: qwreey <me@qwreey.moe>
This commit is contained in:
parent
ef3d952dd4
commit
9c3bfc890a
7 changed files with 246 additions and 64 deletions
File diff suppressed because one or more lines are too long
|
|
@ -85,67 +85,86 @@ export 타입을 최상위에서 그대로 재노출하는 게 Luau에서 문제
|
||||||
|
|
||||||
**순서 의존성은 각 `InitXxx`를 `require`처럼 멱등하게 만들어서 해소한다**
|
**순서 의존성은 각 `InitXxx`를 `require`처럼 멱등하게 만들어서 해소한다**
|
||||||
(2026-08-19, 사용자 제안) — 서브시스템 간 호출 순서를 최상위 `New()`가
|
(2026-08-19, 사용자 제안) — 서브시스템 간 호출 순서를 최상위 `New()`가
|
||||||
직접 관리할 필요가 없다: 각 `InitXxx` 파일이 자기 톱레벨(함수 클로저
|
직접 관리할 필요가 없다.
|
||||||
**밖**, 파일 스코프)에 `local relate = Relate()`를 하나 두고, `module`을
|
|
||||||
weak key 삼아 "이 `module` 인스턴스에 이미 Init됐는지"를 기록한다. 이미
|
**[2026-08-19 같은 날 후속 정정] 멱등 가드는 `RunInit` 하나로 통합됨 —
|
||||||
됐으면 그대로 스킵, 아니면 실제 작업을 한 번만 수행 — Lua의 `require`
|
파일마다 `Relate()`/`INITED` 센티널을 따로 두지 않는다.** 원래는 각
|
||||||
캐시와 같은 발상이지만, `require` 캐시는 **파일** 단위(Init 함수 자체는
|
`InitXxx` 파일이 자기 톱레벨에 `local relate = Relate()`를 두고 파일
|
||||||
한 번만 로드)인 반면 `New()`는 여러 번 호출돼 서로 다른 `module` 테이블을
|
전용 센티널 키(`INITED`)로 "이 `module`에 이미 Init됐는가"를 기록하는
|
||||||
여러 개 만들 수 있어서, "이 particular 인스턴스에" 멱등하려면 파일 스코프
|
보일러플레이트를 파일마다 반복했는데, **사용자 지적**: 그 반복 자체가
|
||||||
캐시로는 부족하고 `module`을 키로 하는 이 `Relate`가 따로 필요하다.
|
불필요하다 — **함수 자기 자신을 릴레이션 키로 쓰면** 센티널이 따로
|
||||||
|
필요 없다(`Relate<T, (any)->any, boolean>`이 곧 "이 함수, 이 모듈에
|
||||||
|
실행했는가" 표가 됨). `module` 인스턴스마다 공유하는 `RunInit` 메서드
|
||||||
|
하나가 이 판단을 전담하고, 개별 `InitXxx` 파일은 **가드 없이 그냥
|
||||||
|
뮤테이션만** 하면 된다:
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
-- Dispatch/init.luau
|
-- 최상위 init.luau
|
||||||
local Relate = require(...)
|
local Relate = require(...)
|
||||||
local relate = Relate() -- 파일 스코프, 클로저 밖 — 이 Init 전체가 공유하는 단 하나의 인스턴스
|
local runInitRelate = Relate() -- (module, initFn) -> 이미 실행됐는가, 파일 스코프 공유
|
||||||
|
|
||||||
local function Init(module)
|
local function New(): Quad
|
||||||
if relate:GetStrong(module, INITED) then
|
local module = { New = New } :: Quad
|
||||||
return -- 이미 이 module 인스턴스엔 Init됨, no-op
|
|
||||||
|
function module.RunInit(self, initFn)
|
||||||
|
if runInitRelate:GetStrong(self, initFn) then
|
||||||
|
return -- 이 module 인스턴스에 이 initFn은 이미 실행됨, no-op
|
||||||
|
end
|
||||||
|
runInitRelate:SetStrong(self, initFn, true) -- 실행 전에 먼저 표시(순환 의존 대비)
|
||||||
|
initFn(self)
|
||||||
end
|
end
|
||||||
relate:SetStrong(module, INITED, true) -- 실제 작업 전에 먼저 표시(순환 의존 대비, 아래 참고)
|
|
||||||
-- 자신의 의존성도 그냥 require+호출 — 상대도 멱등하므로 중복/순서 걱정 없음
|
module:RunInit(InitDebug)
|
||||||
-- 예: InitLifetime(module)
|
-- 서브시스템이 늘어날 때마다 이 자리에 module:RunInit(InitXxx)를 추가
|
||||||
local ...
|
return module
|
||||||
module.Dispatch = ...
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Debug/init.luau — 가드 없이 그냥 뮤테이션만(RunInit이 이미 1회만 보장)
|
||||||
|
local function Init(module)
|
||||||
|
module.debug = false
|
||||||
end
|
end
|
||||||
return Init
|
return Init
|
||||||
```
|
```
|
||||||
|
|
||||||
이렇게 하면 **의존하는 쪽이 자기 의존성을 직접 호출**하면 되고(`require`가
|
- **의존성을 갖는 `InitXxx`도 패턴이 그대로 유지된다** — 자기 의존성을
|
||||||
의존 그래프를 알아서 풀어주는 것과 같은 감각), 최상위 `New()`는 순서를
|
`module:RunInit(InitLifetime)`처럼 부르기만 하면 되고, `RunInit` 자체가
|
||||||
신경 쓰지 않고 아는 `InitXxx(module)`을 전부 호출해도 된다 — 이미 누가
|
멱등하므로 여러 `InitXxx`가 같은 의존성을 부르는 순서/중복은 걱정할
|
||||||
먼저 채웠으면 알아서 스킵된다.
|
필요 없다(옛 설계와 결론은 같음, 가드 소유 위치만 파일별→공유로 이동).
|
||||||
|
- **GC와도 자연히 맞물림** — `relate-plan.md`의 "API" 절에 따르면
|
||||||
- **GC와도 자연히 맞물림**: `relate-plan.md`의 "API" 절에 따르면 `Relate`의
|
`Relate`의 **첫 인자(`inst`, 여기선 `module`)는 항상 weak**이므로,
|
||||||
**첫 인자(`inst`, 여기선 `module`)는 항상 weak**다(선택의 여지가 없는
|
`Quad` 인스턴스가 더 이상 참조되지 않아 수거되면 이 Init-완료 기록도
|
||||||
고정 동작 — `Weak`/`Strong` 구분은 오직 `value` 쪽 보관 방식만 가리킴).
|
같이 사라진다(별도 정리 로직 불필요, `value`는 `SetStrong` — boolean
|
||||||
그래서 `value` 쪽을 `SetWeak`으로 두든 `SetStrong`으로 두든 상관없이,
|
리터럴이라 GC 결과엔 무관하지만 "다른 곳에서 안전하게 유지되는 것만
|
||||||
어떤 `Quad` 인스턴스(전체 `module` 테이블)가 더 이상 참조되지 않아
|
`SetWeak`" 일반 규칙에 맞음).
|
||||||
수거되면 이 Init-완료 기록도 같이 사라진다 — 별도 정리 로직 불필요.
|
|
||||||
`relate-plan.md`가 이미 확정해둔 "각 모듈이 자기 톱레벨에 `Relate()`
|
|
||||||
하나를 두고 재사용" 관례를 그대로 쓰는 것이라 새 메커니즘 아님.
|
|
||||||
(`relate-plan.md`가 별도로 명시한 "명시적으로 만든 기록은 명시적으로
|
|
||||||
지울 것" 원칙과 충돌하는 게 아니라 — 이 경우는 기록의 키 자체가 죽으면
|
|
||||||
그 기록을 다시 조회할 주체 자체가 사라지므로 지울 대상이 없어지는,
|
|
||||||
원칙이 애초에 상정하지 않은 자리다.)
|
|
||||||
- **`value`는 `SetStrong`으로 통일**: 위 문단대로 GC 결과엔 차이가 없지만
|
|
||||||
(boolean 리터럴은 애초에 GC 대상이 아님), `relate-plan.md`의 일반 규칙
|
|
||||||
"다른 곳에서 안전하게 유지되는 것은 항상 `SetWeak`" 기준으로는 이 `true`
|
|
||||||
플래그를 다른 어디도 붙잡고 있지 않으므로 `SetStrong`이 그 규칙에 맞는
|
|
||||||
선택이다.
|
|
||||||
- **`_initializedBy` 가드(아래 "Bind는 누가, 어떻게 구현하는가" 절, 실제
|
- **`_initializedBy` 가드(아래 "Bind는 누가, 어떻게 구현하는가" 절, 실제
|
||||||
정의는 `base/bind-system-plan.md`)와는 다른 층위** — 그건 backend
|
정의는 `base/bind-system-plan.md`)와는 여전히 다른 층위** — 그건
|
||||||
팩토리가 유일 슬롯을 채웠는지 **누가** 채웠는지까지 구분해야 하는 공개
|
backend 팩토리가 유일 슬롯을 채웠는지 **누가** 채웠는지까지 구분해야
|
||||||
계약(같은 팩토리 재호출=no-op, 다른 팩토리=에러)이고, 이건 quad-base
|
하는 공개 계약(같은 팩토리 재호출=no-op, 다른 팩토리=에러)이고, `RunInit`은
|
||||||
내부 서브시스템 각각이 **한 번만** 도는지만 보면 되는 사적 구현
|
quad-base 내부 서브시스템이 **한 번만** 도는지만 보면 되는 사적 구현
|
||||||
디테일이라 "다른 호출자면 에러" 같은 분기 자체가 없다. 이름이 겹치지
|
디테일이라 "다른 호출자면 에러" 분기 자체가 없다.
|
||||||
않게 구분해서 쓸 것.
|
**⚠️ [2026-08-19 미결, 사용자 질문] `RunInit`을 `QuadRoblox(Quad):
|
||||||
|
QuadRoblox`(backend 설치 진입점) 내부에서도 재사용해도 되는가?**
|
||||||
|
`RunInit`은 함수 identity로만 추적하므로, `InitRoblox`/`InitGtk`처럼
|
||||||
|
**서로 다른 함수가 같은 "백엔드 슬롯"을 다투는 경우를 구분 못 한다**
|
||||||
|
(둘 다 "아직 안 돈 함수"라 각자 조용히 실행됨 — 다른 팩토리 재호출을
|
||||||
|
에러로 잡아야 하는 계약과 정면으로 다름). `RunInit`은 "이 함수가 이미
|
||||||
|
돌았는가"만 답할 수 있고 "이 *슬롯*을 다른 함수가 이미 채웠는가"는
|
||||||
|
답할 수 없다는 게 핵심 차이 — backend 가드에 그대로 재사용하려면
|
||||||
|
별도 슬롯 키(예: 고정 이름 `"backend"`)로 감싸는 한 겹이 더 필요해
|
||||||
|
보이나, 결론 미정. M2/M5 착수 전 확인 필요.
|
||||||
- **플래그를 실제 작업 전에 먼저 세우는 이유**: 나중에 `InitA`↔`InitB`처럼
|
- **플래그를 실제 작업 전에 먼저 세우는 이유**: 나중에 `InitA`↔`InitB`처럼
|
||||||
상호 의존이 생기면([2026-08-19 기준] 지금은 없음, 대비만), 먼저
|
상호 의존이 생기면([2026-08-19 기준] 지금은 없음, 대비만), 먼저
|
||||||
표시해두지 않으면 무한 재귀에 빠진다 — `require`가 순환 참조 시
|
표시해두지 않으면 무한 재귀에 빠진다 — `require`가 순환 참조 시
|
||||||
미완성 exports를 돌려주는 것과 같은 이유로, 실제 작업 시작 전에 먼저
|
미완성 exports를 돌려주는 것과 같은 이유로, 실제 작업 시작 전에 먼저
|
||||||
"완료"로 표시해둔다.
|
"완료"로 표시해둔다.
|
||||||
|
- **실측**: `quad-base/src/init.luau`+`Debug/init.luau`가 위 코드
|
||||||
|
그대로 구현돼 있고, `quad-base/test/smoke.init.luau`가 (a) 같은
|
||||||
|
`initFn`을 여러 번 `RunInit`해도 1회만 실행, (b) `New()`로 만든
|
||||||
|
서로 다른 인스턴스는 기록을 공유하지 않음(각자 독립 1회), (c) 서로
|
||||||
|
다른 `initFn`은 서로의 실행 여부에 영향 안 줌 — 셋 다 `luau`/
|
||||||
|
`luau-analyze`/`selene` 클린으로 확인.
|
||||||
|
|
||||||
## Bind는 누가, 어떻게 구현하는가
|
## Bind는 누가, 어떻게 구현하는가
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,28 @@ require-by-string 의미론(`@self` 등)을 그대로 지원하므로, darklua
|
||||||
형태로 변환할 필요성 자체가 낮다는 판단. quad-base는 엔진 무관이어야
|
형태로 변환할 필요성 자체가 낮다는 판단. quad-base는 엔진 무관이어야
|
||||||
하므로 애초에 Roblox 전용 변환의 적용 대상도 아님.
|
하므로 애초에 Roblox 전용 변환의 적용 대상도 아님.
|
||||||
|
|
||||||
|
**[2026-08-19 후속 확인 — 정확한 경계]** `darklua` 바이너리(0.19.0)를
|
||||||
|
직접 설치해 참고 레포에서 실제로 `darklua process`를 돌려 정확히 어디까지
|
||||||
|
관여하는지 확인:
|
||||||
|
- **`require("@self/X")`/`require("@game/...")`는 `convert_require`가
|
||||||
|
아예 손을 안 댐** — `unable to require resource: unknown source name
|
||||||
|
'@self'`로 경고만 찍고 원문 그대로 통과시킴. 즉 예약 alias는 런타임이
|
||||||
|
직접 처리한다고 전제하고 있고, 이 세션의 결론과 정합.
|
||||||
|
- **커스텀 `.luaurc` alias(`@pkg` 등)는 다르다 — `convert_require`가
|
||||||
|
실제로 `@pkg/assets`를
|
||||||
|
`require(game:GetService('ReplicatedStorage'):WaitForChild('roblox_packages'):WaitForChild('assets'))`
|
||||||
|
로 치환함**(`.luaurc`의 alias 매핑 + Rojo sourcemap을 같이 읽어서
|
||||||
|
해석 — 대상 파일이 실제로 존재해야 성공, `pesde install` 전엔 실패).
|
||||||
|
즉 **커스텀 alias 기반 require를 실제로 쓰려면 darklua(또는 동급
|
||||||
|
빌드 스텝) 없이는 배포 시점에 그 문자열이 그대로 남아 동작을 보장할
|
||||||
|
근거가 없다** — standalone `luau` CLI가 커스텀 alias를 거부하는 것과
|
||||||
|
같은 결(`could not jump to alias`), Roblox 엔진이 예약 alias 밖의
|
||||||
|
커스텀 alias까지 자체적으로 푼다는 근거는 없음.
|
||||||
|
- **결론**: quad는 커스텀 alias를 전혀 안 쓰고 상대경로+`@self`만
|
||||||
|
쓰므로 지금은 darklua가 불필요. **나중에 `@pkg/quad_base`류 축약
|
||||||
|
alias를 도입하고 싶어지면 그때는 darklua(혹은 동급 변환)가 실질적으로
|
||||||
|
필요해진다** — 그 시점에 이 절을 다시 열 것.
|
||||||
|
|
||||||
## require 구조 — `@self`가 필수인 이유
|
## require 구조 — `@self`가 필수인 이유
|
||||||
|
|
||||||
**[2026-08-19, 사용자 지적 + Luau RFC `abstract-module-paths-and-init-dot-luau`로 확인]**
|
**[2026-08-19, 사용자 지적 + Luau RFC `abstract-module-paths-and-init-dot-luau`로 확인]**
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
# 2026-08-19, 여섯 번째 세션 — `RunInit` 재설계, darklua 경계 정밀화
|
||||||
|
|
||||||
|
**요약**: 사용자가 두 가지를 요청. (1) 지난 darklua 기각 근거를 실측으로
|
||||||
|
정밀화, (2) `New()`의 멱등 Init 가드를 파일마다 `Relate`+센티널을 두는
|
||||||
|
대신 **함수 자체를 릴레이션 키로 쓰는 공유 `module:RunInit(initFn)`**로
|
||||||
|
재설계. 둘 다 실제로 구현·검증까지 완료. 이후 대화는 한국어로 진행하기로
|
||||||
|
합의.
|
||||||
|
|
||||||
|
## 1. darklua 경계 실측 — `@self`/`@game`은 안 건드리고, 커스텀 alias만 변환
|
||||||
|
|
||||||
|
`darklua` 0.19.0을 직접 설치해 참고 레포(`initreq/roblox-project-example`)에서
|
||||||
|
`pesde install`+`rojo sourcemap`+`darklua process`를 실제로 돌려봄:
|
||||||
|
- `require("@self/X")`/`require("@game/...")`는 `convert_require`가
|
||||||
|
손을 안 대고 경고만 찍은 채 원문 그대로 통과(`unknown source name`).
|
||||||
|
- 커스텀 `.luaurc` alias(`@pkg`)는 실제로
|
||||||
|
`require(game:GetService('ReplicatedStorage'):WaitForChild('roblox_packages'):WaitForChild('assets'))`로
|
||||||
|
치환됨 — `.luaurc` 매핑 + Rojo sourcemap을 같이 읽어 해석.
|
||||||
|
|
||||||
|
결론: quad는 상대경로+`@self`만 쓰므로 지금 darklua는 정말 불필요하지만,
|
||||||
|
**나중에 `@pkg/quad_base`류 축약 alias를 쓰고 싶어지면 그때는 darklua가
|
||||||
|
실질적으로 필요해진다** — `project-setup-plan.md`에 이 경계를 정확히 반영.
|
||||||
|
|
||||||
|
## 2. `RunInit` 재설계
|
||||||
|
|
||||||
|
사용자 제안: 모듈 설정 완료 여부 릴레이션을 파일마다(`INITED` 센티널 +
|
||||||
|
`Relate()`) 따로 두지 말고, **함수 자체를 릴레이션 키로** 쓰고
|
||||||
|
`module.RunInit(initfun: (module)->any)`를 구현해 "실행한 적 없으면
|
||||||
|
실행"을 전담시키자는 것. 근거: `(any)->any : boolean?` 형태의 릴레이션이
|
||||||
|
간단하고, 파일마다 보일러플레이트를 반복할 이유가 없음.
|
||||||
|
|
||||||
|
실제로 반영:
|
||||||
|
- `quad-base/src/init.luau` — 최상위에 `runInitRelate = Relate()` 하나만
|
||||||
|
두고, `module.RunInit(self, initFn)`이 `(module, initFn) -> boolean?`
|
||||||
|
릴레이션으로 실행 여부를 판정. `Quad` 타입에 `RunInit` 필드 추가.
|
||||||
|
- `quad-base/src/Debug/init.luau` — 가드 보일러플레이트 전부 삭제, 그냥
|
||||||
|
무조건 `module.debug = false`만(멱등은 호출부 `RunInit`이 보장).
|
||||||
|
- `quad-base/test/smoke.init.luau` 신설 — 같은 `initFn` 재호출 시
|
||||||
|
1회만 실행, 서로 다른 `New()` 인스턴스는 기록 비공유, 서로 다른
|
||||||
|
`initFn`은 서로 무간섭 — 3개 시나리오 전부 검증. `selene`이 `assert`
|
||||||
|
메시지 누락/미사용 매개변수 몇 건을 잡아 같이 수정.
|
||||||
|
- `luau`/`luau-analyze`/`selene` 셋 다 클린.
|
||||||
|
- `module-lifecycle-plan.md` "New()의 내부 구성" 절 — 옛 파일별
|
||||||
|
센티널 의사코드를 새 `RunInit` 의사코드로 교체, 근거·GC 특성·
|
||||||
|
`_initializedBy`와의 층위 차이 재서술.
|
||||||
|
|
||||||
|
## 3. 미결 — `RunInit`을 backend 설치 진입점에도 재사용할지
|
||||||
|
|
||||||
|
사용자 질문: `RunInit`을 `QuadRoblox(Quad) -> QuadRoblox`(backend 주입
|
||||||
|
진입점) 내부에서도 그대로 써도 되는지. **답은 아직 안 냄** — `RunInit`은
|
||||||
|
함수 identity로만 추적하는데, backend 가드(`base/bind-system-plan.md`의
|
||||||
|
"Bind는 누가, 어떻게 구현하는가" 절)는 "같은 팩토리 재호출=no-op, **다른
|
||||||
|
팩토리=에러**"라는 계약이 있어서, `InitRoblox`/`InitGtk`처럼 서로 다른
|
||||||
|
함수가 같은 "백엔드 슬롯"을 다투는 상황을 `RunInit`은 구분 못함(둘 다
|
||||||
|
"아직 안 돈 함수"라 각자 조용히 실행되어 버림 — 에러가 나야 하는데 안
|
||||||
|
남). 슬롯 키를 별도로 감싸는 방안을 떠올렸으나 결론은 안 냄 —
|
||||||
|
`module-lifecycle-plan.md`에 ⚠️ 미결로 반영, M2/M5 착수 전 확인 필요.
|
||||||
|
|
||||||
|
## 4. 타입 관련 질문 — 답변 보류
|
||||||
|
|
||||||
|
사용자가 "pesde가 타입만 뽑아주는 게 있다고 들었다"며 quad-roblox가
|
||||||
|
quad-base 타입을 그걸로 갖게 되는지 물음. 워크스페이스 링크(심볼릭
|
||||||
|
링크로 연결된 실제 소스)를 통한 타입 추론은 이미 지난 세션에 실측
|
||||||
|
확인됐고 이 메커니즘과는 무관해 보이지만, `HUMAN_TODO.md` 8번이 말하는
|
||||||
|
"pesde의 타입 추출"(d.ts류, const 지원과 엮인) 기능 자체를 이 세션이
|
||||||
|
따로 조사하지 않아 정확한 답은 다음 턴에서 이어감.
|
||||||
|
|
||||||
|
## 5. 이후 진행
|
||||||
|
|
||||||
|
사용자 요청으로 이제부터 대화를 한국어로 진행.
|
||||||
|
|
@ -1,24 +1,14 @@
|
||||||
--[[
|
--[[
|
||||||
InitDebug(module) — `module.debug` 표면 설치.
|
InitDebug(module) — `module.debug` 표면 설치.
|
||||||
`.claude/base/module-lifecycle-plan.md` "모듈 표면의 디버그 플래그" /
|
`.claude/base/module-lifecycle-plan.md` "모듈 표면의 디버그 플래그" /
|
||||||
"New()의 내부 구성 — InitXxx 팩토리 체이닝" 절 그대로.
|
"RunInit — 함수 자체를 릴레이션 키로 쓰는 멱등 가드" 절 그대로.
|
||||||
|
|
||||||
각 InitXxx가 따르는 멱등 가드 모양의 첫 실제 사례 — 파일 스코프
|
멱등 가드는 `module:RunInit(InitDebug)` 호출부(`init.luau`)가 이미
|
||||||
`relate`(클로저 밖)로 "이 particular module 인스턴스에 이미 Init됐는가"를
|
보장하므로, 이 파일은 조건 없이 딱 한 번만 실행된다는 전제로 그냥
|
||||||
기록한다.
|
뮤테이션만 하면 됨 — 자기 자신의 가드를 따로 안 둠.
|
||||||
]]
|
]]
|
||||||
|
|
||||||
local Relate = require("./Relate")
|
|
||||||
|
|
||||||
local relate = Relate() -- 파일 스코프, 이 Init 전체가 공유하는 단 하나의 인스턴스
|
|
||||||
local INITED = {} -- 이 파일 전용 유일 키(다른 InitXxx의 relate와 네임스페이스가 안 겹침)
|
|
||||||
|
|
||||||
local function Init(module: any)
|
local function Init(module: any)
|
||||||
if relate:GetStrong(module, INITED) then
|
|
||||||
return -- 이미 이 module 인스턴스엔 Init됨, no-op
|
|
||||||
end
|
|
||||||
relate:SetStrong(module, INITED, true) -- 실제 작업 전에 먼저 표시(순환 의존 대비)
|
|
||||||
|
|
||||||
module.debug = false
|
module.debug = false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,39 @@
|
||||||
--[[
|
--[[
|
||||||
quad-base 최상위 진입점.
|
quad-base 최상위 진입점.
|
||||||
`.claude/base/architecture.md` 확정 결정 13 / `.claude/base/module-lifecycle-plan.md`
|
`.claude/base/architecture.md` 확정 결정 13 / `.claude/base/module-lifecycle-plan.md`
|
||||||
"New()의 내부 구성 — InitXxx 팩토리 체이닝" 절 그대로.
|
"New()의 내부 구성 — InitXxx 팩토리 체이닝" / "RunInit — 함수 자체를
|
||||||
|
릴레이션 키로 쓰는 멱등 가드" 절 그대로.
|
||||||
|
|
||||||
`require(quad-base)`는 이미 만들어진 기본 인스턴스 자체다 — 다중
|
`require(quad-base)`는 이미 만들어진 기본 인스턴스 자체다 — 다중
|
||||||
인스턴스화가 필요한 드문 경우에만 반환값의 `.New()`를 명시적으로 호출.
|
인스턴스화가 필요한 드문 경우에만 반환값의 `.New()`를 명시적으로 호출.
|
||||||
]]
|
]]
|
||||||
|
|
||||||
|
local Relate = require("@self/Relate")
|
||||||
local InitDebug = require("@self/Debug")
|
local InitDebug = require("@self/Debug")
|
||||||
|
|
||||||
|
-- (module, initFn) -> 이미 실행됐는가. New()가 몇 번 불려도 module마다
|
||||||
|
-- weak-키잉되므로 별도 정리 불필요(module이 GC되면 이 기록도 같이 사라짐).
|
||||||
|
local runInitRelate = Relate()
|
||||||
|
|
||||||
type Quad = {
|
type Quad = {
|
||||||
New: () -> Quad,
|
New: () -> Quad,
|
||||||
|
RunInit: (self: Quad, initFn: (Quad) -> any) -> (),
|
||||||
debug: boolean,
|
debug: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
local function New(): Quad
|
local function New(): Quad
|
||||||
local module = { New = New } :: Quad
|
local module = { New = New } :: Quad
|
||||||
|
|
||||||
InitDebug(module)
|
function module.RunInit(self, initFn)
|
||||||
-- 서브시스템이 늘어날 때마다 이 자리에 InitXxx(module)를 순서 무관하게 추가
|
if runInitRelate:GetStrong(self, initFn) then
|
||||||
|
return -- 이 module 인스턴스에 이 initFn은 이미 실행됨, no-op
|
||||||
|
end
|
||||||
|
runInitRelate:SetStrong(self, initFn, true) -- 실제 실행 전에 먼저 표시(순환 의존 대비)
|
||||||
|
initFn(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
module:RunInit(InitDebug)
|
||||||
|
-- 서브시스템이 늘어날 때마다 이 자리에 module:RunInit(InitXxx)를 순서 무관하게 추가
|
||||||
|
|
||||||
return module
|
return module
|
||||||
end
|
end
|
||||||
|
|
|
||||||
67
quad-base/test/smoke.init.luau
Normal file
67
quad-base/test/smoke.init.luau
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
--[[ 임시 스모크 테스트 — New()/RunInit의 멱등 가드가 기대대로 동작하는지 확인 ]]
|
||||||
|
|
||||||
|
local Quad = require("../src")
|
||||||
|
|
||||||
|
print("=== 1. New()가 만든 module엔 InitDebug가 이미 반영돼 있음 ===")
|
||||||
|
assert(Quad.debug == false, "top-level require(quad-base)도 New()를 거쳐야 함")
|
||||||
|
print("PASS")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=== 2. RunInit은 같은 함수를 다시 넘겨도 재실행하지 않음 ===")
|
||||||
|
do
|
||||||
|
local runCount = 0
|
||||||
|
local function initFn(module)
|
||||||
|
runCount += 1
|
||||||
|
module.marker = true
|
||||||
|
end
|
||||||
|
|
||||||
|
Quad:RunInit(initFn)
|
||||||
|
Quad:RunInit(initFn)
|
||||||
|
Quad:RunInit(initFn)
|
||||||
|
|
||||||
|
assert(runCount == 1, "같은 initFn을 여러 번 RunInit해도 실제 실행은 1회여야 함")
|
||||||
|
assert(Quad.marker == true, "실행됐다면 마커가 세팅돼 있어야 함")
|
||||||
|
print("PASS: " .. runCount .. "회 호출 중 실제 실행 " .. runCount .. "회")
|
||||||
|
end
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=== 3. New()로 만든 서로 다른 module은 RunInit 기록을 공유하지 않음 ===")
|
||||||
|
do
|
||||||
|
local runCount = 0
|
||||||
|
local function initFn(_module)
|
||||||
|
runCount += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
local a = Quad.New()
|
||||||
|
local b = Quad.New()
|
||||||
|
assert(a ~= b, "New()는 매번 독립된 module을 만들어야 함")
|
||||||
|
|
||||||
|
a:RunInit(initFn)
|
||||||
|
b:RunInit(initFn)
|
||||||
|
|
||||||
|
assert(runCount == 2, "서로 다른 module 인스턴스는 각자 한 번씩 실행돼야 함(총 2회)")
|
||||||
|
print("PASS: 독립 인스턴스 2개 각각 1회씩, 총 " .. runCount .. "회")
|
||||||
|
end
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=== 4. 서로 다른 initFn은 서로의 실행 여부에 영향을 안 줌 ===")
|
||||||
|
do
|
||||||
|
local m = Quad.New()
|
||||||
|
local aRan, bRan = false, false
|
||||||
|
local function initA(_module)
|
||||||
|
aRan = true
|
||||||
|
end
|
||||||
|
local function initB(_module)
|
||||||
|
bRan = true
|
||||||
|
end
|
||||||
|
|
||||||
|
m:RunInit(initA)
|
||||||
|
assert(aRan == true and bRan == false, "initA만 돌았어야 함")
|
||||||
|
m:RunInit(initB)
|
||||||
|
assert(aRan == true and bRan == true, "이제 둘 다 돌았어야 함")
|
||||||
|
m:RunInit(initA) -- no-op이어야 함, 에러 없이 통과하면 충분
|
||||||
|
print("PASS")
|
||||||
|
end
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=== ALL PASS ===")
|
||||||
Loading…
Reference in a new issue