루아 (Lua) 프로그래밍: 테이블과 메타테이블의 모든 것

1 min read

Lua 프로그래밍의 여정에서 가장 중요하고 흥미로운 지점에 도달했습니다. 바로 Lua 언어의 심장이자 가장 중심적인 기능인 테이블(Table)입니다. Lua에는 배열, 딕셔너리, 리스트, 객체 등을 위한 별도의 데이터 타입이 없습니다. 이 모든 것을 단 하나의 강력하고 유연한 구조, 바로 테이블로 표현합니다.

테이블을 이해하는 것은 Lua를 이해하는 것과 같습니다. 이 글에서는 테이블을 생성하고 활용하는 다양한 방법부터, 테이블의 기본 동작을 마법처럼 확장하고 재정의할 수 있게 해주는 메타테이블(Metatable)메타메소드(Metamethod)의 개념까지 깊이 있게 탐구합니다. 이 개념들을 마스터하면 Lua의 진정한 힘을 발휘할 수 있게 될 것입니다.

테이블이란 무엇인가? 만능 데이터 컨테이너

Lua의 테이블(Table)은 본질적으로 연관 배열(Associative Array)입니다. 이는 정수 인덱스뿐만 아니라 문자열, 불리언 등 nil을 제외한 거의 모든 값을 ‘키(key)’로 사용하여 데이터를 저장하고 검색할 수 있는 매우 유연한 데이터 구조를 의미합니다.

  • 유일한 복합 데이터 타입: 배열, 딕셔너리, 객체 등 다른 언어의 다양한 데이터 구조를 테이블 하나로 모두 구현합니다.
  • 동적 크기: 생성 후에도 크기가 고정되지 않고, 언제든지 요소를 추가하거나 삭제할 수 있습니다.
  • 참조 타입: 테이블 변수는 데이터 덩어리 자체가 아니라, 메모리에 있는 테이블 객체를 가리키는 ‘주소’ 또는 ‘참조(reference)’를 저장합니다.
local t1 = {} -- 빈 테이블 생성
local t2 = t1 -- t2는 t1과 같은 테이블 객체를 가리키는 참조를 복사

t1.name = "Alice"
print(t2.name) -- 출력: Alice (같은 객체를 가리키므로 t1의 변경이 t2에 반영됨)

local t3 = {} -- 이것은 완전히 새로운 빈 테이블
print(t1 == t2) -- 출력: true (같은 객체를 참조함)
print(t1 == t3) -- 출력: false (서로 다른 객체임)

테이블 생성과 사용법

테이블은 중괄호 {}를 사용하여 생성하며, 생성과 동시에 초기화할 수 있습니다.

1. 배열(리스트) 스타일
정수 인덱스 1, 2, 3… 에 순차적으로 값이 저장됩니다. Lua의 배열 인덱스는 관례적으로 1부터 시작합니다.

local numbers = {10, 20, 30, 40} -- numbers[1]은 10, numbers[2]는 20

2. 딕셔너리(맵) 스타일
문자열 키와 값을 =로 연결하여 저장합니다.

local person = { name = "Alice", age = 30, city = "Seoul" }

3. 명시적 키 스타일 (가장 유연함)
대괄호 [] 안에 키를 직접 지정합니다. 어떤 타입의 키도 사용할 수 있습니다.

local data = {
  [1] = "First",
  ["type"] = "Example",
  [true] = false,
  ["complex-key!"] = 100
}

테이블 요소 접근, 수정, 삭제

  • 점 표기법 (table.key): 키가 유효한 식별자 규칙을 따르는 문자열일 때 사용합니다. 가독성이 좋습니다.
  • 대괄호 표기법 (table[key]): 어떤 타입의 키에도 사용할 수 있는 범용적인 방법입니다.
local book = { title = "Lua Programming", ["main-author"] = "Roberto" }

print(book.title) -- 점 표기법으로 읽기
book.year = 2024  -- 점 표기법으로 새 요소 추가

print(book["main-author"]) -- 대괄호 표기법으로 읽기
book[1] = "Chapter 1"    -- 대괄호 표기법으로 새 요소 추가

