루아(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
테이블 직렬화와 네트워크 통신
게임이나 분산 시스템에서 테이블을 네트워크를 통해 전송하거나 파일에 저장할 때 필요한 기법들입니다.
-- 효율적인 테이블 직렬화
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 해시 부분)
- 메타테이블과 메타메서드의 강력함
- 성능 최적화와 메모리 관리
- 실전 패턴과 고급 활용법
실무에서 주의할 점들:
#
연산자의 한계와 대안- 순환 참조 처리
- 메모리 누수 방지
- 적절한 자료구조 선택
루아 테이블을 마스터하면 단순한 스크립팅을 넘어서 복잡한 시스템 아키텍처까지 설계할 수 있게 됩니다.
게임 엔진의 컴포넌트 시스템, 웹 서버의 라우팅 시스템, 데이터 분석 도구까지 - 모든 것이 테이블 위에서 구현 가능합니다.
'프로그래밍 언어 실전 가이드' 카테고리의 다른 글
루아 입문 시리즈 #6: 루아와 C 연동 프로그래밍 (0) | 2025.06.13 |
---|---|
루아 입문 시리즈 #5: 루아 에러 처리와 디버깅 완벽 가이드 - 안정적인 Lua 애플리케이션 개발을 위한 실전 기법 (0) | 2025.06.13 |
루아 입문 시리즈 #4: 루아 모듈과 패키지 시스템 완벽 가이드 (0) | 2025.06.11 |
루아 입문 시리즈 #2: 루아(Lua) 함수와 클로저 – 함수형 프로그래밍 맛보기 (0) | 2025.05.15 |
루아 입문 시리즈 #1: 루아(Lua) 프로그래밍 언어 문법 기초: 초보자를 위한 완벽 가이드 (0) | 2025.05.15 |