오늘은 인벤토리 시스템의 데이터 계층(ItemData)을 만들면서, 서로 다른 스크립트 간 OnBeginPlay 실행 순서가 보장되지 않는 문제를 발견하고 해결했음
PlayerInventory가 ItemData의 아이템 테이블을 참조해야 하는데, 어느 쪽이 먼저 초기화를 끝낼지 확신할 수 없는 상황이었음. 폴링(wait()) 없이, 이벤트 기반으로 안전하게 처리하는 방식으로 마무리함
아이템 데이터(ItemDataTable)를 매번 셀 단위로 조회하면 성능이 아깝다고 판단해서, 게임 시작 시 1회만 로드해 메모리에 캐싱하는 ItemData(Logic, 전역 싱글턴)를 만들었음
-- ItemData.OnBeginPlay
local itemDataSet = _DataService:GetTable("ItemDataTable")
local rowCnt = itemDataSet:GetRowCount()
for i = 1, rowCnt do
local data = ItemInfo()
-- ... 필드 채우기
self.itemTable[id] = data
end
그런데 이 itemTable을 실제로 쓰는 쪽은 플레이어별로 붙는 PlayerInventory(Component)였음. 여기서 곧바로 참조하는 코드를 짜다가 의문이 들었음:
PlayerInventory.OnBeginPlay가 실행되는 시점에,ItemData.OnBeginPlay는 이미 끝나 있다고 확신할 수 있나?
처음엔 "Logic은 전역 싱글턴이니까 당연히 먼저 초기화되겠지"라고 가정하고 넘어가려 했음. 하지만 이 가정을 뒷받침할 근거가 없다는 걸 스스로 깨닫고, MSW 공식 문서를 다시 확인해봄
확인된 사실 (MSW 공식 문서)
"각 컴포넌트 내 OnBeginPlay의 호출 순서는 보장되지 않기 때문에,
특정 컴포넌트의 OnBeginPlay로 설정이 완료된 값을 다른 컴포넌트가
참조하면 오작동할 가능성이 있다"
공식 예제 코드(ClearInventoryOnBeginPlay)에서도 다른 컴포넌트(LiteInventory)의 초기화 완료를 기다리기 위해 IsInitialized 플래그를 확인하는 방식을 쓰고 있었음. 즉 "먼저 실행되겠지"라는 가정 자체가 애초에 성립하지 않는 환경이었음
MSW 공식 예제에 나온 방식을 그대로 적용해보려 했음
-- 공식 문서 예제 패턴
local inven = self.Entity.LiteInventory
while not inven.IsInitialized do
wait(0.5)
end
기각한 이유
wait()은 코루틴을 점유한 채 일정 주기(0.5초)마다 계속 깨어나 플래그를 재확인하는 방식이라, 대기 중인 시간 동안 불필요한 컨텍스트 스위칭·검사 연산이 반복 발생함폴링 대신 이벤트로 완전히 대체하려 했는데, 여기서 진짜 문제를 발견함
-- 문제가 될 수 있는 방식
-- PlayerInventory에서 이벤트 핸들러만 등록해두고 기다림
핵심 문제
ItemData가 PlayerInventory보다 먼저 끝나버리면, PlayerInventory가 핸들러를 등록했을 땐 이미 이벤트가 지나가버린 뒤라 영원히 못 받는 상황이 생길 수 있음즉 "이벤트 기반이냐 폴링이냐"가 핵심이 아니라, "이미 끝난 경우를 어떻게 감지하느냐"가 진짜 문제였음
폴링 없이, 놓치는 경우도 없이 처리하기 위해 두 가지를 조합함
ItemData 쪽 — 완료 시 플래그 + 이벤트 둘 다 발행
-- ItemData.OnBeginPlay 마지막
self.isInitialized = true
local event = OnItemDataInitialized()
self:SendInitializedEvent(event)
PlayerInventory 쪽 — 플래그 먼저 확인, 아니면 이벤트로 보완
-- PlayerInventory.OnBeginPlay
if _ItemData.isInitialized then
-- 이미 끝나 있었던 경우: 이벤트 기다릴 필요 없이 즉시 진행
self:InitInventory()
end
-- OnItemDataInitialized 이벤트 핸들러도 별도 등록해서
-- 늦게 끝나는 경우까지 커버 (else 분기 불필요)
이 방식이면:
ItemData가 먼저 끝난 경우 → 플래그가 이미 true이니 즉시 진행PlayerInventory가 먼저 끝난 경우 → 아직 false이므로 이벤트 핸들러가 나중에 잡아줌wait() 반복 확인 없이 처리됨개선 결과
인벤토리 스크립트 작성하기 — MSW 공식 크리에이터 센터
다른 컴포넌트의 초기화 완료를 기다릴 때 IsInitialized 플래그를 확인하는 예제(ClearInventoryOnBeginPlay)가 실려 있음. 이번 문제의 대응 패턴을 잡을 때 기준으로 삼음
InventoryComponent API Reference — MSW 공식 문서
MSW 내장 인벤토리 시스템도 IsInitialized 프로퍼티 + InventoryItemInitEvent 이벤트를 함께 제공한다는 걸 확인. "플래그+이벤트 조합"이 MSW 생태계에서 실제로 쓰이는 패턴이라는 근거로 참고함
멋쟁이사자처럼 X 넥슨 MOD Supporters Hackathon 2주차 회고 / MSW 기본 이벤트 함수 — MSW 공식 문서
이번에 겪은 문제와 비슷한 사례를 코드 예시로 다룸. 공식 문서에서 OnInitialize(엔티티/컴포넌트 생성 직후, OnBeginPlay 이전 1회 호출)를 활용해 참조당하는 쪽(A)은 OnInitialize에서 값을 설정하고, 참조하는 쪽(C)은 OnBeginPlay에서 읽으면 항상 값이 채워져 있음을 보장하는 패턴을 제시함.
하지만 Logic인 ItemData에는 OnInitialize가 없어서 이 대안을 적용할 수 없고, 지금 쓴 "isInitialized 플래그 + OnItemDataInitialized 이벤트" 조합이 Logic 타입 기준으로는 사실상 유일하게 성립하는 해법으로 최종 확정됨.
-- (완료를 알리는 쪽)
property boolean isInitialized = false
method void OnBeginPlay()
-- ... 초기화 로직
self.isInitialized = true
local event = OnXxxInitialized()
self:SendXxxInitializedEvent(event)
end
-- (참조하는 쪽)
method void OnBeginPlay()
if _Xxx.isInitialized then
self:DoInit() -- 이미 끝났으면 즉시 진행
end
-- 이벤트 핸들러는 별도 등록, else 분기 불필요
end