테이블에 존재하지 않는 키로 접근하면 오류 대신 nil을 반환합니다. 요소를 삭제하려면 해당 키에 nil을 할당하면 됩니다.

print(book.publisher) -- 출력: nil (publisher 키가 없음)
book.year = nil       -- year 요소 삭제

테이블 순회하기: ipairspairs

테이블의 모든 요소를 반복적으로 처리하려면 for 루프와 이터레이터 함수를 사용합니다.

  • ipairs(table): 테이블의 배열 부분을 인덱스 1부터 순서대로 순회합니다. 중간에 인덱스가 끊기면 거기서 멈춥니다.
  • pairs(table): 테이블의 모든 키-값 쌍을 순회합니다. 순회 순서는 보장되지 않습니다.
local mixed = { "a", "b", name = "Test", "c" }

print("--- ipairs (배열 부분, 순서 보장) ---")
for index, value in ipairs(mixed) do
    print(index, value)
end
-- 출력:
-- 1  a
-- 2  b
-- 3  c

print("--- pairs (전체, 순서 미보장) ---")
for key, value in pairs(mixed) do
    print(key, value)
end
-- 출력 예시 (순서는 바뀔 수 있음):
-- 1  a
-- 2  b
-- 3  c
-- name Test

어떤 순회 방법을 쓸지는 테이블을 배열로 쓰는지, 딕셔너리로 쓰는지에 따라 결정해야 합니다.

테이블의 숨겨진 힘: 메타테이블(Metatable)

일반적으로 테이블에 정의되지 않은 연산(예: 두 테이블을 더하는 것)을 시도하면 오류가 발생합니다. 하지만 메타테이블을 사용하면 이러한 연산의 동작을 우리가 직접 정의할 수 있습니다.

메타테이블은 다른 테이블의 ‘행동 규칙’을 담고 있는 특별한 테이블입니다. 어떤 테이블에 특정 연산이 수행될 때, Lua는 먼저 해당 연산에 대응하는 규칙(이를 메타메소드라고 함)이 메타테이블에 있는지 확인하고, 있다면 그 규칙을 실행합니다.

메타테이블 설정하기

setmetatable(테이블, 메타테이블) 함수로 특정 테이블에 메타테이블을 연결할 수 있습니다.

local mytable = {}
local mymeta = { message = "This is a metatable" }

setmetatable(mytable, mymeta) -- mytable에 mymeta를 메타테이블로 설정

핵심 메타메소드: 테이블의 동작을 바꾸는 마법

메타테이블 안에 특정 이름(항상 __로 시작)의 함수를 정의하면, 원래 테이블의 동작을 바꿀 수 있습니다.

__index: 존재하지 않는 값 찾아내기

가장 중요하고 널리 사용되는 메타메소드입니다. 테이블에 존재하지 않는 키로 값을 읽으려고 할 때 호출됩니다.

사용법 1: 함수로 기본값 제공하기
__index에 함수를 설정하면, (테이블, 키)를 인자로 받아 실행됩니다.

local defaults = { x = 0, y = 0, z = 0 }
local mt = {
  __index = function(table, key)
    print("메타테이블에서 '"..key.."'를 찾는 중...")
    return defaults[key] -- 기본값 테이블에서 값을 찾아 반환
  end
}

local point = setmetatable({x = 10}, mt)
print(point.x) -- 출력: 10 (point 테이블 자체에 있음)
print(point.y) -- 출력: 메타테이블에서 'y'를 찾는 중... / 0

사용법 2: 테이블로 프로토타입 연결하기 (가장 중요!)
__index에 다른 테이블을 설정하면, 원래 테이블에 키가 없을 때 Lua는 그 다른 테이블에서 키를 다시 찾아봅니다. 이것이 바로 Lua에서 상속(Inheritance)을 구현하는 핵심 원리입니다.

local Shape_prototype = { type = "도형" }

function Shape_prototype:printType()
  print("이것은 " .. self.type .. "입니다.")
end

local mt = { __index = Shape_prototype }

local circle = setmetatable({ radius = 5 }, mt)
local square = setmetatable({ side = 4 }, mt)

