새해 복 많이 받으세요! Ruby Magic에 다시 오신 것을 환영합니다. 이번 겨울 에피소드에서는 바인딩(binding)과 스코프(scope)를 깊이 파헤쳐 보겠습니다. 스키를 신고 저희와 함께 숲속 깊은 곳으로 떠나보시죠.
지난 호에서는 블록, 프록(proc), 람다(lambda)를 비교하며 Ruby의 클로저(closure)를 살펴봤습니다. 세 가지 타입의 차이점 외에도 클로저를 정의하는 핵심 요소가 무엇인지 함께 다뤘습니다.
클로저는 환경(environment)을 가진 일급 함수(first-class function)입니다. 여기서 환경이란 클로저가 생성된 시점에 존재하던 변수들에 대한 매핑을 의미합니다. 클로저는 해당 변수들이 다른 스코프에 정의되어 있더라도 계속해서 접근할 수 있습니다.
지난 호에서는 Ruby의 일급 함수에 해당하는 개념들을 살펴봤지만, 환경 부분은 편의상 건너뛰었습니다. 이번 호에서는 Ruby가 바인딩을 통해 어휘 스코프(lexical scope)를 어떻게 처리하는지 살펴보면서, 그 환경이 클로저, 클래스, 클래스 인스턴스에서 어떻게 동작하는지 알아보겠습니다.
어휘 스코프(Lexical Scope)
프로그래밍에서 스코프(scope)란 코드의 특정 지점에서 사용할 수 있는 바인딩을 의미합니다. 바인딩, 즉 이름 바인딩(name binding)은 이름을 메모리 참조에 연결합니다. 예를 들어 변수의 이름을 그 값에 연결하는 것이죠. 스코프는 self가 무엇을 의미하는지, 어떤 메서드를 호출할 수 있는지, 어떤 변수를 사용할 수 있는지를 결정합니다.
Ruby는 대부분의 현대 프로그래밍 언어처럼 정적 스코프(static scope), 흔히 말하는 어휘 스코프(lexical scope)(동적 스코프와 반대되는 개념)를 사용합니다. 현재 스코프는 코드의 구조를 기반으로 하며, 코드의 특정 지점에서 어떤 변수를 사용할 수 있는지 결정합니다. 즉, 메서드, 블록, 클래스 사이를 이동할 때마다 스코프가 변경됩니다. 각각 서로 다른 지역 변수를 가질 수 있기 때문입니다.
def bar
foo = 1
foo
end
bar # => 1
이 메서드 안에서는 지역 변수(local variable)를 생성하고 콘솔에 출력합니다. 이 변수는 메서드 내부에서 생성되었기 때문에 메서드 내부에서 스코프 내(in scope)에 속합니다.
foo = 1
def bar
foo
end
bar # => NameError (undefined local variable or method `foo' for main:Object)
이번 예제에서는 변수를 메서드 밖에서 생성했습니다. 메서드 내부에서 이 변수를 호출하면 에러가 발생하는데, 변수가 스코프 밖(out of scope)에 있기 때문입니다. 지역 변수는 스코프가 엄격하게 제한되어 있어서, 인자로 전달하지 않는 한 메서드는 자기 자신 외부의 변수에 접근할 수 없습니다.
@foo = 1
def bar
@foo
end
bar # => 1
지역 변수는 로컬 범위에서만 사용할 수 있는 반면, 인스턴스 변수(instance variable)는 클래스 인스턴스의 모든 메서드에서 사용할 수 있습니다.
상속된 스코프와 프록 바인딩
앞선 예제에서 확인했듯이, 스코프는 코드상의 위치를 기반으로 합니다. 메서드 외부에서 정의한 지역 변수는 메서드 내부에서 스코프에 포함되지 않지만, 인스턴스 변수로 변환하면 사용할 수 있게 됩니다. 메서드가 외부의 지역 변수에 접근할 수 없는 이유는 메서드가 자체적인 스코프, 즉 자체적인 바인딩을 가지고 있기 때문입니다.
프록(확장 개념으로 블록과 람다 포함)은 조금 다르게 동작합니다. 프록이 인스턴스화될 때마다 새로운 바인딩이 생성되는데, 이 바인딩은 블록이 생성된 컨텍스트의 지역 변수들에 대한 참조를 상속받습니다.
foo = 1
Proc.new { foo }.call # => 1
이 예제에서는 foo라는 변수에 1을 할당했습니다. 내부적으로 두 번째 줄에서 생성된 Proc 객체는 새로운 바인딩을 만듭니다. 프록을 호출하면 해당 변수의 값을 요청할 수 있습니다.
바인딩은 프록이 초기화되는 시점에 생성되기 때문에, 블록이 변수 정의 이후에 호출된다 해도 변수를 정의하기 전에 프록을 만들 수는 없습니다.
proc = Proc.new { foo }
foo = 1
proc.call # => NameError (undefined local variable or method `foo' for main:Object)
프록을 호출하면 NameError가 발생합니다. 프록의 바인딩에 변수가 정의되어 있지 않기 때문입니다. 따라서 프록에서 접근하는 모든 변수는 프록이 생성되기 전에 정의되어 있거나 인자로 전달되어야 합니다.
foo = 1
proc = Proc.new { foo }
foo = 2
proc.call # => 2
하지만 프록의 바인딩은 변수를 복사하는 대신 참조(reference)를 유지하기 때문에, 메인 컨텍스트에서 변수를 정의한 후에 값을 변경할 수는 있습니다.
foo = 1
Proc.new { foo = 2 }.call
foo #=> 2
이 예제에서 볼 수 있듯이 foo 변수는 프록 내부에서도 외부와 동일한 객체를 가리킵니다. 따라서 프록 내부에서 변수를 수정하면 외부의 변수 값도 함께 변경됩니다.
바인딩(Bindings)
Ruby는 현재 스코프를 추적하기 위해 바인딩을 사용합니다. 바인딩은 코드의 각 위치에서 실행 컨텍스트(execution context)를 캡슐화합니다. binding 메서드는 현재 위치의 바인딩 정보를 담은 Binding 객체를 반환합니다.
foo = 1
binding.local_variables # => [:foo]
Binding 객체에는 #local_variables라는 메서드가 있으며, 현재 스코프에서 사용 가능한 모든 지역 변수의 이름을 반환합니다.
foo = 1
binding.eval("foo") # => 1
#eval 메서드를 사용하면 바인딩 위에서 코드를 평가(evaluate)할 수 있습니다. 위 예제는 단순히 foo를 호출하는 것과 결과가 같아 크게 유용해 보이지 않습니다. 하지만 바인딩은 전달할 수 있는 객체이기 때문에 훨씬 더 흥미로운 용도로 활용할 수 있습니다. 예제를 살펴보겠습니다.
실전 예제
이제 차고에서 안전하게 익힌 바인딩 지식을 스키장에 들고 나가 눈밭에서 마음껏 활용해 볼 시간입니다. Ruby가 언어 전반에서 내부적으로 바인딩을 활용하는 것 외에도, 바인딩 객체를 명시적으로 사용하는 상황이 몇 가지 있습니다. 대표적인 예가 ERB, 즉 Ruby의 템플릿 시스템입니다.
require 'erb'
x = 1
def y
2
end
template = ERB.new("x is <%= x %>, y() returns <%= y %>, self is `<%= self %>`")
template.result(binding) # => "x is 1, y() returns 2, self is `main`"
이 예제에서는 x라는 변수, y라는 메서드, 그리고 둘을 참조하는 ERB 템플릿을 만듭니다. 그런 다음 현재 바인딩을 ERB#result에 전달하면, 템플릿의 ERB 태그가 평가되고 변수가 채워진 문자열이 반환됩니다.
내부적으로 ERB는 Binding#eval을 사용해 각 ERB 태그의 내용을 전달된 바인딩의 스코프에서 평가합니다. 위 예제에서 동작하는 간소화된 구현은 다음과 같습니다.
class DiyErb
def initialize(template)
@template = template
end
def result(binding)
@template.gsub(/<%=(.+?)%>/) do
binding.eval($1)
end
end
end
x = 1
def y
2
end
template = DiyErb.new("x is <%= x %>, y() returns <%= y %>, self is `<%= self %>`")
template.result(binding) # => "x is 1, y() returns 2, self is `main`"
DiyErb 클래스는 초기화 시 템플릿 문자열을 받습니다. #result 메서드는 모든 ERB 태그를 찾아 그 내용을 평가한 결과로 치환합니다. 이를 위해 전달된 바인딩에 대해 ERB 태그의 내용을 인자로 Binding#eval을 호출합니다.
#result 메서드를 호출할 때 현재 바인딩을 전달하면, eval 호출이 메서드 외부, 심지어 클래스 외부에서 정의된 변수에 명시적으로 전달하지 않고도 접근할 수 있습니다.
숲속에서 길을 잃지 않으셨나요?
숲속으로 떠난 스키 여행이 즐거웠기를 바랍니다. 이번 호에서는 지난번에 가볍게 훑고 지나갔던 스코프와 클로저를 더 깊이 들여다봤습니다. 숲속에서 길을 잃게 되지는 않았기를 바랍니다. 바인딩에 대해 더 알고 싶거나 다루었으면 하는 다른 Ruby 주제가 있다면 언제든 알려주세요.
함께해 주셔서 감사합니다. 떠나기 전에 다음 개발자를 위해 바인딩에 묻은 눈을 털어 주는 센스, 잊지 마세요. 이런 마법 같은 여행이 마음에 드셨다면 Ruby Magic을 구독하세요. 매월 한 번 정도 새로운 아티클이 게시될 때 이메일로 받아볼 수 있습니다.