티스토리 뷰

오늘의 포스팅 내용은 Ruby Metaprogramming 이다. 

포스팅의 내용은 사내에서 Ruby Metaprogramming 에 대한 발표를 위해서 아래의 책을 읽고 내용들을 정리한것이다.

https://books.google.co.kr/books/about/Metaprogramming_Ruby_2.html?id=V0iToAEACAAJ&source=kp_cover&redir_esc=y


내용이 많기때문에 시리즈로 소개할 예정이다. 


저번 포스팅(Method - http://idea-sketch.tistory.com/38 )에 이어서 다음편이다. 


오늘은 Blocks 과 Scopes 에 대해서 알아볼 차례이다. 

 Blocks 은 proc 과 lambda 등을 포함해서 callable objects 라고 불리는 구성중에 하나이며

우리는 Ruby 코드를 작성할때 blocks 을 사용하고 있기 때문에 이미 blocks 에 대해서 익숙하다.

하지만 지금까지 우리가 신경쓰지 않았던 blocks 의 특징이 있는데 바로 scope 를 조정할 수 있다는 것이다. 


기본적인 Blocks 

 이미 blocks 에 대해서 알고 있지만 간단하게 다시한번 확인해보자.

def a_method(a, b)
  a + yield(a, b)
end

a_method(1, 2) {|x, y| (x + y) * 3 } # => 10

 우리는 block을 정의할때 중괄호( { } ) 를 사용하거나 do .. end 키워드를 사용한다.

그리고 일반적으로 중괄호를 쓸때는 한줄 코드작성으로 모든작업을 끝낼때 사용하고 

do ... end 키워드를 쓸때는 여러줄로 코드를 작성할때 사용한다.

 또한 정의된 block 을 호출할때는 yield 키워드로 block 을 호출한다.

마지막으로 block 은 위의 예제에서의 x,y 와 같이 호출시 파라미터를 가질수 있고 사용할때 파라미터로 넣어주면 된다. 

block의 마지막 줄에 있는 값은 return 값이 되고 이는 yield 키워드의 return 값이 된다.


Ruby에서는 함수 호출시 block을 전달했는지를 확인하기 위해서 Kernel#block_given? 함수를 제공한다.

def a_method
  return yield if block_given?
  'no block'
end

a_method # => "no block"
a_method { "here's a block!" } # => "here's a block!"

block에 대한 간단한 소개는 여기까지만 하고 이제부터는 조금더 자세하게 알아보자.


Block 은 Closure 이다.

 이번에는 block을 정의했을때 block 내부에서 사용하는 변수들이 어떤값으로 binding 되는지 알아보자.

아래의 예제를 확인해보자.

def my_method
  x = "Goodbye"
  yield("cruel")
end

x = "Hello"
my_method {|y| "#{x}, #{y} world" } # => "Hello, cruel world"

 위의 예제의 block 에서 사용된 변수 x, y에 대해서 확인해보자. 

일단 y는 기존에 알고있듯이 yield 호출시 사용된 파라미터인 'cruel' 이다. 

그러면 x는 어디에서 가져온 x일까!? x는 block을 사용하는 함수내부에서 정의한 x('Goodbye')가 아닌 block이 바인딩 될때의 scope에 존재하는 변수 x('Hello')이다! 

 이를 정리해보면 block은 block이 정의하는 scope 에서 변수들을 바인딩 한다. 

그리고 이를 계속해서 유지하는데 이러한 특성 때문에 block을 클로저(closure) 라고 한다. 

또한 block 내부에서도 변수를 정의하고 사용 할 수 있지만 block이 끝나는 순간 해당변수는 사라지는 로컬변수이다. 

def just_yield
  yield
end
top_level_variable = 1

just_yield do
  top_level_variable += 1
  local_to_block = 1
end

top_level_variable # => 2
local_to_block # => Error! block 내부에서 정의된 로컬변수는 사라졌다! 

그렇다면 block은 클로저(closure)라는 특징을 어떻게 활용할 수 있을까? 이에 대해서 알려면 scope 에 대해서 조금더 자세히 들어가 봐야한다.


Scope

 한번 스스로 Ruby 프로그램의 디버거가 되었다고 생각해보자. 수 많은 소스코드중에 어느곳에 breakpoint가 지정되어있고

breakpoint를 만나기전까지 프로그램 내부를 계속해서 돌아다닐꺼다. 그러다 어느순간 breakpoint 를만나고 

딱 멈추는 순간 디버거인 우리가 볼 수 있는 모든것들의 범위가 scope 이다.   

 이번에는 scope 가 변경되는 시점에 대해서 알아볼 차례이다. 그리고 scope 변경에 따른 특별한 문제에 대해서 알아보고 이를 해결하는 방법에 대해서도 알아보자.


아래의 예제는 Kernel#local_variables 함수를 통해서 scope 가 어떻게 변경되는지를 확인해보는 예제이다.

v1 = 1
class MyClass
  v2 = 2
  local_variables # => [:v2]
  def my_method
    v3 = 3
    local_variables
  end
  local_variables # => [:v2]
end

obj = MyClass.new
obj.my_method # => [:v3]
obj.my_method # => [:v3]
local_variables # => [:v1, :obj]

scope의 변화를 생각하면서 위에서부터 코드를 따라가보자. 

 처음은 top-level scope 이다. top-level 에서 v1의 변수를 정의하고 그 후에 MyClass 정의 로 들어간다.

( top-level은 이미 설명한적이 있다 http://idea-sketch.tistory.com/37)

 그 다음 scope는 MyClass 클래스이다. MyClass에서 v2와 함수들을 정의하고있다. MyClass의 함수들(my_method)는

아직 실행되고 있지 않기때문에 클래스 정의가 끝나기 전까지는 scope는 변경되지 않는다. 

그리고 MyClass 정의가 끝나면 다시 scope는 top-level scope로 이동된다.


 이번에는 MyClass 객체인 obj가 my_method를 두번 호출하면 어떤일이 생기는지 생각해보자. 

첫번째 my_method를 호출했을때 scope는 my_method 로 변경되고 로컬변수인 v3를 정의한다. 

그리고 함수가 끝나고 top-level scope로 변경될때 기존의 my_method의 scope는 사라진다. 

그다음 두번째 my_method를 호출할때 다시한번 로컬변수인 v3를 새로 정의한다.

 이처럼 Ruby에서는 scope가 변경될때는 기존에 binding 되었던 정보들은 버리고 scope에 맞는 새로운 정보로 binding 한다.

이러한 특성때문에 Ruby에서는 inner scope 에서 outer scope 에 있는 변수에 접근 할 수 없다!

outer scope 에서 inner scope 로 변경될때 이미 outer scope 에 있는정보들은 버려지기 때문이다.

즉 MyClass 내부에서 v1 변수에 접근할경우 "falls out of scope" 가 발생한다.

v1= 1
class MyClass
  v2 = v1
end
# NameError: undefined local variable or method `v1' for MyClass:Class

이번에는 Scope Gates 에 대해서 알아보면서 scope의 변경을 좀더 쉽게 구분해보자. 


Scope Gates

Ruby에서는 아래의 3가지 scope 변화시에 기존의 binding 정보를 버리고 새로운 정보를 binding 한다.

  1. 클래스 정의시
  2. 모듈 정의시
  3. 함수

위의 3가지 상황에서 대한 키워드가 이미 정의되어 있는데 이는 class, module, def 이다. 

그리고 이렇게 scope의 변경시점을 정의한 키워드들을 Scope Gates 라고 부른다. 

Scope Gates를 생각하면서 위의 예제를 다시한번보자.

v1 = 1
class MyClass # SCOPE GATE: entering class
  v2 = 2
  local_variables # => ["v2"]
  def my_method # SCOPE GATE: entering def
    v3 = 3
    local_variables
  end # SCOPE GATE: leaving def
  local_variables # => ["v2"]
end # SCOPE GATE: leaving class

obj = MyClass.new
obj.my_method # => [:v3]
local_variables # => [:v1, :obj]

이제는 처음보다 scope를 구분해서 보기 쉬워졌다. 

top-level scope, MyClass 정의시에 scope, 그리고 my_method 호출시 scope 이렇게 3개의 scope가 보이기 시작했다. 

class/module 정의와 method 에서의 scope 에는 미묘한 차이가 있다.

class/module 정의 scope 에서의 코드는 즉시 실행되지만 method scope에서의 코드는 method를 호출할때 실행한다.


Flattening the Scope

하지만 만약 다른 Scope의 변수를 사용하고 싶을때는 어떻게 해야할까? 

my_var = "Success"
class MyClass
  # 우리는 여기서 my_var 를 참조하고 싶다...
  def my_method
    # 그리고 여기서도...
  end
end

어떻게 하면 my_var 를 가지고 Scope Gates 를 통과할수 있을까?

우선 my_var 를 가지고 class 의 Scope Gates를 통과할 수는 없다 하지만 우리는 Scope Gates 인 class 키워드를 사용하지 않고도 클래스를 정의 할 수 있다. 바로 Class.new 이다. Class.new 는 완벽히 class 키워드를 대체한다!

my_var = "Success"
➤ MyClass = Class.new do
➤ # 이제 여기서는 my_var 를 참조할수 있다!
➤ puts "#{my_var} in the class definition!"
    def my_method
      # 하지만 여기서는...?
    end
end

Class.new 를 이용해서 이제는 클래스 정의시에 scope가 변경되었음에도 불구하고 my_var를 참조할 수 있게되었다.

그렇지만 아직 my_method 에서는 my_var를 참조하지 못하고 있다. 여기는 어떻게 해결해야 할까?

질문에 대한 대답은 def 대신에 바로 이전 포스팅(http://idea-sketch.tistory.com/38)에서 사용했던 Dynamic Methods 를 이용하면 된다. 

my_var = "Success"
MyClass = Class.new do
  "#{my_var} in the class definition"
➤ define_method :my_method do
➤     "#{my_var} in the method"
➤ end
end
➤ MyClass.new.my_method

require_relative "../test/assertions"
assert_equals "Success in the method", MyClass.new.my_method
# => Success in the class definition
# => Success in the method

이렇게 기존의 Scope Gates 를 대체하는 다른 키워드를 사용해서 Scope에 변수를 통과시키는 트릭을 Flat Scope 라고 부른다. 


Sharing the Scope

Flat Scope 를 배움으로써 우리는 여러 scope 에서 마음대로 변수들을 공유할 수있게 되었다. 

이번에는 함수들에서 변수를 공유하고 싶을때 사용하는 방법을 알아보자.

def define_methods
  shared = 0
  Kernel.send :define_method, :counter do
    shared
  end
  Kernel.send :define_method, :inc do |x|
    shared += x
  end
end

define_methods 
counter # => 0
inc(4)
counter # => 4

위의 예제는 2개의 Kernel Methods 를 정의해서 shared라는 변수를 공유하고 있다. 

(심지어 shared는 define_metehods 에서 생성된 로컬변수이다.!)

다른 함수들은 Scope Gates 에 의해서 shared 에 대해 알지 못하지만 inc 와 counter 함수는 shared를 공유하고 있다.


이렇게 Scope Gate로 구분되는 범위내에서 여러 Flat Scope를 적용해서 변수를 공유하고 그 외의 함수들에서는 변수의 존재를 모르게 하는방법을 Shared Scope 라고 한다. 

Shared Scope는 실제로 많이 사용되지는 않지만 잘만 사용한다면 아주 유용한 트릭이다. 


지금까지 배운 Scope Gates 와 Flat Scopes, Shared Scopes 를 이용하면 원하는곳에서 변수를 사용 할 수 있게 되었다. 


Instance_eval

마지막으로 Scope Gates 에 변수를 통과시키는 또다른 방법이 있다. 

바로 BasicObject#instance_eval 을 이용하는 방법이다. 아래의 예제를 보자.

class MyClass
  def initialize
    @v = 1
  end
end

obj = MyClass.new
obj.instance_eval do
  self # => #<myclass:0x3340dc @v="1">
  @v # => 1
end

v = 2
obj.instance_eval { @v = v }
obj.instance_eval { @v } # => 2

예제를 보면 obj.instance_eval 을 이용해서 객체 자신을 확인하거나 변수에 접근할 수 있다.

마지막 3라인을 보면 알겠지만 instance_eval을 이용해서 인스턴스변수인 @v에 v값을 할당하며 Flat Scope와 동일하게 동작하는 것을 볼 수있다. 

예제와 같이 instance_eval을 이용하면 Scope Gates 에 변수를 통과시킬수 있다.


하지만 instance_eval을 사용할때의 주의할점이 있는데 이는 너무 강력하기때문에 private 로 감춰놓은 변수,함수들에 대해서도 접근이 가능해지면서 캡슐화가 깨지게 된다. 이는 클래스를 제작한 개발자가 외부에 노출시키고 싶지 않아서 감춰놓았던 정보들을 억지로 보는 방법이 되기 때문에 사용할때 주의가 필요하다. 


Clean Room

때때로 객체를 생성하고 block을 사용해서만 특정 작업을 하길 원할때가 있다. 이럴때 사용되는 개념이 Clean Room 이다.

class CleanRoom
  def current_temperature
  # ...
  end
end

clean_room = CleanRoom.new
clean_room.instance_eval do
  if current_temperature < 20
    # TODO: wear jacket
  end
end

Clean Room은 block을 이용해서 특정작업을 진행하기 위한 목적이므로 block 에서 호출할 함수들 외에는 노출시키지 않는다.

이상적인 Clean Room은 인스턴스변수나 불필요한 함수들을 갖고 있지 않기때문에 BasicObject를 상속받아서 

기존에 배운 Blank Slates(http://idea-sketch.tistory.com/38) 로 만들어주는게 좋다. 



오늘 배운 내용들을 정리해보자.

  1. Scope Gates 라는 키워드들은 scope 를 구분하는 기준이 된다.
  2. Scope Gates 는 class, module, def 키워드 이다. 
  3. block은 정의되는 순간의 scope 정보로 binding 되기 때문에 block을 이용하면 변수의 값을 Scope Gates를 통과해서 참조할 수 있다.
  4. class 는 Class.new, module은 Module.new, def는 Module#define_method 를 이용해서 Scope Gates를 통과시킬수 있고 이런 트릭을 Flat Scope 라고 부른다. 
  5. 하나의 Scope Gate 내에서 여러 함수들을 Flat Scope 적용하면 변수들을 공유할 수있다. 이런 트릭을 Shared Scope 라고 부른다. 
  6. instance_eval 을 이용해서 Flat Scope 와 동일한 효과를 낼 수 있다.
  7. instance_eval 은 캡슐화를 깨트리기 때문에 사용할때 주의가 필요하다.
누군가에게 도움이 되길 바라면서 오늘의 포스팅 끝~




 

댓글