메이플스토리월드 :: 인벤토리 구현 01

chooha·2026년 7월 28일

메이플스토리월드

목록 보기
1/15

📝 개발일지 - ItemData 초기화 순서 트러블슈팅

👨‍💻 오늘의 개발 작업

오늘은 인벤토리 시스템의 데이터 계층(ItemData)을 만들면서, 서로 다른 스크립트 간 OnBeginPlay 실행 순서가 보장되지 않는 문제를 발견하고 해결했음

PlayerInventory가 ItemData의 아이템 테이블을 참조해야 하는데, 어느 쪽이 먼저 초기화를 끝낼지 확신할 수 없는 상황이었음. 폴링(wait()) 없이, 이벤트 기반으로 안전하게 처리하는 방식으로 마무리함


💡 오늘의 기록

1. 문제 상황 파악

아이템 데이터(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는 이미 끝나 있다고 확신할 수 있나?

2. 근거 없이 넘겨짚었던 부분

처음엔 "Logic은 전역 싱글턴이니까 당연히 먼저 초기화되겠지"라고 가정하고 넘어가려 했음. 하지만 이 가정을 뒷받침할 근거가 없다는 걸 스스로 깨닫고, MSW 공식 문서를 다시 확인해봄

확인된 사실 (MSW 공식 문서)

"각 컴포넌트 내 OnBeginPlay의 호출 순서는 보장되지 않기 때문에,
특정 컴포넌트의 OnBeginPlay로 설정이 완료된 값을 다른 컴포넌트가
참조하면 오작동할 가능성이 있다"

공식 예제 코드(ClearInventoryOnBeginPlay)에서도 다른 컴포넌트(LiteInventory)의 초기화 완료를 기다리기 위해 IsInitialized 플래그를 확인하는 방식을 쓰고 있었음. 즉 "먼저 실행되겠지"라는 가정 자체가 애초에 성립하지 않는 환경이었음

3. 첫 번째 접근 - 폴링 방식 (기각)

MSW 공식 예제에 나온 방식을 그대로 적용해보려 했음

-- 공식 문서 예제 패턴
local inven = self.Entity.LiteInventory
while not inven.IsInitialized do
    wait(0.5)
end

기각한 이유

  • wait()은 코루틴을 점유한 채 일정 주기(0.5초)마다 계속 깨어나 플래그를 재확인하는 방식이라, 대기 중인 시간 동안 불필요한 컨텍스트 스위칭·검사 연산이 반복 발생함
  • 접속자가 늘어날수록 플레이어 수만큼 폴링 루프가 동시에 돌아가는 구조라, 서버 부하가 인원수에 비례해서 커짐 — 과제 요구사항인 "서버 최적화"와 정면으로 배치됨
  • 폴링 주기(0.5초) 때문에 실제 초기화가 끝난 시점과 이를 감지하는 시점 사이에 최대 0.5초의 불필요한 지연이 생김. 이벤트 방식이면 완료되는 즉시(지연 없이) 콜백이 실행됨
  • 결국 "값이 준비됐는지 반복해서 찔러보는" 방식이라, 값의 변화를 스스로 알려주는(이벤트 발행) 방식보다 근본적으로 비효율적인 구조임

4. 두 번째 접근 - 순수 이벤트 방식의 함정 ⚠️

폴링 대신 이벤트로 완전히 대체하려 했는데, 여기서 진짜 문제를 발견함

-- 문제가 될 수 있는 방식
-- PlayerInventory에서 이벤트 핸들러만 등록해두고 기다림

핵심 문제

  • 이벤트는 "구독한 시점 이후에 발생한 것"만 받을 수 있음
  • 만약 ItemData가 PlayerInventory보다 먼저 끝나버리면, PlayerInventory가 핸들러를 등록했을 땐 이미 이벤트가 지나가버린 뒤라 영원히 못 받는 상황이 생길 수 있음

즉 "이벤트 기반이냐 폴링이냐"가 핵심이 아니라, "이미 끝난 경우를 어떻게 감지하느냐"가 진짜 문제였음

5. 최종 해결 - 플래그 우선 확인 + 이벤트 보완

폴링 없이, 놓치는 경우도 없이 처리하기 위해 두 가지를 조합함

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() 반복 확인 없이 처리됨

6. 성과와 개선 효과

개선 결과

  • ✅ 레이스 컨디션 제거: 실행 순서와 무관하게 항상 안전하게 초기화됨
  • ✅ 폴링 없음: 지침서 원칙(이벤트 기반 우선) 그대로 유지
  • ✅ 이벤트 구멍 없음: "이미 끝난 경우"까지 플래그로 커버해서 이벤트만 썼을 때의 함정을 회피
  • ✅ 재사용 가능한 패턴 확립: 앞으로 다른 초기화 순서 문제에도 동일 패턴 적용 예정

📚 개발 참고

  • 인벤토리 스크립트 작성하기 — 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 타입 기준으로는 사실상 유일하게 성립하는 해법으로 최종 확정됨.

Logic ↔ Component 간 초기화 시 체크리스트

-- (완료를 알리는 쪽)
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

0개의 댓글