print(circle.radius) -- 출력: 5 (circle 자체에 있음)
print(circle.type)   -- 출력: 도형 (circle에 없으므로 Shape_prototype에서 찾음)
circle:printType()   -- 출력: 이것은 도형입니다.

__newindex: 존재하지 않는 곳에 값 쓰기

__index의 반대 개념으로, 테이블에 존재하지 않는 키에 값을 쓰려고 할 때 호출됩니다. 값의 유효성을 검사하거나 읽기 전용 테이블을 만드는 데 유용합니다.

local readonly_mt = {
  __newindex = function(table, key, value)
    error("이 테이블은 읽기 전용입니다. '" .. key .. "'에 쓸 수 없습니다.")
  end
}

local config = setmetatable({ version = "1.0" }, readonly_mt)
print(config.version) -- 출력: 1.0
-- config.version = "2.0" -- 오류 발생!

그 외 유용한 메타메소드

  • 산술 연산: __add(덧셈), __mul(곱셈) 등을 정의하여 테이블 간의 연산을 가능하게 합니다.
  • __tostring: print(테이블)이나 tostring(테이블) 호출 시 테이블을 어떻게 문자열로 표현할지 정의합니다. 디버깅에 매우 유용합니다.
  • __call: 테이블을 함수처럼 테이블() 형태로 호출할 때 실행될 동작을 정의합니다.
  • __len: 길이 연산자 #테이블의 동작을 재정의합니다.
  • __eq: == 연산자로 두 테이블의 내용을 비교하는 방법을 정의합니다.
-- 벡터 덧셈과 출력을 위한 메타테이블
local Vector_mt = {
  __add = function(v1, v2)
    return setmetatable({x = v1.x + v2.x, y = v1.y + v2.y}, Vector_mt)
  end,
  __tostring = function(v)
    return string.format("Vector(x=%g, y=%g)", v.x, v.y)
  end
}

local vec1 = setmetatable({x=1, y=2}, Vector_mt)
local vec2 = setmetatable({x=3, y=4}, Vector_mt)
local vec3 = vec1 + vec2 -- __add 메타메소드 호출

print(vec3) -- 출력: Vector(x=4, y=6) (__tostring 덕분)

이 글에서는 Lua의 심장인 테이블과 그 잠재력을 극대화하는 메타테이블에 대해 알아보았습니다.

  • 테이블은 배열, 딕셔너리, 객체 등 모든 것을 표현하는 Lua의 유일무이한 만능 데이터 구조입니다.
  • ipairspairs를 사용하여 테이블의 내용을 효과적으로 순회할 수 있습니다.
  • 메타테이블은 테이블의 기본 동작을 재정의하는 ‘규칙집’과 같습니다.
  • __index__newindex는 존재하지 않는 키에 대한 접근을 제어하며, 특히 __index는 Lua에서 객체지향 프로그래밍의 상속을 구현하는 핵심 열쇠입니다.

테이블과 메타테이블에 대한 깊은 이해는 여러분을 초보자에서 숙련된 Lua 개발자로 이끄는 가장 중요한 발판입니다.

루아 Lua 프로그래밍 : 모듈과 패키지 가이드

지금까지 우리는 함수로 코드를 묶고, 테이블로 데이터를 구조화하는 방법을 익혔습니다. 하지만 프로젝트의 규모가 커지기 시작하면, 모든 코드를 단 하나의 파일에 담는 것은 금세 한계에...
eve
53 sec read

루아(Lua) 프로그래밍: 제어 구조 조건과 반복

지금까지 우리는 변수에 데이터를 저장하고, 연산자로 이 데이터들을 계산하고 비교하는 방법을 배웠습니다. 하지만 프로그램이 단순히 위에서 아래로 순서대로만 실행된다면, 매우 단순한 작업밖에 할 수...
eve
1 min read

Lua 프로그래밍: 연산자와 표현식

이전 글에서 데이터에 이름을 붙여 변수에 저장하고, Lua가 데이터를 어떤 종류로 다루는지 알아보았습니다. 하지만 데이터는 그 자체로 두면 아무 일도 하지 않습니다. 이 데이터에...
eve
1 min read