메타테이블과 메타메서드
메타테이블과 메타메서드 (Metatables and Metamethods)
모든 값은 자신의 "메타테이블(metatable)"과 연관될 수 있는데, nil만 예외예요. 메타테이블은 보통의 테이블로, 그 필드가 "메타이벤트(metaevent)"라고 불리는 특정 동작을 정의해요. 어떤 값에 특정 메타이벤트가 정의돼 있으면, 그 값에 해당 연산을 적용했을 때 루아는 자동으로 그 메타이벤트를 호출해요.
메타테이블의 각 메타이벤트는 키-값 쌍으로 저장돼요.
- 메타이벤트는 키가 함수인 것과 키가 다른 값인 것 두 종류가 있어요.
- 함수 메타이벤트는 보통 "메타메서드(metamethod)"라고 불러요. 특정 연산이 수행될 때 호출되는 함수예요.
- 함수가 아닌 메타이벤트(예:
__metatable)는 값을 대신 저장해, 다른 용도로 쓰여요.
메타테이블을 조작하는 기본 함수는 getmetatable과 setmetatable이에요. setmetatable(t, mt)는 테이블 t의 메타테이블을 mt로 바꿔요. 메타테이블을 nil로 설정하면 그 테이블의 메타테이블을 제거해요. 반대로 getmetatable(t)는 테이블 t의 메타테이블을 반환해요.
local t = {}
local mt = {}
setmetatable(t, mt)
print(getmetatable(t)) --> table: 0x... (메타테이블 mt)
데이터 타입마다 기본 메타테이블이 하나씩 있을 수 있어요. 예를 들어 대상 언어에 기본 메타테이블이 있으면, 그 데이터 타입의 모든 값이 그 메타테이블을 공유해요. 문자열은 기본 메타테이블을 가져서 #(길이) 연산자나 ..(연결) 연산자 같은 동작을 정의해요.
메타테이블의 표준 메타이벤트는 다음과 같아요 (적용 대상과 동작 설명):
__add,__sub,__mul,__div,__idiv,__mod,__pow,__unm,__band,__bor,__bxor,__bnot,__shl,__shr— 산술·비트 연산자 메타메서드.a + b처럼 피연산자 중 하나에 정의돼 있으면 그 함수가 호출돼요.__concat—..연결 연산자 메타메서드.__len—#길이 연산자 메타메서드.__eq,__lt,__le— 비교 연산자 메타메서드.==,<,<=에 정의돼요.__index— 테이블 필드 접근t[k]에서 해당 키가 없을 때 호출되는 메타메서드. 함수뿐 아니라 테이블일 수도 있는데, 테이블이면 그 테이블에서 다시 조회돼요. (상속 구현에 핵심)__newindex— 테이블 필드 할당t[k] = v에서 해당 키가 없을 때 호출되는 메타메서드. 역시 함수 또는 테이블일 수 있어요.__call— 테이블을 함수처럼 호출t(...)할 때 호출되는 메타메서드.__close— to-be-closed 변수가 블록을 벗어날 때 호출되는 메타메서드 (3.3.8 참조).__tostring—tostring(t)이 호출될 때 대상의 문자열 표현을 만드는 메타메서드.__metatable—getmetatable이 반환할 대상을 바꾸거나 접근을 막는 메타메서드. 값이 문자열이나 다른 값이면getmetatable은 그 값을 그대로 반환하고setmetatable은 오류를 내요.
프로토콜 규칙 — 메타이벤트가 값에 정의돼 있는지 확인하려면, 본연의 (코어) 값 연산을 먼저 시도해요. 예를 들어 __index 메타메서드는 실제 키 조회가 실패했을 때만 호출돼요. __metatable은 값이 접근 통제를 위해 다른 값을 가지는 특수 케이스예요.
__index와 __newindex — 객체 지향 상속이나 필드 위임(field delegation)을 구현하는 가장 강력한 수단이에요. __index가 테이블이면 필드 조회가 실패했을 때 그 테이블에서 다시 조회돼요. 함수면 실패한 키를 인자로 호출돼요. __newindex도 동일하게 테이블 또는 함수일 수 있어요. __newindex가 테이블이면, 새 값을 넣으려는 시도는 그 테이블에서 수행돼요.
local parent = { greeting = "안녕" }
local child = setmetatable({}, { __index = parent })
print(child.greeting) --> 안녕 (자식에 없으니 부모에서 조회)
본문
산술 연산자 메타메서드
a + b, a - b, a * b, a / b, a // b(정수 나눗셈), a % b, a ^ b, -a(단항 부정 __unm), 그리고 비트 연산 a & b, a | b, a ~ b, ~a(비트 NOT __bnot), 시프트 a << b, a >> b는 각각 __add, __sub, __mul, __div, __idiv, __mod, __pow, __unm, __band, __bor, __bxor, __bnot, __shl, __shr 메타메서드에 대응해요. 인자 중 하나에 해당 메타메서드가 정의돼 있으면 호출돼요. 시도는 왼쪽부터, 그다음 오른쪽 피연산자 순으로 이뤄져요. 동작이 정의되지 않았으면 오류가 발생해요.
연결 연산자 (__concat)
a .. b는 __concat 메타메서드에 대응해요. 역시 왼쪽 피연산자부터, 그다음 오른쪽 순으로 시도해요. 메타메서드가 없으면 오류가 발생해요.
길이 연산자 (__len)
# 연산자는 __len 메타메서드에 대응해요. 문자열의 길이는 항상 그 바이트 수로 정의되고, 테이블의 길이는 명시된 대로 지역적 규칙을 따르며, 나머지 값에 #을 적용하면 __len이 호출돼요. 메타메서드가 없으면 오류가 발생해요.
비교 연산자 (__eq, __lt, __le)
==, <, <= 연산자는 각각 __eq, __lt, __le 메타메서드에 대응해요. __eq는 두 피연산자가 모두 같은 기본 타입이고 (table, function, userdata, thread) 어느 한쪽에 __eq가 정의돼 있을 때 호출돼요. __lt와 __le는 피연산자가 같은 타입이 아닐 수도 있고, a < b에서 __lt(a, b)가 호출돼요. a <= b에는 __le가 먼저 호출되고, 없으면 __lt를 뒤집어(not b < a) 계산하기도 해요.
인덱스와 새 인덱스 (__index, __newindex)
__index는 테이블에서 실제 키 조회가 실패했을 때 호출돼요. __newindex는 테이블에서 실제 키 할당이 "없는 키"에 대해 이뤄질 때 호출돼요. __index/__newindex가 함수면 키를 인자로 호출되고, 테이블이면 그 테이블에서 조회/할당이 진행돼요. 이 둘은 상속, 위임, 기본값, 객체지향 등에 쓰이는 핵심 메타이벤트예요.
함수처럼 호출 (__call)
테이블에 __call이 있으면 그 테이블을 함수처럼 호출할 수 있어요. t(args)는 __call을 호출하고, 첫 번째 인자는 테이블 자신(t)이에요.
문자열 변환 (__tostring)
tostring(v)는 v에 __tostring 메타메서드가 있으면 그것을 호출해서 문자열로 바꿔요. 없으면 표준 문자열 표현(예: table: 0x...)을 만들어요.
메타테이블 보호 (__metatable)
메타테이블 필드 __metatable에 값이 있으면, getmetatable은 이 값을 반환하고 setmetatable은 오류를 발생시켜요. 이를 통해 메타테이블을 읽거나 바꾸는 것을 제한할 수 있어요.
닫기 (__close)
to-be-closed 변수가 되어 있는 값에 __close 메타메서드가 있으면, 변수가 스코프를 벗어날 때 루아가 이 메서드를 호출해요. 자원 정리(파일 닫기, 잠금 해제 등)에 쓰여요.