언어 — 가시성 규칙

언어 — 가시성 규칙 (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은 블록의 마지막 문장이어야 해요.

모범 사례

  • 많은 언어에서 그렇듯, 지역 변수를 최대한 사용하고 전역 변수 사용을 줄이는 게 좋아요 — 전역 변수는 이름 충돌과 뜻밖의 공유를 일으킬 수 있어요.
  • 변수를 블록 안에서만 필요하면 그 블록 안에서 선언해 범위를 최소화하세요.
  • 클로저를 쓸 때 업밸류 캡처의 동작(값이 아니라 변수를 공유)을 이해하는 게 중요해요.

출처: 가시성 규칙 (Visibility Rules)

본문

스코프와 업밸류의 상호작용

업밸류는 지역 변수의 스코프가 끝난 뒤에도, 그 변수를 포획한 클로저가 살아있는 한 값이 아닌 변수 자체로 계속 유지돼요. 따라서 클로저가 살아있는 동안 업밸류 변수는 가비지 컬렉션 되지 않아요. 이는 "변수를 공유하는" 클로저의 특성을 낳아요 — 같은 반복 루프에서 생긴 클로저들이 공통 업밸류를 공유하는 문제(고전적 버그)가 생길 수 있어요.

-- 고전적 함정: 루프 변수를 클로저가 공유
local funcs = {}
for i = 1, 3 do
  funcs[i] = function() return i end
end
print(funcs[1]())  --> 3  (모두 같은 i 업밸류 참조)

이를 피하려면 루프 본문에서 지역 변수로 i를 복사해 각각 다른 업밸류로 만들어야 해요.

더 알아보기