Ruby for Impatient Nuby
- Ruby for Impatient Nuby
원문주소
https://docs.google.com/document/d/15yEpi2ZMB2Lld5lA1TANt13SJ_cKygP314cqyKhELwQ/preview?pli=1
Ruby for Impatient Nuby
지은이
서민구 (Minkoo Seo)
minkoo.seo AT gmail.com
머릿말
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시 2.0 South Korea 라이센스 에 따라 이용하실 수 있습니다. 도와주신 분들(공성식, 이민, 정목, 홍민희. 이상 가나다순)께 감사드립니다.
이 글은 많은 시간을 들이지 않고 루비 언어의 전반적인 내용을 알아 볼 수 있게 하기 위한 간략한 개론입니다. 본 글은 프로그래밍에 익숙한 분들을 대상으로 하며 하나 이상의 프로그래밍 언어에 익숙하신 분들이 읽기 편하게 작성되었습니다. 본 글에 대한 의견은 제 블로그내의 토론 에 커멘트 형태로 남겨주시거나 이메일(minkoo.seo AT gmail.com)으로 주시기 바랍니다. 또한 본 글의 수정을 직접 하기를 원하시는 분들께는 제한 없이 수정권한을 드리겠습니다. 이 경우 역시 이메일을 주시면 수정 권한을 설정해드리겠습니다.
본 글의 구성은 다음과 같습니다. 먼저 1장에서 루비의 기본 문법을 소개하고, 2장에서 함수, 객체지향적 특징, 블록 구조 등에 대하여 설명합니다. 3장에서는 루비의 메타 프로그래밍에 대하여 간략히 소개드립니다. 이 내용을 보고 난뒤 Rails에 대한 내용은 대산 님이 작성하신 따라하기 1 를 참고하세요.
들어가기에 앞서
먼저, 오늘의 루비를 있게한 킬러 애플리케이션 Rails에 대한 데모 를 보시기 바랍니다. 다 볼 필요는 없고, weblog를 15분에 만드는 데모만 보면 됩니다.
데모를 보셨습니까? 놀라셨나요? 그러면 루비에 대해서 알아보도록 하죠. 먼저 루비를 설치해야겠죠. 윈도우 사용자라면 One-click Installer 를 통해 루비를 설치합니다. 리눅스 사용자라면 ruby, ri, rdoc, lib-ruby등의 패키지를 설치해야합니다. yum이나 apt-get으로 설치하세요. 다음 루비가 설치가되었다면 콘솔에서 다음과 같이 입력합니다.
mkseo@mkseo:~$ irb
irb(main):001:0>
바로 여기가 interactive 콘솔입니다.
루비의 기본 문법과 자료구조
Hello World
"Hello World"를 시작하겠습니다.
irb(main):001:0> "hello world"
=> "hello world"
irb(main):002:0>
간단하죠? 이 예는 모든 명령이 값을 반환한다는 중요한 사실을 말해줍니다. 또는 출력문을 이용할 수도 있습니다.
irb(main):003:0> puts "hello world"
hello world
=> nil
irb(main):004:0> print "hello world"
hello world
=> nil
irb(main):002:0> p "hello world"
"hello world"
=> nil
puts는 한줄에 객체의 내용을 출력합니다. print는 newline이 따라 붙지 않지만 이 경우에는 interactive shell 이므로 줄이 자동으로 바뀝니다. p는 객체의 내용을 자세히 보여주며, 이를 위해 객체의 inspect 메소드를 호출하고 그 결과를 출력합니다. 그러나 이 예에서는 문자열의 경우 inspect 메소드의 결과값이 문자열 그 자체와 동일하므로 별다른 차이는 없습니다. "=> nil"에서 nil은 Null을 의미합니다. 출력문의 경우에는 반환값이 nil이라는 거죠.
루비의 자료구조
다음, 루비의 type들에 대해서 알아보겠습니다. 루비의 모든 변수에는 타입이 주어지지 않습니다. 그러나 루비의 모든 값에는 타입이 존재합니다. 예를들어
irb(main):005:0> s = "hello world"
=> "hello world"
irb(main):006:0> puts s
hello world
=> nil
irb(main):007:0>
과 같이 타입 없이 변수를 선언할 수 있습니다. 또한 변수내용 출력시에는 다음과 같이 #{...} 를 사용할 수 있습니다.
irb(main):006:0> puts "Your message: #{s}"
Your message: hello world
=> nil
한편, 루비에서는 정수, 실수, 매우 큰 정수, 리스트, 해시, range가 기본적으로 존재합니다.
irb(main):014:0> a = 1 # 정수
=> 1
irb(main):015:0> b = 1.1 # 실수
=> 1.1
irb(main):016:0> c = 1000000000000000000000000000000000 # 매우 큰 정수
=> 1000000000000000000000000000000000
irb(main):017:0> c.class
=> Bignum
irb(main):018:0> d = [1] # 배열 or 리스트
=> [1]
irb(main):020:0> e = { 1=>’a', 2=>’b’ } # 해시
=> {1=>"a", 2=>"b"}
irb(main):021:0> e[2]
=> "b"
대강 어떻게 돌아가는지 감을 잡으셨을 것입니다. c.class 는 c라는 인스턴스 변수의 클래스를 알려주며 매우 큰 수가 자동으로 Bignum 클래스로 처리된 것을 볼 수 있습니다. d는 리스트이고, e는 해시입니다. 한가지 중요한 사실을 빠뜨렸는데요, 루비에서는 everything is an object입니다.
다만 성능을 위해 정수는 a라는 변수가 1이라는 인스턴스를 참조하는게 아니라, a라는 변수 자체에 값을 담고 있습니다. 실수의 경우에는 객체가 생성되며 변수에는 그 객체에 대한 참조가 저장됩니다. 하지만 이것은 성능을 위한 설명일 뿐이고, 실제 사용자의 입장에서는 다음과 같이 정수에 대해서도 메소드를 호출할 수 있습니다.
irb(main):022:0> a = -1
=> -1
irb(main):023:0> a.abs
=> 1
irb(main):024:0> a.next
=> 0
irb(main):025:0>
abs나 next는 a라는 변수에 대한 메소드입니다. 그러나 위에서 보다시피 a.abs()라고 굳이 하지 않고 a.abs 라고 메소드를 호출할 수 있습니다. 이와 같은 특징 때문에 코드를 매우 간략하고 읽기 편하게 만들 수 있다는 장점이 있습니다.
리스트가 나왔었으니 리스트를 조금 갖고 놀아보죠.
irb(main):025:0> l = [1,2,3,4]
=> [1, 2, 3, 4]
irb(main):026:0> l[0]
=> 1
irb(main):027:0> l[1..2]
=> [2, 3]
irb(main):028:0> l[-1]
=> 4
irb(main):029:0> l[100]
=> nil
irb(main):030:0>
이처럼 리스트는 임의의 첨자로 내용을 보거나 1..2 와 같은 표현으로 부분을 받아올 수도 있습니다. 여기서 1..2는 1번째 원소부터 2번째 원소까지를 의미합니다. l[-1]은 거꾸로 첫번째 원소(즉 4)를 의미합니다. l[-2]는 3이겠죠.
- // TODO: range 설명
rb(main):009:0> (1..10).to_a
=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
irb(main):010:0>
to_a는 array(리스트)로 바꿔달라는 메소드입니다.
다음은 해시에 대한 예를 보여드리겠습니다.
irb(main):001:0> h = { } # 새로운 해시 생성
=> {}
irb(main):002:0> h[0] = 1 # 키 0에 값 1을 저장
=> 1
irb(main):003:0> h # 해시의 내용을 보여줍니다.
=> {0=>1}
irb(main):004:0> h.keys # 해시의 keys라는 메소드를 호출했습니다. 괄호가 필요 없음에 주목하세요.
=> [0]
irb(main):005:0> h[1] = 2
=> 2
irb(main):006:0> h.keys
=> [0, 1]
irb(main):007:0>
흐름 제어
다음은 if - else 조건문의 예입니다.
irb(main):007:0> if 1 + 1 == 2
irb(main):008:1> puts "yes!"
irb(main):009:1> else
irb(main):010:1* puts "oops?"
irb(main):011:1> end
yes!
=> nil
irb(main):012:0>
보기 좋게 indent 를 하였지만 indent 자체에 문법적인 의미는 없습니다. 다음은 조건문을 한줄로 적은 예입니다.
irb(main):012:0> if 1 + 1 == 2 then puts "yes" else puts "oops?" end
yes
=> nil
irb(main):013:0> if 1 + 1 == 2; puts "yes" else puts "oops?" end
yes
=> nil
이처럼 루비는 줄바꾸기가 중요한 언어입니다. 두번째 형태에서 1 + 1 == 2; 에 있는 세미콜론은 쉘 스크립트에서 하듯이 줄바꿈을 의미합니다. 혹은 이 코드는 if문의 반환값 자체를 써서 줄일 수도 있습니다.
irb(main):002:0> puts (if 1 + 1 == 2 then "yes" else "oops?" end)
yes
=> nil
다음은 삼항 연산자의 예입니다.
irb(main):005:0> puts (1 + 1 == 2 ? "Yes" : "oops?")
Yes
=> nil
다음은 case문입니다.
irb(main):023:0> a = 2
=> 2
irb(main):024:0> case a
irb(main):025:1> when 1; puts 1
irb(main):026:1> when 2; puts 2
irb(main):027:1> end
2
=> nil
caes문을 사용할 때 꼭 case에 넘겨주는 변수가 없어도 됩니다. 그렇게하면 차례로 when 문이 실행되므로 가장 처음 참인 문장이 실행되고 빠지게 됩니다. 예를들면,
irb(main):028:0> a = 2
=> 2
irb(main):029:0> case
irb(main):030:1* when a == 1; puts 1
irb(main):031:1> when a == 2; puts 2
irb(main):032:1> when a == 3; puts 3
irb(main):033:1> end
2
=> nil
irb(main):034:0>
이런것도 되는 것이죠. 이런 것들을 보면 루비 해커들의 해킹능력은 끝이 없어보입니다. 한편 루비는 자연스러운 영어식 코딩도 가능합니다.
irb(main):034:0> a = 2
=> 2
irb(main):035:0> puts "2″ if a == 2
2
=> nil
"puts two if a equals to two" 완벽한 영어 문장이죠? 이런의미에서도 아름다운 언어입니다.
irb(main):036:0> puts "not 1″ unless a == 1
not 1
=> nil
unless도 있습니다만, 이해하는데 시간이 좀 걸립니다. 조건이 거짓인 경우를 말하죠. 조건문에서는 nil과 false는 false이며 그 외에는 참을 의미합니다.
irb(main):037:0> 1 if nil
=> nil
irb(main):038:0> 1 if false
=> nil
irb(main):039:0> 1 if true
=> 1
irb(main):040:0> 1 if ""
(irb):40: warning: string literal in condition
=> 1
irb(main):041:0> 1 if 0
=> 1
반복문
리스트를 traverse해보겠습니다.
irb(main):001:0> list = [1,2,3,4,5,6,7,8,9,10]
=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
irb(main):002:0> list.each { |i| puts i if i % 2 == 0 }
2
4
6
8
10
=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
irb(main):003:0>
each 는 리스트의 메소드입니다. each는 리스트내 각 원소를 뒤에나오는 {…}로 구성된 블록으로 넘깁니다. 블럭에서는 넘어온 값을 i 라고 받았습니다 (|i| 부분). 다음, i가 짝수면 프린트를 했습니다.
한편 { .. } 는 do … end 로도 쓸 수 있습니다. 보통은 블록이 한줄이면 { … }에 쓰고 여러줄짜리면 do .. end 로 구성하는데 사실은 둘간에 연산자 우선순위의 미묘한 차이가 있으나 평상시에는 바꿔써도 큰 차이를 보이지는 않습니다.
결과 부분을 주의해서 보세요. 2,4,6,8,10이 줄바꿈을 하며 출력된 내용이 puts로 출력된 부분입니다. => 뒤의 내용은 each 메소드의 반환 값입니다. 따라서 each를 호출할때 우리에게는 2번의 기회(블록안에서의 명령 실행과 반환값)이 있게 되는 셈입니다.
현대적인 언어에서는 누구나 갖고 있는 for each 절 역시 지원합니다.
irb(main):003:0> for i in list
irb(main):004:1> puts i if i % 5 == 0
irb(main):005:1> end
5
10
=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
irb(main):006:0>
반복 대상을 range로 지정할 수도 있습니다.
irb(main):006:0> for i in (1..10)
irb(main):007:1> puts i if i % 5 == 0
irb(main):008:1> end
5
10
=> 1..10
irb(main):009:0>
여기서 (1..10)이 1부터 10까지의 range죠. 이런식으로 연습하면서, 코딩은 실제로는 에디터 상에서 해서 파일에 저장합니다. 그러면 실행이 가능하게 되는 거죠.
mkseo@mkseo:~$ cat > a.rb
#!/usr/bin/ruby
puts "hello world"
mkseo@mkseo:~$ chmod +x a.rb
mkseo@mkseo:~$ ./a.rb
hello world
mkseo@mkseo:~$
이제 조금 더 복잡한 반복문을 해보도록 하죠. 루비에도 당연히 while, do-while가 있습니다. 그 외에는 redo/next/break 라는 키워드들이 있는데 이 중 next는 다른언어의 continue에 해당하고 break는 그 break가 맞습니다. redo만 해보죠.
irb(main):015:0> $status = 0 # $는 전역 변수를 의미함
=> 0
irb(main):016:0> def f(i)
irb(main):017:1> if $status == 0
irb(main):018:2> $status = 1
irb(main):019:2> 0
irb(main):020:2> else
irb(main):021:2* i
irb(main):022:2> end
irb(main):023:1> end
=> nil
irb(main):027:0> for i in (1..5)
irb(main):028:1> if f(i) != 0 then puts f(i) else redo end
irb(main):029:1> end
1
2
3
4
5
=> 1..5
irb(main):030:0>
최초 $status == 0 이 성립하기에 처음에는 반환값이 0일것입니다. 따라서 if 문에서 redo를 하게 되고, 결국 i = 0인 루프가 한번 더 수행됩니다. redo 는 이처럼 현재의 값을 그대로 갖고 반복문 안을 한 번 더 수행하게 합니다. 아마 네트워크가 잠시 응답이 없다던가, DB가 잠시 응답이 없다던가 하는 상황에서의 재수행등에 활용할 수 있을 것입니다. 이 외에도 반복문을 완전히 처음부터 재수행하는 retry도 있습니다.
함수, 객체지향적 특징, 블록 구조
1장의 puts, p, print, each 등은 모두 함수입니다. 이를 우리도 구현해보도록 하죠.
irb(main):018:0> def f(n)
irb(main):019:1> n + 1
irb(main):020:1> end
=> nil
irb(main):021:0> f(1)
=> 2
irb(main):022:0>
말씀드렸듯이 모든 문장은 값을 반환합니다. 따라서 함수 f안에서 return n + 1이 아니라 n + 1이라고만 써도 그 문장이 함수의 가장 마지막 문장이기에 n + 1의 반환값이 함수의 반환값으로 사용됩니다. 물론 return 을 써도 정상동작합니다. 따라서 보통은 return 문이 꼭 필요하면 return 문을 쓰고, 그렇지 않으면 return이라는 키워드 자체는 생략하는 것이 일반적이라고 알고 있습니다만, 어디까지나 개인의 코딩 습관에따라 달라질 수 있습니다.
다음은 피보나치 수열의 예입니다.
irb(main):022:0> def fibo(n)
irb(main):023:1> return 1 if n == 1 || n == 2 # 이 경우에는 return이 필요합니다.
irb(main):024:1> return fibo(n - 1) + f(n - 2)
irb(main):025:1> end
=> nil
irb(main):026:0> fibo(1)
=> 1
irb(main):027:0> fibo(2)
=> 1
irb(main):028:0> fibo(3)
=> 3
irb(main):029:0>
fibo 함수내 첫줄에서 return 문은 꼭 필요합니다. return 이 없다면 해당 문장이 실행되고 반환값은 그대로 사라져버리고 다음줄로 실행이 계속 진행되버리기 때문입니다.
이외에도, 매개변수에 기본 값을 준다던가 또는 임의 개수의 인자를 받는 다던가 하는 것도 가능합니다. 임의 개수의 인자를 받는 부분을 보죠.
irb(main):029:0> def g(*args)
irb(main):030:1> a1, a2 = args[0], args[1]
irb(main):031:1> puts a1
irb(main):032:1> puts a2
irb(main):033:1> end
=> nil
irb(main):034:0> g(1,2)
1
2
=> nil
irb(main):035:0>
- *args로 쓰면 인자를 리스트로 받게 됩니다.
한편 루비는 position에 의해 인자를 구분합니다. 함수에 준 첫번째 인자는 함수의 첫번째 매개변수에 들어가고, 두번째 인자는 함수의 두번째 인자에 들어가죠. 그러나 해싱을 사용해 named parameter 를 구현할 수 있습니다. 방법은, 함수의 마지막 인자가 해시일때는 { } 를 사용해 명시적으로 해당 인자가 해시임을 지정하지 않아도 된다는 점을 이용하는 것입니다.
irb(main):006:0> def f(opt, vars)
irb(main):007:1> a1, a2 = vars[:a1], vars[:a2]
irb(main):008:1> puts "opt = #{opt}\ta1 = #{a1}\ta2 = #{a2}"
irb(main):009:1> end
=> nil
irb(main):010:0> f(1, :a1=> 1, :a2 => 2)
opt = 1 a1 = 1 a2 = 2
=> nil
irb(main):011:0>
- // TODO: 타입 설명 부분에서 심볼 언급
이 코드에서 :a1, :a2는 심볼입니다. 간단히 아무때나 쓸 수 있는 enum이라고 생각하면 됩니다. 굳이 정의 없이 써도 되는거죠.
한편 함수는 다음과 같이 불러도 됩니다.
irb(main):011:0> f 1, :a1=> 1, :a2 => 2
opt = 1 a1 = 1 a2 = 2
=> nil
즉, 괄호가 없어도 된다는 것이죠. 해싱과 괄호 제거 규칙으로 인해 상당히 자연스러운 메소드들이 등장할 수 있습니다. 예를들면 다음의 rails 메소드들 같은 것들이죠.
render_text "hello world" # 화면에 hello world를 찍는다.
redirect_to :action => "index" # index 라는 action으로 리다이렉트 한다.
이에 대해서는 rails와 메타 프로그래밍에서 더 예를 보실 수 있을 겁니다.
다음은 block에 대해 말씀드리죠. 블록은 { ... } 형태를 띄고 함수에 넘겨집니다.
irb(main):035:0> def f(a)
irb(main):036:1> yield a + 1 # 여기는 함수 f내에서 블록과 상호작용하는 부분입니다.
irb(main):037:1> end
=> nil
irb(main):038:0> f(1) { |n| puts n } # <- 바로 여기가 블록입니다.
2
=> nil
irb(main):039:0>
여기서 함수 f는 인자 a 를 받습니다. 다음, yield a+1를 사용해서 f에게 주어진 블록에 a+1을 넘깁니다. f함수의 사용자는 { .. }를 사용해 블록을 지정하는데, 블록안에는 f가 넘겨주는 값을 받기 위한 변수 n이 있습니다. 블록에서는 이 n을 받아와서 화면에 출력을 하는 거죠. "그게 뭐야. 복잡하기만 한데?"라고 생각하시는 분. 이 방법이 얼마나 아름다운 dependency injection 기법인지 곰곰히 생각해보시기 바랍니다. AOP에도 환상적으로 적용이 가능하죠..
위의 문법은 다음과도 같이 할 수도 있습니다.
irb(main):001:0> def f(a, &blk)
irb(main):002:1> blk.call(a + 1)
irb(main):003:1> end
=> nil
irb(main):004:0> f(1) { |n| puts n }
2
=> nil
irb(main):005:0>
&blk는 f에 주어진 블록을 받는데 사용되며, 이 블록을 명시적으로 호출하는데는 call을 사용합니다.
다음은 루비의 core library중 하나인 File을 통해 과연 이 블록이란 것이 어떻게 쓰이는가를 보겠습니다. 다음은 파일 a의 내용을 출력하는 예입니다.
mkseo@mkseo:~$ cat > a
hello
ruby
world
mkseo@mkseo:~$ irb
irb(main):001:0> File.open("a") do |f|
irb(main):002:1* f.each_line { |l| puts l }
irb(main):003:1> end
hello
ruby
world
=> #<File:a (closed)>
irb(main):004:0>
File.open은 파일을 연뒤 이를 yield 합니다. 그러면 넘어온 f를 받고, f의 각 라인을 traverse합니다. 그러면서 각 라인을 l로 받아 화면에 출력합니다. 흥미로운 점은 결과에서 보이듯이 파일이 자동으로 닫혔다는 것이죠. 이는 File.open의 구현이 대충 다음과 같음을 짐작케 합니다.
def open(..)
f = open a file
yield f
f.close
end
지금까지는 단순히 { ... }를 블록이라고만 지칭하였습니다. 그러나 사실은 이는 설명을 위한 지나친 간략화입니다. 루비에는 { .. } 를 사용하는 표현이 세가지 있습니다. 1) 블록, 2) Proc 클래스, 3) proc/lambda죠.
블록은 객체가 아니라 단순히 블록 그 자체로 처리됩니다. 2와 3번의 Proc와 proc/labmda는 객체입니다. 따라서 1번의 블록은 2번과 3번의 경우보다 처리가 더 빠릅니다. 2번의 Proc라는 클래스는 proc/lambda가 제공하는 기능중 일부만 제공합니다. Proc 을 사용한 처리는 권장하지 않으며, lambda 를 사용하기를 권하는 것이 일반적입니다. proc 이라는 명칭은 단순히 lambda에 대한 alias로서만 제공되고 있습니다. Proc보다 lambda가 권장되는 이유는 lambda의 경우, 매개변수의 개수를 확인하고 몇가지 예외처리를 하는 등 Proc보다 더 강력한 기능을 제공하기 때문입니다. 이제 머리아픈 설명은 끝내고 이들이 주는 멋진 장점을 알아보도록 하겠습니다.
다음은 간단한 stack generator의 예입니다. (다음의 코드는 OSCON에서 루비 언어의 창시자 Matz가 보인 예입니다.)
irb(main):037:0> def generate_stack
irb(main):038:1> stack = []
irb(main):039:1> push = lambda { |n| stack << n }
irb(main):040:1> pop = lambda { stack.pop }
irb(main):041:1> return push, pop
irb(main):042:1> end
=> nil
irb(main):043:0> s1_push, s1_pop = generate_stack
=> [#<Proc:0xb7cbce6c@(irb):39>, #<Proc:0xb7cbcd90@(irb):40>]
irb(main):044:0> s1_push.call(1)
=> [1]
irb(main):045:0> s1_push.call(2)
=> [1, 2]
irb(main):046:0> s1_pop.call
=> 2
irb(main):047:0> s2_push, s2_pop = generate_stack
=> [#<Proc:0xb7cbce6c@(irb):39>, #<Proc:0xb7cbcd90@(irb):40>]
irb(main):048:0> s2_push.call(1)
=> [1]
irb(main):049:0> s1_push.call(3)
=> [1, 3]
irb(main):050:0>
문법적인 면을 보면, 생성된 lambda 를 호출할때는 call을 쓰면 되었고, generate_stack 이라는 함수는 push와 pop이라는 lambda를 한꺼번에 return 했습니다.
내용적인 측면에서, s1 이라 불리는 스택과 s2라 불리는 스택이 별개임을 알 수 있습니다. 이처럼 lambda는 자신을 둘러싼 conext를 새로이 잡으면서 생성이 됩니다. 바로 closure를 생성해주는 것입니다.
자 이제 클래스의 세계로 들어가보겠습니다. 간단하게 어떤 수를 주면 그것을 1씩 증가하는 클래스입니다.
irb(main):005:0> class Incrementor
irb(main):006:1> def initialize(val)
irb(main):007:2> @val = val
irb(main):008:2> end
irb(main):009:1>
irb(main):010:1* def next
irb(main):011:2> @val += 1
irb(main):012:2> end
irb(main):013:1> end
=> nil
irb(main):014:0> i = Incrementor.new(1)
=> #<Incrementor:0xb7c73b24 @val=1>
irb(main):015:0> i.next
=> 2
irb(main):016:0> i.next
=> 3
irb(main):017:0> i.next
=> 4
irb(main):018:0>
def initialize는 생성자입니다. @val 은 인스턴스 변수를 지칭합니다. 생성자에서 곧바로 @val 에 값을 할당하는 부분에서 보다시피, 변수를 선언해놓고 값을 할당할 필요 없이 곧바로 쓸 수 있습니다.
def next로정의된 메소드에서는 @val += 1 이 수행될 때, @val + 1 이 @val에 저장됨과 동시에 @val += 1이라는 문장의 결과값으로서 반환됩니다. 따라서 위의 예에서는 i.next 의 결과값으로 1씩 증가된 값이 반환되고 있음을 볼 수 있습니다.
다음은 이 클래스에 제일 처음 주었던 초기값이 무엇인지 알 수 있게 해보겠습니다. 이 경우, 루비는 모든 종류의 수정작업에 대해 클래스/함수의 정의가 열려있음을 사용해 다음과 같이 할 수 있습니다.
irb(main):005:0> class Incrementor
irb(main):006:1> def initialize(val)
irb(main):007:2> @val = val
irb(main):008:2> end
irb(main):009:1>
irb(main):010:1* def next
irb(main):011:2> @val += 1
irb(main):012:2> end
irb(main):013:1> end # 여기까지는 앞서 했던 부분
=> nil
irb(main):018:0> class Incrementor
irb(main):019:1> def initialize(val)
irb(main):020:2> @initial_value = val
irb(main):021:2> @val = val
irb(main):022:2> end
irb(main):023:1>
irb(main):024:1* attr_reader :initial_value
irb(main):025:1> end # 여기까지 필요한 부분을 새로 정의함
=> nil
irb(main):028:0> j = Incrementor.new(1)
=> #<Incrementor:0xb7c553f4 @initial_value=1, @val=1>
irb(main):029:0> j.next
=> 2
irb(main):030:0> j.initial_value
=> 1
attr_reader는 뒤에 주어진 심볼 :initial_value에 대한 읽기 메소드를 자동으로 생성합니다. 3편에서 다루겠지만, 이러한 메소드 생성은 메타 프로그래밍 기법에 근간을 두고 있습니다.
이처럼 루비의 클래스 정의가 열려있는 까닭에 마음만 먹으면 우리는 루비에서 기본적으로 제공하는 메소드/클래스들을 마음껏 고칠 수 있으며, 실제로 몇몇 라이브러리들은 루비 core 클래스들의 기능을 확장하는 방식으로 구현되기도 합니다. 루비 커뮤니티는 이러한 기존 클래스의 수정/변형에 상당히 열린자세를 취합니다. 루비를 사용하다보면 기본적으로 제공되는 라이브러리를 수정해 새로운 기능을 제공하는 경우를 어렵지 않게 찾을 수 있습니다. 다른 언어에서는 이러한 Monkey-Patch를 지양합니다. 그러나 루비에서는 이러한 수정에 대해 관대한 편이며, 이는 커뮤니티의 분위기 및 해당 언어를 사용하는 개발자들의 기본 마인드의 차이라할 수 있습니다.
한편 클래스 메소드에는 여러가지 접근 지정자를 지정할 수 있습니다. 루비도 다른 언어와 마찬가지로 접근 지정자에는 public, protected, private 이 있습니다. 그러나 이들의 의미는 다른 널리 알려진 언어(Java, C++ 등)과는 다소 다릅니다. 이들 세가지 지정자의 의미는 크게 다음과 같이 요약할 수 있습니다.
- public: 어디서나 호출가능. 메소드의 경우 어떤 접근 지정자도 지정하지 않는다면 기본값은 public. 단, initialize는 항상 private이다.
- protected: 메소드를 정의한 클래스와 그 서브클래스에서 호출가능.
- private: 명시적인 수신자를 통해서는 호출할 수 없다. 다시 말해, 동일 인스턴스내에서만 호출할 수 있다.
private 에 대해 부연하자면, 명시적인 수신자를 통할 수 없다는 것은 clsInstance.methodName( ) 과 같이 특정 클래스의 인스턴스를 지정하는 형태의 호출이 불가능하다는 말입니다. 따라서 private 메소드는 외부에서 호출이 불가능할 뿐만 아니라, 동일한 클래스의 인스턴스끼리도 서로 부를 수 없습니다. 반면, 한 클래스의 인스턴스에서 서로다른 메소드간에서 서로 호출할 경우에는 self 라는 인스턴스를 명시적으로 지정하지 않아도 되므로 private 메소드라고 할지라도 서로 부를 수 있습니다.
예를 들어,
irb(main):001:0> class Foo
irb(main):002:1> private
irb(main):003:1> def bar
irb(main):004:2> puts “bar”
irb(main):005:2> end
irb(main):006:1> end
=> nil
irb(main):007:0> f = Foo.new
=> #<Foo:0xb7d21a68>
irb(main):008:0> f.bar
NoMethodError: private method `bar’ called for #<Foo:0xb7d21a68>
from (irb):8
from :0
는 Foo의 private 메소드 bar를 외부에서 호출하려하였으므로 에러가 발생합니다. 이는 다른 언어와 동일한 점입니다. 또한 같은 클래스 인스턴스안에서는 private 메소드를 얼마든지 호출 할 수 있습니다.
irb(main):001:0> class Foo
irb(main):002:1> private
irb(main):003:1> def bar
irb(main):004:2> puts "bar"
irb(main):005:2> end
irb(main):006:1>
irb(main):007:1* public
irb(main):008:1> def call_bar
irb(main):009:2> bar
irb(main):010:2> end
irb(main):011:1> end
=> nil
irb(main):012:0> f = Foo.new
=> #<Foo:0x2c62378>
irb(main):013:0> f.call_bar
bar
=> nil
이 역시 다른 언어와 큰 차이가 없습니다. 그러나 같은 클래스의 서로 다른 인스턴스끼리라 할지라도, 상대 인스턴스를 명시적인 수신자로 지정하게되면 private 을 호출할 수 없습니다.
irb(main):009:0> class Foo
irb(main):010:1> private
irb(main):011:1> def bar
irb(main):012:2> puts "bar"
irb(main):013:2> end
irb(main):014:1>
irb(main):015:1* def call_bar(f)
irb(main):016:2> f.bar
irb(main):017:2> end
irb(main):018:1> end
=> nil
irb(main):019:0> g = Foo.new
=> #<Foo:0xb7d02fdc>
irb(main):020:0> f.call_bar(g)
NoMethodError: private method `call_bar’ called for #<Foo:0xb7d21a68>
from (irb):20
from :0
irb(main):021:0>
이러한 private의 의미는 많은 오해를 불러일으키는 부분이니 잘 기억해야합니다.
그러나 private이 동일 인스턴스에서만 호출할 수 있다는 의미를 갖는다는데 대해 많은 프로그래머들이 반감을 일으키기도 합니다. 이에 대한 흔히 사용되는 원칙은 private을 알고리즘을 정의하는데 사용한다는 것입니다. 같은 인스턴스에서 같은 인스턴스를 다루는데 필요한 알고리즘 말이죠. 한편 Matz는 private이 없었다면 puts 를 만드는것이 불가능하다고 지적합니다.
이게 무슨말일까요? 다시 되짚어 봅시다. 화면에 무언가를 출력하는데는 puts를 씁니다. 그러나 지금까지 puts의 호출에서는 항상 명시적인 수신자가 없었습니다. 즉, 자바의 경우였다면 System.out.println 을 사용하고 println 의 수신자는 System.out입니다. 그러나 puts는 마치 함수를 불러쓰는 것 처럼 그냥 puts라고 불렀죠. 이는 모든 것이 객체다라는 원칙에 어긋나보이지 않나요? 그러나 사실 뚜껑을 열고보면 그렇지 않습니다. 그 이유는..
irb(main):001:0> self
=> main
irb(main):002:0> self.class
=> Object
irb(main):003:0>
작성중인 부분
이 밑으로는 아직 작성중입니다. (2006년 10월 14일.)
이처럼 우리가 그냥 정의하는 함수들은 실상은 Object 라는 클래스의 name이라는 인스턴스 내부였던 것입니다. 따라서 다음과 같이 foo를 정의한다면,
irb(main):003:0> def foo
irb(main):004:1> puts “foo”
irb(main):005:1> end
=> nil
irb(main):006:0> foo
foo
=> nil
irb(main):007:0> o = Object.new
=> #<Object:0xb7c58afc>
irb(main):008:0> o.foo
foo
=> nil
irb(main):009:0>
foo가 기본적으로는 public 접근 지정자가 적용되므로 다른 Object에서도 호출이 가능한 것입니다. 더군다나 Object는 모든 클래스의 부모 클래스이므로 자동으로 이 메소드가 상속될 것이고, 따라서 다음과 같이 아무데서나 불러쓸 수 있는 것입니다.
irb(main):009:0> class Bar
irb(main):010:1> def initialize
irb(main):011:2> foo
irb(main):012:2> end
irb(main):013:1> end
=> nil
irb(main):014:0> Bar.new
foo
=> #<Bar:0xb7c43c38>
irb(main):015:0>
이와 같은 방식으로 루비에서는 모든 것이 객체 지향적으로 다루어집니다.
한편 puts는 Kernel 이라는 클래스에 정의되어 있으며 Object클래스내부로 Kernel이 mixin 되어 있습니다. (mixin에 대해서는 이 글의 목적 중 하나인 ‘최대한 짧게’로 인해 설명을 생략하겠습니다. 관심있으신 분은 http://www.cs.rice.edu/~eallen/papers/p079-allen.pdf를 참고해보세요.)
* TODO
.. inject ..
.. duck typing ..
[DRAFT!!]
메타 프로그래밍. 임의 메소드에 캐싱 기능 넣기.
module Cache
def cache_method(target_method)
hidden_target_method = “__#{target_method.to_s}”.to_sym
alias_method hidden_target_method, target_method
define_method(target_method) do |*args|
cache_name = “@__#{target_method.to_s}_cache”
cache = instance_variable_get(cache_name)
if cache.nil?
cache = instance_variable_set(cache_name, Hash.new)
end
if cache.include?(args)
return cache[args]
else
return cache[args] = send(hidden_target_method, *args)
end
end
end
end
class Series
def fibo(n)
return 1 if n == 1 || n == 2
return fibo(n - 1) + fibo(n - 2)
end
end
puts “Normal Execution”
t = Time.new
f = Series.new
puts f.fibo(30)
puts “Elapsed: #{Time.new - t}”
puts “Cached Execution”
t = Time.new
Series.extend(Cache)
Series.instance_eval do
cache_method :fibo
end
puts f.fibo(40)
puts “Elapsed: #{Time.new - t}”
실행결과
Normal Execution
832040
Elapsed: 4.802257
Cached Execution
102334155
Elapsed: 0.002274