언어 — 가시성 규칙
언어 — 가시성 규칙 (The Language — Visibility Rules)
루아는 어휘적 스코핑(lexical scoping)을 사용해요. 즉, 지역 변수는 프로그램의 구조(코드의 물리적 위치)에 따라 결정되는 범위(scope)를 가져요. 지역 변수의 범위는 그 변수가 선언된 블록의 끝까지예요. 블록 안에서 선언된 지역 변수는 그 블록 안에서만 접근할 수 있어요. 전역 변수는 전역 테이블을 통해 어디서든 접근할 수 있고, 지역 변수는 선언된 블록 안에서만 보여요.
지역 변수의 범위 (Scope of local variables)
지역 변수의 스코프는 다음 규칙을 따라요.
- 지역 변수는 선언된 지점부터 그 블록의 끝까지 범위를 가져요.
- 블록은
do ... end블록, 함수 본문, 제어 구조(if/while/for/repeat)의 본문 등으로 도입돼요. - 지역 변수의 범위는 "선언 이후"부터 시작되므로, 선언 앞에서는 그 변수를 참조할 수 없어요.
local x = 10
print(x) --> 10
do
local x = 20 -- 내부 지역 변수 (바깥 x를 가림)
print(x) --> 20
end
print(x) --> 10 (바깥 x는 그대로)
그림자 규칙 (Shadowing)
안쪽 블록에서 같은 이름의 지역 변수를 선언하면 바깥 변수를 가립니다(shadow). 그 스코프 안에서는 안쪽 변수가 바깥 변수를 대신해요. 블록을 벗어나면 바깥 변수가 다시 보여요.
local a = 1
do
local a = 2
print(a) --> 2
end
print(a) --> 1
지역 변수의 초기화와 자기 참조
지역 변수를 선언하면서 초기화 표현식에서 그 변수 이름을 쓰면, 그 이름은 (초기화가 끝나기 전에) 바깥 범위의 같은 이름(있다면)을 가리켜요. 즉, 초기화 중 자신은 아직 자기 자신이 아니에요.
local x = 5
local x = x + 1 -- 우변의 x는 바깥 x(5)를 읽음 → x=6
이는 흔한 "자기 참조를 통한 누적" 관용구를 만들었지만, 의도하지 않으면 버그의 원인이 되기도 해요.
전역 변수의 범위
전역 변수는 범위 제한이 없어요. 어디서든 _ENV 테이블을 통해 접근할 수 있어요. 단, 전역 변수 이름이 지역 변수로 가려질 수는 있어요.
global_var = 1
function f()
local global_var = 99 -- 지역 변수가 전역을 가림
print(global_var) --> 99
end
f()
print(global_var) --> 1 (전역은 그대로)
제어 구조에서의 스코프
제어 구조는 각자 지역 변수 스코프를 도입해요.
- for 루프의 제어 변수는 for 문 스코프 안에서만 유효한 지역 변수예요.
- for 루프 본문에서 선언된 변수도 루프 안에서만 유효해요.
repeat루프에서until조건은 루프 본문의 지역 변수를 볼 수 있어요.
for i = 1, 3 do
print(i) -- i는 루프 안에서 유효
end
print(i) -- i는 루프 밖에서 nil (또는 바깥 i)
클로저와 업밸류 (Closures and upvalues)
함수가 다른 함수 안에서 정의되면, 안쪽 함수는 바깥 함수의 지역 변수를 "포획(capture)"해서 업밸류(upvalue) 로 사용할 수 있어요. 업밸류는 안쪽 함수가 호출될 때마다 재생성되는 것이 아니라, 바깥 함수가 실행되는 동안 그 인스턴스의 지역 변수를 공유해요.
function makeCounter()
local count = 0 -- 업밸류가 될 변수
return function()
count = count + 1
return count
end
end
local c1 = makeCounter()
print(c1()) --> 1
print(c1()) --> 2
local c2 = makeCounter()
print(c2()) --> 1 (독립된 count)
이런 함수를 클로저(closure) 라고 하고, 포획된 변수 하나하나를 업밸류(upvalue) 라고 해요. 업밸류를 사용하면 함수가 자신이 정의된 환경의 상태를 기억해서, 상태 있는 함수(stateful function)나 반복자, 콜백 등을 우아하게 만들 수 있어요.
가시성 규칙 문법
블록(block)은 문장들의 나열이고, 지역 변수는 블록 안에서 선언돼요. 블록 문법과 범위 규칙에 따라 "선언 지점 이후 ~ 블록 끝"까지가 그 지역 변수의 범위예요.
block ::= {stat} [retstat]
지역 변수 선언이 블록의 끝에 오지 못하게 하는 제약은 없지만, return은 블록의 마지막 문장이어야 해요.
모범 사례
- 많은 언어에서 그렇듯, 지역 변수를 최대한 사용하고 전역 변수 사용을 줄이는 게 좋아요 — 전역 변수는 이름 충돌과 뜻밖의 공유를 일으킬 수 있어요.
- 변수를 블록 안에서만 필요하면 그 블록 안에서 선언해 범위를 최소화하세요.
- 클로저를 쓸 때 업밸류 캡처의 동작(값이 아니라 변수를 공유)을 이해하는 게 중요해요.
본문
스코프와 업밸류의 상호작용
업밸류는 지역 변수의 스코프가 끝난 뒤에도, 그 변수를 포획한 클로저가 살아있는 한 값이 아닌 변수 자체로 계속 유지돼요. 따라서 클로저가 살아있는 동안 업밸류 변수는 가비지 컬렉션 되지 않아요. 이는 "변수를 공유하는" 클로저의 특성을 낳아요 — 같은 반복 루프에서 생긴 클로저들이 공통 업밸류를 공유하는 문제(고전적 버그)가 생길 수 있어요.
-- 고전적 함정: 루프 변수를 클로저가 공유
local funcs = {}
for i = 1, 3 do
funcs[i] = function() return i end
end
print(funcs[1]()) --> 3 (모두 같은 i 업밸류 참조)
이를 피하려면 루프 본문에서 지역 변수로 i를 복사해 각각 다른 업밸류로 만들어야 해요.