프로그래밍 언어 실전 가이드

루아 입문 시리즈 #3: 루아 테이블 완전 정복 – 연관 배열부터 메타테이블까지

devcomet 2025. 5. 16. 16:30
728x90
반응형

루아 입문 시리즈 #3: 루아 테이블 완전 정복 – 연관 배열부터 메타테이블까지
루아 입문 시리즈 #3: 루아 테이블 완전 정복 – 연관 배열부터 메타테이블까지

 

루아(Lua)를 제대로 이해하려면 테이블(Table)을 빼놓을 수 없습니다.

다른 언어에서 배열, 객체, 딕셔너리로 나뉘어진 기능들이 루아에서는 모두 테이블 하나로 통합되어 있습니다.

이것이 루아의 단순함이면서 동시에 강력함의 원천입니다.

이 글에서는 루아 테이블의 내부 동작 원리부터 실무에서 마주할 수 있는 복잡한 활용 사례까지, 단계별로 심도 있게 다뤄보겠습니다.

특히 다른 언어 경험자들이 놓치기 쉬운 루아만의 독특한 특성들을 중점적으로 살펴보겠습니다.


루아 테이블의 내부 구조와 동작 원리

루아 테이블이 다른 언어의 자료구조와 다른 점은

배열 부분(array part)해시 부분(hash part)이 내부적으로 분리되어 있다는 것입니다.

이는 성능 최적화를 위한 설계로, 연속된 정수 키는 배열로, 나머지는 해시 테이블로 관리됩니다.

-- 루아는 내부적으로 이렇게 구분합니다
local mixed = {
    -- 배열 부분 (연속된 정수 인덱스)
    "first",    -- [1]
    "second",   -- [2]
    "third",    -- [3]

    -- 해시 부분 (비연속 인덱스나 문자열 키)
    [10] = "tenth",
    name = "example",
    ["special-key"] = "value"
}

-- 배열 부분의 길이만 반환
print(#mixed)  -- 출력: 3 (해시 부분은 무시됨)

이러한 내부 구조 때문에 # 연산자는 배열 부분의 길이만 반환하며, 중간에 nil이 있으면 예상과 다른 결과가 나올 수 있습니다.

local sparse = {1, 2, nil, 4, 5}
print(#sparse)  -- 2 또는 5 (구현에 따라 다름)

-- 안전한 배열 길이 계산
local function safeLength(t)
    local count = 0
    for i = 1, math.huge do
        if t[i] == nil then break end
        count = i
    end
    return count
end

테이블 생성 최적화와 성능 고려사항

테이블 생성 시 초기 크기를 예측할 수 있다면 성능을 크게 향상시킬 수 있습니다.

루아는 테이블이 커질 때마다 메모리를 재할당하는데, 이를 미리 방지할 수 있습니다.

-- 비효율적인 방법 (매번 재할당)
local function createSlowArray(size)
    local t = {}
    for i = 1, size do
        t[i] = i * i
    end
    return t
end

-- 효율적인 방법 (사전 할당)
local function createFastArray(size)
    local t = {}
    -- 배열 부분 사전 할당
    for i = 1, size do
        t[i] = false  -- 임시값으로 공간 확보
    end
    -- 실제 값 할당
    for i = 1, size do
        t[i] = i * i
    end
    return t
end

-- 해시 부분도 고려한 테이블 생성
local function createOptimizedTable()
    return {
        -- 배열 부분 (연속된 인덱스로 시작)
        nil, nil, nil, nil, nil,  -- 5개 슬롯 확보

        -- 해시 부분 초기화
        _size = 0,
        _capacity = 5
    }
end

고급 테이블 순회 패턴과 성능 비교

테이블 순회는 상황에 따라 다른 방법을 선택해야 합니다.

각 방법의 특성과 성능을 이해하고 적절히 활용해보겠습니다.

local data = {}
for i = 1, 1000 do
    data[i] = math.random(1, 100)
    data["key" .. i] = "value" .. i
end

-- 1. ipairs: 배열 부분만 순회 (가장 빠름)
local function sumWithIpairs(t)
    local sum = 0
    for i, v in ipairs(t) do
        sum = sum + v
    end
    return sum
end

-- 2. pairs: 전체 테이블 순회 (해시 부분 포함)
local function sumWithPairs(t)
    local sum = 0
    for k, v in pairs(t) do
        if type(v) == "number" then
            sum = sum + v
        end
    end
    return sum
end

-- 3. 숫자 인덱스 직접 순회 (메모리 효율적)
local function sumWithIndex(t)
    local sum = 0
    for i = 1, #t do
        sum = sum + t[i]
    end
    return sum
end

-- 4. 커스텀 반복자 (특수한 조건)
local function evenIndexIterator(t)
    local i = 0
    return function()
        repeat
            i = i + 2
        until t[i] == nil or i > #t
        if t[i] then
            return i, t[i]
        end
    end
end

-- 짝수 인덱스만 순회
for i, v in evenIndexIterator(data) do
    print(i, v)
end

테이블 복사와 참조의 함정

루아 테이블은 참조 타입이므로, 복사할 때 주의해야 할 점들이 많습니다.

얕은 복사와 깊은 복사의 차이를 명확히 이해해야 합니다.

-- 얕은 복사 함수들
local function shallowCopy1(original)
    local copy = {}
    for k, v in pairs(original) do
        copy[k] = v
    end
    return copy
end

-- table.move를 활용한 배열 복사 (Lua 5.3+)
local function copyArray(original)
    return table.move(original, 1, #original, 1, {})
end

-- 깊은 복사 (재귀적)
local function deepCopy(original)
    local copy = {}
    for k, v in pairs(original) do
        if type(v) == "table" then
            copy[k] = deepCopy(v)  -- 재귀적 복사
        else
            copy[k] = v
        end
    end
    return copy
end

-- 순환 참조를 고려한 안전한 깊은 복사
local function safeDeepCopy(original, seen)
    seen = seen or {}
    if seen[original] then
        return seen[original]  -- 순환 참조 감지
    end

    local copy = {}
    seen[original] = copy

    for k, v in pairs(original) do
        if type(v) == "table" then
            copy[safeDeepCopy(k, seen)] = safeDeepCopy(v, seen)
        else
            copy[k] = v
        end
    end

    return copy
end

-- 실제 사용 예제
local original = {
    name = "test",
    nested = {
        value = 42,
        inner = {data = "important"}
    }
}
original.self = original  -- 순환 참조

local safe = safeDeepCopy(original)
print(safe.nested.inner.data)  -- "important"

메타테이블의 심화 활용 - 연산자 오버로딩

메타테이블을 통한 연산자 오버로딩은 루아의 가장 강력한 기능 중 하나입니다.

복잡한 자료구조를 직관적으로 다룰 수 있게 해줍니다.

-- 벡터 클래스 구현
local Vector = {}
Vector.__index = Vector

function Vector.new(x, y, z)
    return setmetatable({x = x or 0, y = y or 0, z = z or 0}, Vector)
end

-- 산술 연산 메타메서드
function Vector:__add(other)
    return Vector.new(self.x + other.x, self.y + other.y, self.z + other.z)
end

function Vector:__sub(other)
    return Vector.new(self.x - other.x, self.y - other.y, self.z - other.z)
end

function Vector:__mul(scalar)
    if type(scalar) == "number" then
        return Vector.new(self.x * scalar, self.y * scalar, self.z * scalar)
    elseif getmetatable(scalar) == Vector then
        -- 내적 계산
        return self.x * scalar.x + self.y * scalar.y + self.z * scalar.z
    end
end

-- 비교 연산 메타메서드
function Vector:__eq(other)
    return self.x == other.x and self.y == other.y and self.z == other.z
end

function Vector:__lt(other)
    return self:magnitude() < other:magnitude()
end

-- 문자열 표현
function Vector:__tostring()
    return string.format("Vector(%.2f, %.2f, %.2f)", self.x, self.y, self.z)
end

-- 헬퍼 메서드
function Vector:magnitude()
    return math.sqrt(self.x^2 + self.y^2 + self.z^2)
end

function Vector:normalize()
    local mag = self:magnitude()
    if mag > 0 then
        return self * (1 / mag)
    end
    return Vector.new(0, 0, 0)
end

-- 사용 예제
local v1 = Vector.new(3, 4, 0)
local v2 = Vector.new(1, 2, 0)

print(v1 + v2)          -- Vector(4.00, 6.00, 0.00)
print(v1 * 2)           -- Vector(6.00, 8.00, 0.00)
print(v1 * v2)          -- 11 (내적)
print(v1 == v2)         -- false
print(v1 > v2)          -- true (크기 비교)

__index와 __newindex를 활용한 고급 패턴

__index__newindex 메타메서드를 조합하면 강력한 추상화 패턴을 구현할 수 있습니다.

-- 속성 검증이 있는 객체
local function createValidatedTable(schema)
    local data = {}
    local mt = {
        __index = function(t, k)
            return data[k]
        end,

        __newindex = function(t, k, v)
            local validator = schema[k]
            if validator then
                if not validator(v) then
                    error(string.format("Invalid value for key '%s': %s", k, tostring(v)))
                end
            end
            data[k] = v
        end,

        __pairs = function(t)
            return pairs(data)
        end
    }

    return setmetatable({}, mt)
end

-- 사용 예제
local person = createValidatedTable({
    name = function(v) return type(v) == "string" and #v > 0 end,
    age = function(v) return type(v) == "number" and v >= 0 and v <= 150 end,
    email = function(v) return type(v) == "string" and v:match("@") end
})

person.name = "김철수"      -- OK
person.age = 30            -- OK
-- person.age = -5         -- 에러!
-- person.email = "invalid" -- 에러!

-- 변화 감지 테이블
local function createObservableTable(onChange)
    local data = {}
    local mt = {
        __index = data,
        __newindex = function(t, k, v)
            local oldValue = data[k]
            data[k] = v
            if onChange then
                onChange(k, oldValue, v)
            end
        end
    }
    return setmetatable({}, mt)
end

-- 사용 예제
local config = createObservableTable(function(key, oldVal, newVal)
    print(string.format("Config changed: %s = %s -> %s", key, oldVal, newVal))
end)

config.debug = true        -- Config changed: debug = nil -> true
config.maxUsers = 100      -- Config changed: maxUsers = nil -> 100
config.debug = false       -- Config changed: debug = true -> false

고성능 캐싱과 메모이제이션 구현

메타테이블을 활용하여 투명한 캐싱 시스템을 구현할 수 있습니다.

-- 약한 참조를 활용한 메모이제이션
local function memoize(func)
    local cache = setmetatable({}, {__mode = "k"})  -- 키가 약한 참조

    return function(...)
        local key = table.concat({...}, "\0")  -- 간단한 키 생성

        if cache[key] == nil then
            cache[key] = func(...)
        end

        return cache[key]
    end
end

-- 복잡한 키를 위한 고급 메모이제이션
local function advancedMemoize(func)
    local cache = {}

    local function serialize(...)
        local args = {...}
        local parts = {}
        for i, v in ipairs(args) do
            if type(v) == "table" then
                parts[i] = tostring(v)  -- 테이블 주소 사용
            else
                parts[i] = tostring(v)
            end
        end
        return table.concat(parts, "|")
    end

    return function(...)
        local key = serialize(...)

        if cache[key] == nil then
            cache[key] = func(...)
        end

        return cache[key]
    end
end

-- 피보나치 예제
local fibonacci = memoize(function(n)
    if n <= 1 then return n end
    return fibonacci(n - 1) + fibonacci(n - 2)
end)

print(fibonacci(40))  -- 첫 호출은 느림
print(fibonacci(40))  -- 두 번째 호출은 즉시 반환

-- LRU 캐시 구현
local function createLRUCache(maxSize)
    local cache = {}
    local order = {}
    local positions = {}

    local function updateOrder(key)
        local pos = positions[key]
        if pos then
            table.remove(order, pos)
            -- 위치 업데이트
            for i = pos, #order do
                positions[order[i]] = i
            end
        end

        table.insert(order, 1, key)
        positions[key] = 1

        -- 위치 재조정
        for i = 2, #order do
            positions[order[i]] = i
        end
    end

    return {
        get = function(key)
            if cache[key] then
                updateOrder(key)
                return cache[key]
            end
        end,

        set = function(key, value)
            if cache[key] then
                cache[key] = value
                updateOrder(key)
            else
                if #order >= maxSize then
                    -- 가장 오래된 항목 제거
                    local oldest = order[#order]
                    cache[oldest] = nil
                    positions[oldest] = nil
                    table.remove(order)
                end

                cache[key] = value
                updateOrder(key)
            end
        end
    }
end

실전 프로젝트 패턴 - 이벤트 시스템과 상태 관리

반응형

복잡한 애플리케이션에서 자주 사용되는 패턴들을 테이블과 메타테이블로 구현해보겠습니다.

-- 타입 안전한 이벤트 시스템
local EventSystem = {}
EventSystem.__index = EventSystem

function EventSystem.new()
    local instance = {
        listeners = {},
        eventTypes = {},
        onceListeners = setmetatable({}, {__mode = "k"})
    }
    return setmetatable(instance, EventSystem)
end

function EventSystem:defineEvent(eventName, schema)
    self.eventTypes[eventName] = schema
end

function EventSystem:on(eventName, callback, options)
    options = options or {}

    if not self.listeners[eventName] then
        self.listeners[eventName] = {}
    end

    local listener = {
        callback = callback,
        priority = options.priority or 0,
        once = options.once or false,
        filter = options.filter
    }

    table.insert(self.listeners[eventName], listener)

    -- 우선순위에 따라 정렬
    table.sort(self.listeners[eventName], function(a, b)
        return a.priority > b.priority
    end)

    return listener  -- 나중에 제거할 때 사용
end

function EventSystem:emit(eventName, eventData)
    -- 타입 검증
    local schema = self.eventTypes[eventName]
    if schema and not schema(eventData) then
        error("Event data validation failed for: " .. eventName)
    end

    local listeners = self.listeners[eventName]
    if not listeners then return end

    -- 리스너 실행
    for i = #listeners, 1, -1 do
        local listener = listeners[i]

        -- 필터 확인
        if not listener.filter or listener.filter(eventData) then
            local success, result = pcall(listener.callback, eventData)

            if not success then
                print("Error in event listener:", result)
            end

            -- once 리스너 제거
            if listener.once then
                table.remove(listeners, i)
            end

            -- 이벤트 전파 중단
            if result == false then
                break
            end
        end
    end
end

-- 상태 머신 구현
local StateMachine = {}
StateMachine.__index = StateMachine

function StateMachine.new(initialState)
    local instance = {
        currentState = initialState,
        states = {},
        transitions = {},
        history = {},
        context = {}
    }
    return setmetatable(instance, StateMachine)
end

function StateMachine:addState(name, config)
    self.states[name] = {
        enter = config.enter,
        exit = config.exit,
        update = config.update,
        timeout = config.timeout
    }
end

function StateMachine:addTransition(from, to, trigger, guard)
    if not self.transitions[from] then
        self.transitions[from] = {}
    end

    self.transitions[from][trigger] = {
        to = to,
        guard = guard
    }
end

function StateMachine:trigger(event, data)
    local current = self.currentState
    local transitions = self.transitions[current]

    if not transitions or not transitions[event] then
        return false  -- 전환 불가
    end

    local transition = transitions[event]

    -- 가드 조건 확인
    if transition.guard and not transition.guard(self.context, data) then
        return false
    end

    -- 상태 전환 실행
    local oldState = self.states[current]
    local newState = self.states[transition.to]

    if oldState and oldState.exit then
        oldState.exit(self.context)
    end

    -- 히스토리 기록
    table.insert(self.history, {
        from = current,
        to = transition.to,
        trigger = event,
        timestamp = os.time(),
        data = data
    })

    self.currentState = transition.to

    if newState and newState.enter then
        newState.enter(self.context, data)
    end

    return true
end

-- 사용 예제
local gameEvents = EventSystem.new()

-- 이벤트 타입 정의
gameEvents:defineEvent("playerMove", function(data)
    return type(data.x) == "number" and type(data.y) == "number"
end)

-- 리스너 등록
gameEvents:on("playerMove", function(data)
    print(string.format("Player moved to (%d, %d)", data.x, data.y))
end, {priority = 10})

gameEvents:on("playerMove", function(data)
    -- 충돌 검사
    if data.x < 0 or data.y < 0 then
        print("Invalid move!")
        return false  -- 이벤트 전파 중단
    end
end, {priority = 20})

-- 이벤트 발생
gameEvents:emit("playerMove", {x = 10, y = 20})

-- 상태 머신 예제
local player = StateMachine.new("idle")

player:addState("idle", {
    enter = function(ctx) print("Player is idle") end
})

player:addState("moving", {
    enter = function(ctx, data) 
        print("Player starts moving to", data.target)
        ctx.target = data.target
    end,
    exit = function(ctx) print("Player stops moving") end
})

player:addTransition("idle", "moving", "move", function(ctx, data)
    return data.target ~= nil
end)

player:addTransition("moving", "idle", "stop")

player:trigger("move", {target = "town"})
player:trigger("stop")

메모리 효율적인 대용량 데이터 처리

대용량 데이터를 다룰 때는 메모리 사용량과 가비지 컬렉션을 신중히 고려해야 합니다.

-- 청크 단위 데이터 처리
local function createChunkedProcessor(chunkSize)
    local buffer = {}
    local processors = {}

    return {
        addProcessor = function(processor)
            table.insert(processors, processor)
        end,

        process = function(data)
            table.insert(buffer, data)

            if #buffer >= chunkSize then
                -- 청크 처리
                for _, processor in ipairs(processors) do
                    processor(buffer)
                end

                -- 버퍼 초기화 (재사용)
                for i = 1, #buffer do
                    buffer[i] = nil
                end
            end
        end,

        flush = function()
            if #buffer > 0 then
                for _, processor in ipairs(processors) do
                    processor(buffer)
                end
                for i = 1, #buffer do
                    buffer[i] = nil
                end
            end
        end
    }
end

-- 객체 풀을 활용한 메모리 재사용
local function createObjectPool(createFn, resetFn, maxSize)
    local pool = {}
    local size = 0

    return {
        acquire = function()
            if size > 0 then
                local obj = pool[size]
                pool[size] = nil
                size = size - 1
                return obj
            else
                return createFn()
            end
        end,

        release = function(obj)
            if size < maxSize then
                if resetFn then resetFn(obj) end
                size = size + 1
                pool[size] = obj
            end
        end,

        size = function() return size end,
        maxSize = function() return maxSize end
    }
end

-- 사용 예제
local vectorPool = createObjectPool(
    function() return {x = 0, y = 0, z = 0} end,  -- 생성
    function(v) v.x, v.y, v.z = 0, 0, 0 end,     -- 초기화
    100  -- 최대 크기
)

-- 대량의 벡터 연산
local function processVectors(count)
    local vectors = {}

    -- 벡터 생성
    for i = 1, count do
        local v = vectorPool:acquire()
        v.x, v.y, v.z = math.random(), math.random(), math.random()
        vectors[i] = v
    end

    -- 처리 로직...

    -- 벡터 반환
    for i = 1, count do
        vectorPool:release(vectors[i])
        vectors[i] = nil
    end
end

디버깅과 성능 분석 도구

실무에서 테이블을 디버깅하고 성능을 분석할 때 유용한 도구들을 만들어보겠습니다.

-- 테이블 구조 분석기
local function analyzeTable(t, name, depth, seen)
    name = name or "table"
    depth = depth or 0
    seen = seen or {}

    if seen[t] then
        return {circular = true, reference = seen[t]}
    end

    seen[t] = name

    local analysis = {
        name = name,
        depth = depth,
        arrayPart = 0,
        hashPart = 0,
        totalSize = 0,
        types = {},
        children = {}
    }

    -- 배열 부분 크기
    analysis.arrayPart = #t

    -- 전체 요소 분석
    for k, v in pairs(t) do
        analysis.totalSize = analysis.totalSize + 1

        local keyType = type(k)
        local valueType = type(v)

        -- 타입 통계
        analysis.types[valueType] = (analysis.types[valueType] or 0) + 1

        -- 해시 부분 계산
        if type(k) ~= "number" or k > analysis.arrayPart or k < 1 then
            analysis.hashPart = analysis.hashPart + 1
        end

        -- 중첩 테이블 재귀 분석
        if valueType == "table" and depth < 5 then  -- 깊이 제한
            analysis.children[k] = analyzeTable(v, name .. "." .. tostring(k), depth + 1, seen)
        end
    end

    return analysis
end

-- 성능 측정 도구
local function benchmark(name, func, iterations)
    iterations = iterations or 1000

    -- GC 정리
    collectgarbage("collect")
    local memBefore = collectgarbage("count")

    local startTime = os.clock()

    for i = 1, iterations do
        func()
    end

    local endTime = os.clock()
    local memAfter = collectgarbage("count")

    return {
        name = name,
        iterations = iterations,
        totalTime = endTime - startTime,
        avgTime = (endTime - startTime) / iterations,
        memoryUsed = memAfter - memBefore
    }
end

-- 테이블 비교 도구
local function deepCompare(t1, t2, path)
    path = path or "root"

    if type(t1) ~= type(t2) then
        return false, path .. ": type mismatch (" .. type(t1) .. " vs " .. type(t2) .. ")"
    end

    if type(t1) ~= "table" then
        if t1 ~= t2 then
            return false, path .. ": value mismatch (" .. tostring(t1) .. " vs " .. tostring(t2) .. ")"
        end
        return true
    end

    -- 키 집합 비교
    local keys1, keys2 = {}, {}
    for k in pairs(t1) do keys1[k] = true end
    for k in pairs(t2) do keys2[k] = true end

    for k in pairs(keys1) do
        if not keys2[k] then
            return false, path .. "." .. tostring(k) .. ": missing in second table"
        end
    end

    for k in pairs(keys2) do
        if not keys1[k] then
            return false, path .. "." .. tostring(k) .. ": extra in second table"
        end
    end

    -- 값 재귀 비교
    for k in pairs(keys1) do
        local success, error = deepCompare(t1[k], t2[k], path .. "." .. tostring(k))
        if not success then
            return false, error
        end
    end

    return true
end

-- 사용 예제
local complexTable = {
    users = {
        {id = 1, name = "김철수", settings = {theme = "dark"}},
        {id = 2, name = "이영희", settings = {theme = "light"}}
    },
    config = {
        debug = true,
        maxConnections = 100
    }
}

local analysis = analyzeTable(complexTable, "complexTable")
print("Array part:", analysis.arrayPart)
print("Hash part:", analysis.hashPart)
print("Total size:", analysis.totalSize)

-- 성능 비교
local results = {}
results[1] = benchmark("ipairs", function()
    local data = {1, 2, 3, 4, 5}
    local sum = 0
    for i, v in ipairs(data) do
        sum = sum + v
    end
end)

results[2] = benchmark("pairs", function()
    local data = {1, 2, 3, 4, 5}
    local sum = 0
    for k, v in pairs(data) do
        if type(v) == "number" then
            sum = sum + v
        end
    end
end)

for _, result in ipairs(results) do
    print(string.format("%s: %.6f ms/iter, %.2f KB memory", 
        result.name, result.avgTime * 1000, result.memoryUsed))
end

실제 프로덕션에서의 테이블 최적화 사례

실제 게임 개발이나 서버 개발에서 마주할 수 있는 구체적인 최적화 사례들을 살펴보겠습니다.

-- 게임 엔티티 시스템 최적화
local EntityManager = {}
EntityManager.__index = EntityManager

function EntityManager.new()
    local instance = {
        entities = {},
        components = {},
        systems = {},
        freeIds = {},
        nextId = 1,

        -- 성능 최적화를 위한 캐시
        queryCache = setmetatable({}, {__mode = "v"}),
        dirtyQueries = {}
    }
    return setmetatable(instance, EntityManager)
end

function EntityManager:createEntity()
    local id
    if #self.freeIds > 0 then
        id = table.remove(self.freeIds)
    else
        id = self.nextId
        self.nextId = self.nextId + 1
    end

    self.entities[id] = {}
    return id
end

function EntityManager:addComponent(entityId, componentType, data)
    if not self.components[componentType] then
        self.components[componentType] = {}
    end

    self.components[componentType][entityId] = data

    -- 쿼리 캐시 무효화
    self:invalidateQueries(componentType)
end

function EntityManager:getEntitiesWithComponents(...)
    local componentTypes = {...}
    table.sort(componentTypes)  -- 캐시 키 일관성
    local cacheKey = table.concat(componentTypes, ",")

    if self.queryCache[cacheKey] and not self.dirtyQueries[cacheKey] then
        return self.queryCache[cacheKey]
    end

    local result = {}

    -- 가장 작은 컴포넌트 집합부터 시작 (최적화)
    local smallestSet = componentTypes[1]
    local smallestSize = math.huge

    for _, componentType in ipairs(componentTypes) do
        local components = self.components[componentType]
        if components then
            local size = 0
            for _ in pairs(components) do size = size + 1 end
            if size < smallestSize then
                smallestSize = size
                smallestSet = componentType
            end
        else
            -- 컴포넌트가 없으면 결과도 빈 집합
            self.queryCache[cacheKey] = {}
            return {}
        end
    end

    -- 교집합 계산
    for entityId in pairs(self.components[smallestSet]) do
        local hasAll = true
        for _, componentType in ipairs(componentTypes) do
            if not self.components[componentType][entityId] then
                hasAll = false
                break
            end
        end

        if hasAll then
            result[entityId] = {}
            for _, componentType in ipairs(componentTypes) do
                result[entityId][componentType] = self.components[componentType][entityId]
            end
        end
    end

    self.queryCache[cacheKey] = result
    self.dirtyQueries[cacheKey] = nil

    return result
end

function EntityManager:invalidateQueries(componentType)
    for query in pairs(self.queryCache) do
        if query:find(componentType) then
            self.dirtyQueries[query] = true
        end
    end
end

-- 대용량 데이터 스트리밍 처리
local function createDataStreamer(batchSize, processorFn)
    local buffer = {}
    local stats = {
        processed = 0,
        errors = 0,
        startTime = os.time()
    }

    return {
        push = function(data)
            table.insert(buffer, data)

            if #buffer >= batchSize then
                local batch = buffer
                buffer = {}  -- 새 버퍼 생성

                -- 비동기적으로 처리 (코루틴 활용)
                local co = coroutine.create(function()
                    local success, error = pcall(processorFn, batch)
                    if success then
                        stats.processed = stats.processed + #batch
                    else
                        stats.errors = stats.errors + 1
                        print("Batch processing error:", error)
                    end
                end)

                coroutine.resume(co)
            end
        end,

        flush = function()
            if #buffer > 0 then
                local success, error = pcall(processorFn, buffer)
                if success then
                    stats.processed = stats.processed + #buffer
                else
                    stats.errors = stats.errors + 1
                    print("Final batch error:", error)
                end
                buffer = {}
            end
        end,

        getStats = function()
            local elapsed = os.time() - stats.startTime
            return {
                processed = stats.processed,
                errors = stats.errors,
                throughput = stats.processed / math.max(elapsed, 1),
                uptime = elapsed
            }
        end
    }
end

테이블 직렬화와 네트워크 통신

728x90

게임이나 분산 시스템에서 테이블을 네트워크를 통해 전송하거나 파일에 저장할 때 필요한 기법들입니다.

-- 효율적인 테이블 직렬화
local function serialize(t, options)
    options = options or {}
    local seen = {}
    local output = {}
    local indent = 0

    local function serializeValue(value, key)
        local valueType = type(value)

        if valueType == "string" then
            return string.format("%q", value)
        elseif valueType == "number" then
            return tostring(value)
        elseif valueType == "boolean" then
            return tostring(value)
        elseif valueType == "table" then
            if seen[value] then
                if options.handleCycles then
                    return "nil --[[circular reference]]"
                else
                    error("Circular reference detected")
                end
            end

            seen[value] = true
            local result = {}
            table.insert(result, "{")

            if options.pretty then
                indent = indent + 1
            end

            -- 배열 부분 먼저
            local arrayPart = {}
            for i = 1, #value do
                local serialized = serializeValue(value[i])
                table.insert(arrayPart, serialized)
            end

            if #arrayPart > 0 then
                if options.pretty then
                    table.insert(result, "\n" .. string.rep("  ", indent) .. table.concat(arrayPart, ",\n" .. string.rep("  ", indent)))
                else
                    table.insert(result, table.concat(arrayPart, ","))
                end
            end

            -- 해시 부분
            local hashPart = {}
            for k, v in pairs(value) do
                if type(k) ~= "number" or k > #value or k < 1 then
                    local keyStr
                    if type(k) == "string" and k:match("^[%a_][%w_]*$") then
                        keyStr = k
                    else
                        keyStr = "[" .. serializeValue(k) .. "]"
                    end

                    local valueStr = serializeValue(v, k)
                    table.insert(hashPart, keyStr .. " = " .. valueStr)
                end
            end

            if #hashPart > 0 then
                if #arrayPart > 0 then
                    if options.pretty then
                        table.insert(result, ",\n" .. string.rep("  ", indent))
                    else
                        table.insert(result, ",")
                    end
                end

                if options.pretty then
                    table.insert(result, "\n" .. string.rep("  ", indent) .. table.concat(hashPart, ",\n" .. string.rep("  ", indent)))
                else
                    table.insert(result, table.concat(hashPart, ","))
                end
            end

            if options.pretty then
                indent = indent - 1
                table.insert(result, "\n" .. string.rep("  ", indent) .. "}")
            else
                table.insert(result, "}")
            end

            seen[value] = nil
            return table.concat(result)
        else
            return "nil"
        end
    end

    return serializeValue(t)
end

-- 바이너리 패킹 (네트워크 전송용)
local function packTable(t)
    local function packValue(value)
        local valueType = type(value)

        if valueType == "nil" then
            return "\0"
        elseif valueType == "boolean" then
            return value and "\1\1" or "\1\0"
        elseif valueType == "number" then
            -- 간단한 정수/실수 구분
            if math.floor(value) == value and value >= -2^31 and value < 2^31 then
                return "\2" .. string.pack("<i4", value)
            else
                return "\3" .. string.pack("<d", value)
            end
        elseif valueType == "string" then
            local len = #value
            if len < 255 then
                return "\4" .. string.char(len) .. value
            else
                return "\5" .. string.pack("<I4", len) .. value
            end
        elseif valueType == "table" then
            local parts = {"\6"}

            -- 배열 부분
            local arrayLen = #value
            table.insert(parts, string.pack("<I4", arrayLen))
            for i = 1, arrayLen do
                table.insert(parts, packValue(value[i]))
            end

            -- 해시 부분
            local hashPairs = {}
            for k, v in pairs(value) do
                if type(k) ~= "number" or k > arrayLen or k < 1 then
                    table.insert(hashPairs, {k, v})
                end
            end

            table.insert(parts, string.pack("<I4", #hashPairs))
            for _, pair in ipairs(hashPairs) do
                table.insert(parts, packValue(pair[1]))
                table.insert(parts, packValue(pair[2]))
            end

            return table.concat(parts)
        end

        return "\0"  -- 지원하지 않는 타입
    end

    return packValue(t)
end

-- 언패킹 함수
local function unpackTable(data)
    local pos = 1

    local function unpackValue()
        if pos > #data then return nil end

        local typeCode = data:byte(pos)
        pos = pos + 1

        if typeCode == 0 then
            return nil
        elseif typeCode == 1 then
            local value = data:byte(pos) == 1
            pos = pos + 1
            return value
        elseif typeCode == 2 then
            local value = string.unpack("<i4", data, pos)
            pos = pos + 4
            return value
        elseif typeCode == 3 then
            local value = string.unpack("<d", data, pos)
            pos = pos + 8
            return value
        elseif typeCode == 4 then
            local len = data:byte(pos)
            pos = pos + 1
            local value = data:sub(pos, pos + len - 1)
            pos = pos + len
            return value
        elseif typeCode == 5 then
            local len = string.unpack("<I4", data, pos)
            pos = pos + 4
            local value = data:sub(pos, pos + len - 1)
            pos = pos + len
            return value
        elseif typeCode == 6 then
            local result = {}

            -- 배열 부분
            local arrayLen = string.unpack("<I4", data, pos)
            pos = pos + 4

            for i = 1, arrayLen do
                result[i] = unpackValue()
            end

            -- 해시 부분
            local hashLen = string.unpack("<I4", data, pos)
            pos = pos + 4

            for i = 1, hashLen do
                local key = unpackValue()
                local value = unpackValue()
                result[key] = value
            end

            return result
        end

        return nil
    end

    return unpackValue()
end

-- 사용 예제
local testData = {
    name = "Player1",
    level = 42,
    inventory = {"sword", "potion", "key"},
    stats = {health = 100, mana = 50}
}

-- 텍스트 직렬화
local serialized = serialize(testData, {pretty = true})
print("Serialized size:", #serialized, "bytes")

-- 바이너리 패킹
local packed = packTable(testData)
print("Packed size:", #packed, "bytes")

-- 압축률 비교
print("Compression ratio:", #packed / #serialized)

local unpacked = unpackTable(packed)
local success, error = deepCompare(testData, unpacked)
print("Data integrity:", success and "OK" or error)

결론과 다음 단계

루아 테이블은 단순해 보이지만 실제로는 매우 깊이 있는 주제입니다.

이 글에서 다룬 내용들을 정리하면:

 

핵심 개념들:

  • 테이블의 내부 구조 (배열 부분 vs 해시 부분)
  • 메타테이블과 메타메서드의 강력함
  • 성능 최적화와 메모리 관리
  • 실전 패턴과 고급 활용법

실무에서 주의할 점들:

  • # 연산자의 한계와 대안
  • 순환 참조 처리
  • 메모리 누수 방지
  • 적절한 자료구조 선택

루아 테이블을 마스터하면 단순한 스크립팅을 넘어서 복잡한 시스템 아키텍처까지 설계할 수 있게 됩니다.

게임 엔진의 컴포넌트 시스템, 웹 서버의 라우팅 시스템, 데이터 분석 도구까지 - 모든 것이 테이블 위에서 구현 가능합니다.


이전 글: 루아 입문 시리즈 #2: 루아(Lua) 함수와 클로저 – 함수형 프로그래밍 맛보기

728x90
반응형