· KLDP.org · KLDP.net · KLDP Wiki · KLDP BBS ·
Humble Little Ruby Book/Chap2


1. 2 이제 코드를 쪼개 봅시다


이제 기본적인 객체들과 객체들을 다루는데 조금 익숙해졌으니, 코드를 나누기 시작할 차례입니다. 물론 한 덩어리의 큰 코드로 응용 프로그램을 실행할 것이라고는 생각하셨던 것은 아니시겠죠! 다시 쓰고 싶은 코드를 매번 반복해서 써야 하고, 30000줄 전에 썼던 변수를 건드리지는 않을까 노심초사하며 코드를 작성해야 한다면 정말 바보같을 겁니다. 그래서 언어의 천재들께서 코드를 나누는 몇 가지 방법을 창조하셨습니다.

이 방법들 중에서 가장 기초적인 것은 블록입니다. 블록은 코드를 시작과 끝을 나타내는 예약어로 문맥에서 떼어낸 코드 조각일 뿐입니다. 시작을 나타내는 키워드는 여기서 예제로 보여줄 begin 키워드나 다음 장에서 배울 if나 for 같은 것들입니다. 코드 블록을 만드려면 블록의 시작을 나타내는 말 다음에 필요한 것(begin 키워드에서는 필요한 것이 없으니 그냥 begin만 쓰시면 됩니다)들을 덧붙이고 이것을 블록으로 놓을 코드 앞에 쓰고 마지막에 end 키워드로 마치면 됩니다. 어떤 분들은 기억하시기 어려울 겁니다. 예제를 보세요.

begin
  puts "I'm in a block."
  puts "Still there..."
  puts "Stillllll in here..."
  puts "OK, I'm done."
end

begin/end 자체를 사용하는 것은 블록이 문맥에서 분리되었다는 것을 나타내는 것 뿐이지만, 이것을 다양한 구성체와 짝 짓고 잘 조합하면 다른 결과를 낼 수 있습니다. 이 절에서 이런 구성체에 대해서 많이 다루고, 나중에 블록들을 더 효과적으로 쓰는 법을 알려드릴 거에요. (그런게 있어요. 절 믿으세요.)

1.1. 메소드

이전의 예제들에서 저는 별로 설명을 하지 않고 오히려 더 자유롭게 메소드를 썼습니다. 객체와 변수가 프로그래밍의 명사라고 한다면, 메소드는 동사라고 할 수 있습니다. "무엇을 하는 것"이 메소드입니다. 좀 더 전문적인 말로, 메소드는 호출받는 코드로 들어가는 매개변수(인자나 옵션이라고도 부릅니다)라는 변수를 이용하여 다른 코드 내에서 호출할 수 있는 코드의 조각입니다. 임의로 호출할 수 있는 begin/end 블록이라고 생각하세요. 임의로 호출 할 수 있다는 것은 언제든지 어디서든 호출할 수 있다는 뜻입니다. 모든 메소드 호출들이 살고 있는 "메소드" 블록 모임 같은 것은 없습니다. 매개변수와 그와 같은 것들이 좀 헷깔린다는 것은 알지만, 이미 텍스트를 puts로 보내거나 문자열을 chop으로 보냈을 때, 메소드로 매개변수를 보낸 적이 있습니다! 그런 것을 한다고 한 적도 없는데 이미 그렇게 하고 있었던 것입니다! 저는 은근슬적 그런 식으로 넘어갔습니다. 저는 말 솜씨가 좋은 범죄자랍니다.

methods.jpg
[JPG image (69.83 KB)]

그림 5: 메소드 호출.

좀 더 전문적으로 말한다면, 루비에서 메소드를 호출하는 것은 메소드를 "호출"하는 것이 아닙니다. 물론 호출한다는 표현을 자주 사용하기는 하지만 말입니다. 실제로는 객체에 "야~ 너 이 메소드 있니?"라는 메시지를 보내는 것입니다. 메소드가 있으면 메소드가 실행되고 메소드가 없으면 이를 갈면서 한탄하며, NoMethodError 예외를 던진답니다. "좋구나, 좋기는 하지만 메소드는 뭣하러 있는거지?" 라고 말씀하시겠지요. 물론, "무언가를 하는 것"의 용도로 사용할 수 있겠습니다만, 더 큰 목적이 있을까요? 물론 있지요.

일단, 메소드는 중복되는 것을 없애줍니다. 예를 들어, 똑같은 15줄짜리 코드를 전체 프로그램에서 여러 번 사용할 생각인데, 계속 반복해서 써 넣어야 한다면 참 바보같을 겁니다. 그저 메소드를 만들어서 필요할 때마다 호출하면 되는 것이지요. 두번째로, 메소드는 코드를 더 잘 나눌 수 있게 해 줍니다. 550 줄짜리 코드 한 덩어리는 별로 재미 없을겁니다. (심하게 자신을 학대하고 혹사하고, 마조히스트 같은 미친 사람이라면 그럴수도 있겠지만 그런 사람들은 모두 회계 일을 하고 있으니까요. 제 말이 맞나요?) 메소드는 거대하고 난잡한 로직들을 작고 더 관리하기 편한 조각으로 나눕니다.

1.1.1. 메소드 정의하기


이제 여러분은 자신만의 뽀대나는 메소드를 만들 수 있기를 기대하고 갈망하고 있을 겁니다. 이제 더 이상 지체할 필요가 없네, 급한 친구! 바로 여기 자네가 찾고 있던 게 있으니.

def my_new_method(name)
  puts "hey, " + name + ", this is my new method..."
end
my_new_method('magnus')
→    hey, magnus, this is my new method...

def를 쓰고 메소드 이름과 매개변수를 쓰면 됩니다. 매개변수라고 하는 것은 그 메소드 내에서 쓸 수 있게 넘겨주는 변수라는 것을 기억하시죠? 다음 줄부터 메소드 코드가 포함되고, end 키워드로 끝이 납니다. 이게 전부입니다. 간단하죠?

음, 사실 메소드는 그게 전부가 아닙니다. 첫째로, 메소드 이름은 (물론, 관례상) 소문자로 시작하고 가능하면 전부 소문자인 것이 좋습니다. 이유는 루비가 대문자로 시작하는 것은 메소드라고 생각하지 않고, 상수나 클래스라고 생각하기 때문이죠. 이렇게 된다면 응용 프로그램이 지저분하게 동작할 수 있죠. 관례에 대해서 말씀드리는 김에 말씀드리는 것인데 메소드 이름에 어울리는 다른 관례들이 있습니다. 먼저, 어떤 속성에 대해서 묻는 것이면 물음표로 끝이 나야 합니다. 예를 들어서 프랑스 군의 승리 여부를 묻는 메소드를 작성한다면 French.has_military_victories?와 같은 메소드를 작성할 것입니다. 물론 반환값은 false일 것입니다. 다른 관례는, 메소드가 그것을 호출한 객체를 수정하는 경우에는 메소드 이름이 느낌표로 끝나야 합니다. 예를 들어, 모든 현존하는 사이보그 소프트웨어를 루비로 대체하여 모든 로봇 유명인사들이 호화로운 루비 기반 시스템으로 동작한다고 합시다. 게다가 얼굴 주름을 펴는 성형수술까지 해서 40살은 더 젊어보이게 해야 된다고 하면, 그저 Dolly_Parton.facelift!라고 해 주면 됩니다. 아니면, 나이를 임의의 값으로 바꿀 수도 있어서 Bob_Baker.set_age!(30)을 호출해도 됩니다.

메소드에 대해서 다음으로 알아볼 것은 넘겨받는 매개변수(인자 아니면 무엇이든 간에 마찬가지)입니다. 이 변수들은 메소드에 넘어가서 지역 변수가 됩니다. 지역 변수는 지역적이어서 코드(메소드) 블록 안에서만 사용할 수 있는 것입니다. 이 말은 코드 블록 안에서 생성된 변수와 매개변수는 코드 블록 밖에서는 사용할 수 없다는 뜻입니다. 말로 이것을 설명하기에는 조금 어려우니까 예제를 보도록 합시다.

def my_method(first, second)
  puts first
  third = second
  puts second
end
my_method("yes.", "no.")
→   yes.
→   no.
puts first
→   ! NameError: undefined local variable or method
puts third
→   ! NameError: undefined local variable or method

반환값으로 반환하거나 바깥으로 넘겨준 경우가 아니라면 매개변수와 생성된 지역변수 모두 메소드 밖에서는 사용할 수 없다는 점을 주목하시기 바랍니다. 이 개념을 스코프(scoping)라고 하고 루비 프로그래밍을 하면서 여러 번 접할 것입니다. 새로운 개념에서 스코프가 중요해지면 강조하여 알려드리겠습니다. 변수는 전역적으로, 지역적으로, 아니면 클래스 스코프 내 등에서 스코프하여 사용할 수 있습니다. 조건 블록이나 반복 블록을 포함하여 어떤 블록이든지 지역 변수를 둘 수 있습니다. 다른 적용 범위에 대해서는 앞으로 차차 배우게 되겠지만 메소드에서 스코프가 어떻게 동작하는지를 잘 기억에 두셔야 합니다. 주의하시지 않으신다면 심각한 두통이 올 수도 있습니다.

이제 매개변수가 무엇이고 무엇을 하는 것인지를 이해하셨으니, 멋진 것을 해 봅시다. 특정 매개변수를 요구하고 싶지 않다면 어떻게 하면 될까요? 아니면 여러 개의 매개변수를 받고 싶으실 것입니다. 루비에서는 두 가지 모두 가능합니다. 루비는 메소드에 선택적(optional)인 매개변수를 지원합니다. 실제로는 선택적이라기보다는 기본값을 할당할 수 있다는 것에 더 가깝습니다. 예를 들어 다음과 같습니다.

def new_method(a = "This", b = "is", c = "fun")
  puts a + ' ' + b + ' ' + c + '.'
end
new_method('Rails')
→   Rails is fun.

이 테크닉은 매개변수나 지역 변수가 99%의 경우에 특정한 값으로 메소드가 동작하는 경우라면 매우 유용하지만 매번 그 값을 바꿀 수도 있습니다. 기본값을 사용하고 싶을 때마다 메소드로 nil값을 보내면 메소드가 이것을 받아서 필요한 것을 검사하여 걸러내고, 이래저래 처리할 수 있겠지만 이것이 타입을 보존해주는 것도 아니고 별로 할 필요없는 일입니다. 기본값을 정해줄 수 있는 이런 특징 때문에 필요한 매개변수만 간단히 정하고, 나머지는 그대로 둘 수 있습니다. 그렇지만 이 특징을 쓰려면 매개변수가 같은 순서대로 있어야 하고, 목록에서 다른 매개변수를 건너뛰면 안 됩니다. 자주 명시적으로 정하지 않을 것들을 뒤쪽에 놓는 것이 좋겠지요.

매개변수 목록은 가변적일 수 있습니다. 넘겨준 매개변수들과의 관계를 출력하는 더 좋은 메소드를 만들면 메소드는 이렇게 됩니다.

def print_relation(relation, *names)
  puts "My #{relation} include: #{names.join(', ')}."
end
print_relation("cousins", "Morgan", "Miles", "Lindsey")
→   My cousins include: Morgan, Miles, Lindsey.

목록에는 몇명이 오든 간에 이름을 넣을 수 있습니다. 마지막 매개변수의 식별자 앞에 별표(*) 문자를 놓으면 가변 길이 목록으로 바꿀 수 있습니다. 실제로는 객체에서 배열이 생성된 것인데, 그렇기 때문에 join 메소드를 사용할 수 있는 것입니다. 이 테크닉은 객체의 목록을 다루거나 메소드의 유연성을 최대화할 때 유용합니다. 예를들어 메소드를 여러 번 호출하기보다 하나 이상의 객체들은 한번에 넘겨 호출하는 경우에도 유용합니다.

1.1.2. 메소드 사용하기

이제 어떻게 메소드를 만드는지 배웠으니, 어떻게 하면 더 효과적으로 사용하는지를 알고 싶을 것입니다. 이전 코드 예제에서 보았듯이 메소드 호출은 메소드 이름에 매개변수가 있다면 매개변수를 붙여서 호출하면 될 만큼 간단한 것입니다. 메소드 호출의 형식은 여러가지가 있습니다. 어떤 경우에 메소드는 매개변수를 받지 않기 때문에 매개변수와 그것을 둘러싸는 괄호가 필요없습니다. 여러 경우에 괄호 없이 메소드를 호출할 수 있지만 별로 좋은 방법이 아닙니다. 매개변수 하나만을 호출하는 경우가 아니라면 두 글자 더 쳐 넣는 것 때문에 가독성을 헤친다면 바보같은 짓이겠죠. 예제를 보겠습니다.

puts "Look ma!  No parentheses!"
puts("Look ma!  No parentheses!")
puts
puts()

모두 올바르게 puts를 호출한 것입니다. 처음 두 예제는 괄호가 필수가 아니라는 것을 보여줍니다. 그 다음 예제는 메소드에 매개변수가 없을 수도 있다는 것을 보여줍니다. 대부분의 메소드는 매개변수가 필요하고 제대로된 개수의 매개변수를 받지 못하면 ArgumentError를 던질 것입니다.

메소드란 것 대단하지 않나요? 그러나 그것으로 무엇을 하는 것이죠? 변수들이 그 안에서 사용되고 바깥으로 나오면 쓸모없어지는데 말입니다. 여기서 메소드의 반환값이 등장하게 됩니다. 메소드는 하나 이상의 값을 밖에서 사용할 수 있도록 메소드 안에서 반환합니다. 메소드는 항상 값을 반환하는데 명시적으로 결정되지 않았으면 nil을 반환(메소드가 정의되었을 때에도 루비는 nil을 반환하죠)하거나 메소드 안에서 쓰인 마지막 값을 반환합니다. 예를 들어

def return_me(value)
  scoped_value = value
end
def echo_me(value)
  value
end
def multi_return
  return 'more', 'than', 'one'
end
my_value = return_me('this is fun!')
puts my_value
→   this is fun!
one, two, three = multi_return
puts one + three
→   more one

메소드 안에 반환값이 없을 경우에는 메소드에서 사용한 마지막 값이 반환된다. 값은 첫 번째 예제처럼 변수가 될 수도 있고, 생성된 객체가 될 수도 있고(문자열 표현 그 자체를 한 줄에 넣으면 문자열 객체가 생성된다) 두 번째 예제처럼 메소드의 마지막 줄에서 참조하는 어떤 객체든지 될 수 있다. 따라서 첫 번째 예제처럼, 반환하고 싶은 값을 마지막 값으로 하면 반환 명령이나 마지막 참조가 꼭 필요한 것은 아니다. 이런 경우가 아니라면 두 번째 예제에서는 값을 반환하는 방법 중 마지막 줄의 참조값을 이용하는 것이고, 마지막 예제는 반환 문의 사용법을 보여주고 있다. 마지막 예제는 (변수 대입에 대하여 알아보았던 절에서 배웠듯이) 메소드 반환시에 반환문과 함께 병렬적으로 변수에 값을 대입하는 것을 보여준다. 대입문의 좌항에서 모은 배열을 간단히 놓을 수 있기 때문에 배열에 값을 놓는데에도 이 방법을 이용할 수 있다.

1.2. 블록과 Proc 객체

이 장의 전반부에서 블록에 대해서 알아보았지만, 이제 더 깊이 알아보려고 합니다. 블록은 루비에서 아주 강력한 개념입니다. 그러나 새로 배우는 사람에게는 혼란스러울 수도 있기 때문에 약간의 논의가 필요합니다. 루비에서 블록은 문맥상으로 실행할 필요가 있는 루비 코드를 담고 있는 객체입니다. 코드 블록이 객체라는 것이 이해가 되지 않겠지만 루비에 있는 모든 것은 객체라는 사실을 기억해 주세요.

1.2.1. 블록 기초

이전에 블록은 간단히 do/end 구조로 둘러싸인 코드라고 했지만, 단지 그것 뿐인 것은 아닙니다. 블록은 여러 가지 방법으로 구성할 수 있으며 그렇게 함으로써 코드를 담고 있는 객체를 생성하여 메소드로 넘겨주거나 변수에 담아둘 수 있습니다. 간단히 말해서, 루비 코드 블록은 이름이 붙어있지 않은 메소드와 비슷합니다. 아마도 C의 함수 포인터나 C++의 함수 객체, 파이썬의 람다와 리스트 함축, 펄의 익명 함수, 자바의 익명 내부 함수, 혹은 좀 더 가까운 예로 스몰토크나 리스프의 블록과 매우 비슷하다고 생각하시면 더 이해가 쉬울 수도 있습니다. 방금 예로 든 언어들 중에서 사용해 보신 언어가 있으시건 익숙한 것이 없으시건 간에 이것은 이상한 일이 아닙니다. 보통 언어 전문가가 아니고서는 생소한 개념입니다. 운 좋게도, 어떤 언어에서든지 아주 중요한 이 개념을 제가 가르쳐드리겠다는 겁니다! 그리고 그걸 굳이 안 배우시겠다고 하시더라도 안타깝지만 이것 없이는 루비 코드를 작성하실 수가 없습니다.

블록의 간단한 사용법인 메소드 매개변수를 한번 보겠습니다. 많은 메소드들이 블록을 매개변수로 받는데 그 중에 하나를 지금 봅시다.

myarray = %w{one two three four}
myarray.each {|element| print "[" + element + "]... " }
→   [one]... [two]... [three]... [four]...

이 코드 조각은 간단히 each 메소드를 사용하여 배열을 반복시키고 각 원소들을 코드 블록으로 넘겨줍니다. 코드 블록은 배열의 원소를 인자로 취급하고 메소드 인자가 된 것 같은 효과를 줍니다. 여기서는 코드 블록을 중괄호를 이용하여 구성하였습니다. 이것은 do/end 조합을 사용하는 것 이외에 코드 블록을 형성하는 방법입니다. 비록 each 메소드를 사용하여 코드 블록을 연 것처럼 보이지만 실제로는 코드 블록을 매개변수화하여 each 메소드에 넘겨준 것입니다. 무슨 뜻인지 이해하지 못하시겠으면 아마도 다음 예제를 살펴보시면 명확히 아실 수 있으실 것입니다. 이미 개념을 잡으셨다면 다음 문단으로 넘어가세요. 똑같은 말을 반복하는 것이니까요.

다음 코드 줄을 떼어놓고 각각의 호출 부분을 분리하여 보겠습니다.

myarray.each {|element| print "[" + element + "]... " }

먼저 배열 객체인 myarrayeach 메소드 호출합니다. 메소드는 블록을 인자로 취급합니다. 다시 말하면 실행하려고 하는 코드 블록을 매개변수로 받습니다. 이 블록은 이전에 보았던 begin/end 블록과 매우 유사합니다. 원한다면, 다음과 같은 코드로 다시 쓸 수도 있습니다.

myarray.each do |element|
  print "[" + element + "]... "
end

간단히 중괄호가 do/end로 대체되었다는 점에 주의하십시오. 두 가지 표현 방식 모두 같은 일을 하지만 한 두줄짜리 코드에서는 중괄호 표현 방식이 더 간결하고 이해하기 쉽습니다. 나중에 여러분께서 작성하신 메소드에서 블록을 이용하는 방법을 배울 때 논의해 볼 이러한 메소드의 관점에서 코드는 루비가 블록의 매개변수를 넘겨주고 그 블록을 실행하게 합니다. 루비가 이렇게 하고 블록의 값이 있다면 블록의 값을 반환하고 더 정확하게는 메소드의 값을 반환합니다. 개념을 바로잡기 위하여 제어 흐름을 그림으로 표현해 봅시다.

아직까지 이해하시지 못하시더라도 이해하셔야 합니다. 부록 A의 문서화 절에 있는 링크를 방문해 보시고, 구글을 찾아보시고, 부록 A의 루비 언어 절에 모아놓은 블로그를 방문해 보세요. 제가 여러분을 이해시키지 못했다고 하더라도 누군가, 어디선가 여러분이 이해하실 수 있는 방법으로 이 개념을 설명해 놓았을 것입니다. 저는 이것이 루비에서 아주 멋지고, 유용하고 강력하고 필수적인 개념이 아니었다면 이 개념을 완벽히 이해하지 않았을 것입니다. 정말 블록을 이해하셨다면 이제 여러분께서 작성하신 코드에서 이것을 사용하는 방법을 배워봅시다.

1.2.2. Proc과 블록

Proc 객체는 블록이 변수에 담겨진 형태라고 생각하세요. 둘의 차이점이 있지만 아직까지는 꼭 알아야 할 정도로 중요한 내용은 아닙니다. 그리고 때가 되면 알게 되겠지요. 가장 근본적인 차이점은 성능이지만 이것은 끝날 때 논의하겠습니다.

Proc 객체는 간단히 말해서 실행가능한 코드 블록을 담고 있는 Proc 클래스의 객체입니다.

myproc = Proc.new {|animal| puts "I love #{animal}!"}
myproc.call("pandas")
→   I love pandas!

보시다시피 Proc은 생성자가 호출될 때 생성되고 매개변수로 블록을 받습니다. 블록에 있는 코드는 Proc의 인스턴스 내에 숨어버리고 언제든지 호출할 수 있게 됩니다. Proc 객체는 코드 블록을 생성하고 그것을 넘겨주거나 그것으로부터 새로운 블록을 생성할 때 특히 유용합니다. Proc 객체 내의 코드를 호출하려면 call 메소드를 호출하면 되고, 이 메소드가 이전에 넘겨주었던 블록의 내부에 있는 코드를 호출하게 됩니다. 예를 들어, 큰 텔레비전 방송국이 새로운 쇼 프로그램 예정표에 호스트와 함께 화면에 보여주는 루비 스크립트를 작성하는 일을 맡겼다고 합시다. 밴조 빌의 브릿지 브리게이드, 라파엘 라미레스 로드리게스 드 예수의 쿠치나 이탈리아나와 팬더!모니움과 같은 쇼 프로그램들 말이죠. 여러분은 그저 텍스트를 콘솔에 표시하고, 방송국의 화려한 그래픽 백엔드가 나머지를 모두 다 할 겁니다. 네, 그렇습니다. 방송국의 기술이 그 정도로 대단한 것이죠. 유일한 문제점은 호스트가 너무 자주 바뀐다는 것입니다. 예를 들어 라파엘씨가 갑자기 쇼 중간 쯤에 쿠치나 이탈리아나의 주방장을 호치민씨로 교체했다고 합시다. 쇼 이름과 호스트 이름을 따로 정하여 호스트 이름을 방송 중에 바꿀 수 있어야 합니다. "블록을 이용하면 되겠군!"이라고 말씀하시겠지요.

def make_show_name(show)
  Proc.new {|host| show + " with " + host}
end
show1 = make_show_name("Practical Cannibalism")
show2 = make_show_name("Cotillions in the Amazon")
show1.call("H. Annabellector")
→   Practical Cannibalism with H. Annabellector
show2.call("Jack Hannah")
→   Cotillions in the Amazon with Jack Hannah
show1.call("Kirstie Alley")
→   Practical Cannibalism with Kirstie Alley

이전에 본 대로 이것은 전형적인 Proc 호출이지만, 여기 뭔가 진행되고 있는 것에 주목하십시오. Proc이 생성될 때 쇼 프로그램의 이름을 넘겨주고, 그 다음부터는 이것에 대하여 언급하지 않았습니다. 어떻게 이것이 가능할까요? make_show_name 메소드로 넘어간 쇼 매개변수가 메소드가 끝나서 스코프 밖으로 나가면서 제거되어야 할 것인데 말이죠. 아, 하지만 이것이 Proc 객체의 아름다운 점입니다. 생성되었을 당시의 문맥을 보존하고 항상 이것에 접근할 수 있습니다. 이것이 우리가 별다른 노력을 기울이지 않았는데도 쇼 프로그램이 이름이 보존되는 이유입니다.

Proc 객체를 생성하는 다른 방법은 코드 블록을 람다(lambda) 메소드를 이용하여 묶는 것입니다. 이 메소드를 호출하는 것은 Proc.new를 호출하는 것과 동일합니다.

myproc = lambda {|x| puts "Argument: #{x}"}
myproc.call("Texas forever!")
→  Argument: Texas forever!

lambda 함수가 마치 Proc.new처럼 코드 블록을 받아서 Proc에 묶는 것을 보고 계십니다. 위의 예제만으로는 서로 다른점을 알 수 없습니다. 첫째로 lambda로 생성된 Proc 객체는 Proc.new로 생성된 것보다 인자의 검사가 더 엄격하다는 차이점이 있습니다.

lproc = lambda {|a,b| puts "#{a + b} <- the sum"}
nproc = Proc.new {|a,b| puts "#{a + b} <- the sum"}
nproc.call(1, 2, 3)
→  3
lproc.call(1, 2, 3)
→  !ArgumentError (wrong number of arguments (3 for 2))

Proc.new로 생성된 Proc 객체는 너무 많은 인자를 넘겨줄 경우에도 잘 동작합니다만 나치 같은 lambda Proc의 인자 검사는 ArgumentError 예외를 던집니다. 참 답답하네요. 단지 너무 많은 인자를 넘겨주었다고 해서 전체 응용 프로그램이 멈추다니요. 좋지 않은데요. 어쨌든 둘이 구별되는 점은 여러분의 응용 프로그램의 흐름을 제어하는 방법에 있습니다. lambda로 생성된 객체는 값을 반환할 때 블록 바깥의 흐름에는 영향을 주지 않습니다. 반면에, Proc.new로 생성된 Proc객체는 값을 반환할 때 바깥에 있는 메소드가 종료되게 됩니다.

def procnew
  new_proc = Proc.new { return "I got here..." }
  new_proc.call
  return "...but not here."
end
def lambdaproc
  new_proc = lambda { return "You get here..." }
  new_proc.call
  return "And I got here!"
end
puts lambdaproc
→   And I got here!
puts procnew
→   I got here...

procnew의 경우를 살펴보면, 반환된 값은 블록 내에서 반환한 값이 됩니다. 람다로 생성한 Proc 객체는 단순히 그 값을 부모 메소드에 반환하고 부모 메소드가 이것은 변수에 담거나 필요하다면 반환하거나 할 수 있습니다. 이것이 기억하셔야 할 중요한 구별되는 점입니다. 왜냐하면 Proc 객체를 메소드 내에서 사용하면서 왜 메소드가 중간에 빠져나가는지를 알지 못하여 심한 두통으로 고생할 수가 있기 때문입니다. (경험에서 우러나오는 말입니다!) 이제 여러분은 Proc 객체를 이용하여 블록을 여러분의 코드안에 넣었을 때 어떻게 동작하는지를 이해하셨으니, 이제 이것을 어떻게하면 여러분의 메소드에 더 단단히 합치는지를 알아봅시다.

1.2.3. 블록 만들기

여러분께서 작성하신 메소드에서 블록을 동작시키는 방법이 몇 가지 있습니다. 첫 번째 방법은 다른 객체들을 다룰 때처럼 Proc 객체를 매개변수로 넘겨받는 방법입니다. 하지만 이것은 별로 좋은 방법 같지는 않고, 성능에 타격이 있습니다. 저는 이 타격을 기름진 베이컨 조각으로 찰싹 맞는 것과 황홀함 사이의 수준에 두고 싶네요. 운 좋게도 루비에는 야단법석 떨 필요 없이 성능 저하를 최소화하는 몇 가지 방법이 있습니다. 블록을 매번 여러분의 코드에 통합시키는 방법은 꽤 간단합니다. 그저 여기저기에 있는 필요한 위치에 yield를 주입하고 메소드의 경계를 풍부하게 응용하여 작은 통합된 개발 접시에 합쳐 담은 뒤 바삭해질 때까지 400도씨에서 15분간 구우면 됩니다.

묵시적 블록 사용 Proc 매개변수를 얻는 곳의 바깥쪽에서는, 루비에서 블록을 매개변수로 사용하는 방법은 한 가지 밖에 없습니다. 그러나 이 방법은 더 직관적일 뿐만 아니라 더 잘 수행됩니다. 저는 이것을 묵시적 블록 사용이라고 부르는데 메소드에게 "어이, 나는 이 블록을 여기서 사용한다."라고 알려줄 수 없기 때문이고, 그리고 나서 그것을 메소드 내에서 호출할 수 없기 때문입니다. 그저 블록의 코드 제어를 양보하는 것입니다. 이것은 예제 없이는 이해하기 힘들겠군요. 그래서 좀 간단한 코드 조각을 보여드리겠습니다.

def yieldme
  print "1. Enter method. "
  yield
  print "3. Exit method."
end
yieldme { print "2. Enter block. "}
→   1. Enter method. 2. Enter block. 3. Exit method.

여기서 일어나는 일에 주목해주세요. 먼저 메소드로 들어가서 첫번째 명령문을 수행합니다. 두번째로, yield메소드가 호출되고 우리의 블록이 수행됩니다. 쓰레드가 임시적으로 블록에게 제어권을 양보합니다. 블록이 종료되면 블록을 호출한 곳으로 제어권이 돌아옵니다. 마지막으로 메소드의 나머지 부분이 수행됩니다. 이것이 배열에 each 메소드를 사용하였을 때 동작하던 방식입니다. 우리가 이 메소드를 어떤 이유에서든지 다시 작성하고 싶어졌다고 합시다. 아마 여러분의 애완동물인 미국너구리가 볼링 선수를 좋아하고, 메소드을 다시 작성하라고 설득했을 것입니다. 블록을 수행하기 위해서 yield를 사용할 수 있습니다.

def myeach(myarray)
  iter = 0
  while (iter < myarray.length):
     yield(myarray[iter])
     iter += 1
  end
end
testarray = [1,2,3,4,5]
myeach(testarray) {|item| print "#{item}:"}
→   1:2:3:4:5:

이 코드 조각에서 특히 while이 있는 줄은 좀 어렵겠지만 이것은 이해할 수 있을 정도로 충분히 간단한 코드 조각이기 때문에 좀 참아주세요. while 블록은 순환을 생성하는데, 이것은 블록 안에서 코드를 여러번 수행한다는 뜻입니다(53 페이지에서 순환에 대하여 더 자세히 알아보실 수 있습니다). 복잡해 보이겠지만 이전과는 같은 개념이 적용됩니다. 메소드는 제어권을 블록에게 넘겨줍니다. 여기서 다른 점은 매개변수를 코드를 반복할 때마다 매번 블록으로 넘겨주었다는 것입니다. 여러분이 작성한 집합체(collection)에서 yield를 사용하면 이와 같이 반복자를 훌륭하게 구현할 수 있습니다.

앰퍼샌드(&) 매개변수 메소드의 마지막 매개변수 앞에 앰퍼샌드를 덧붙이면 메소드로 넘겨진 블록은 정상적으로 매개변수로 넘겨줄 때처럼 Proc이 됩니다. 글쎄요, 완전히 정상적으로 그렇다는 것은 아니고 약간의 수법이 있습니다.

def ampersand(&block)
  block.call
  yield
end
ampersand { print "I'm gettin' called! " }
→   I'm gettin' called! I'm gettin' called!

넘겨준 블록이 Proc이 될 것이라고 했으니 call 메소드를 쓸 수 있습니다만, yield 역시 동작한다는 것에 주목하세요. 이것은 call이나 yield를 각각 다른 경우에서 쓸 수 있는 재미있고 유용한 수법입니다.

1.3. 여러분의 객체는 클래스가 부족합니다!

이전에 여러번 언급했듯이 루비에 있는 모든 것은 객체입니다. 물론 루비는 객체를 묘사하는 클래스라는 것으로부터 객체를 생성해 낼 수 있습니다. C#, C++, 파이썬이나 자바와 같은 객체 지향 언어에서 프로그램을 짜 본 경험이 있으시다면 클래스와 객체의 개념은 생소한 것이 아닐 것입니다. 그러나 그런 언어들의 객체 지향 구현과 루비의 구현은 구별되는 점이 있습니다.

한 가지 생소한 것은 루비는 객체의 타입을 다루는 방법입니다. C++이나 자바는 단지 정적(혹은 명시적)인 타입을 다룹니다. 이런 식으로 타입을 다루려면 객체의 타입은 컴파일 시간에 정의되어야 하고 그렇지 않으면 컴파일러가 에러 메시지를 출력합니다. 대부분의 정적으로 타입을 다루는 대부분의 현대 언어들은 투영(reflection)이나 동적으로 타입을 가져올 수 있는 모듈을 제공합니다. 그러나 루비는 타입을 완전히 다른 방법으로 접근합니다. 파이썬을 사용해 보신 적이 있다면 이런 동적 타입에 익숙할 것입니다. 루비는 같은 아이디어를 이용하지만 "덕 타이핑(duck typing)"이라고 부릅니다(이것이 좀 더 설명하기 쉽습니다).

duck.jpg
[JPG image (95.85 KB)]

그림 6: 각각의 클래스가 sayhello라는 메소드를 구현하고 있고 둘 다 메시지에 반응합니다. yourclasssmells를 추가하고 싶군요.

C++이나 PHP 프로그래머에게 생소할만한(그러나 자바나 C# 프로그래머에게는 생소하지 않을) 다른 개념은 언어 전반에 걸친 클래스 계층입니다. 루비에서는 모든 클래스는 Object 클래스에서 파생된 Class 클래스의 인스턴스인 객체입니다. 다음과 같은 코드로 보여드릴 수 있습니다.

puts File.class
→   Class
puts File.superclass
→   Object
puts Object.superclass
→   nil

클래스의 상위 클래스는 그 클래스가 어디서부터 파생되었는지를 나타내는 것입니다. 루비에서 클래스가 다른 클래스로부터 모든 메소드와 변수를 "상속"받을 수 있습니다. 결과적으로 클래스가 다른 클래스로부터 파생되었다면 (예를 들어 다른 클래스로부터 상속 받았다면), 상위 클래스의 모든 메소드와 변수에 접근할 수 있습니다. 루비의 결함은 다른 몇몇 언어들과 달리 클래스가 한번에 한 클래스에서만 상속받을 수 있다는 것입니다. 예를 들어서 클래스는 다른 클래스로부터 상속받은 다른 클래스로부터 상속받은 클래스로부터 상속받을 수 있지만 하나의 클래스는 여러 클래스로부터 한번에 상속받을 수 없습니다. 보시다시피 File 클래스는 그저 최상위 클래스(아무 것으로부터 상속받지 않은 클래스)인 Object 클래스로부터 상속받은 Class 클래스의 인스턴스입니다. 다른 클래스들처럼 File 모든 다른 Object의 메소드와 변수에 접근할 수 있다는 뜻입니다. 이것은 Object 클래스는 모든 다른 객체들의 근본이라는 뜻이기도 합니다. 아담과 이브, 창조자, 설계자, 모든 객체와 클래스의 어머니입니다!

클래스가 어떤 것의 인스턴스가 된다는 말은 좀 혼란스러울 것입니다. 그러나 루비에 있는 모든 것은 객체라는 점을 명심하세요. 모든 것 말입니다. 그래서 클래스를 정의할 때 Class 클래스의 인스턴스를 만드는 것입니다. 클래스로부터 새로운 객체를 만들 때, 클래스가 묘사하고 있는 새로운 객체 인스턴스를 반환하는 메소드인 (클래스 이름).new를 호출합니다. 루비에 있는 것은 무엇이든지 객체입니다!

1.3.1. 클래스 정의하기

자 이제 시작해 봅시다. 클래스를 정의하려면 class 키워드를 첫 줄에 놓고 클래스에서 상속을 받으려면 < 를 쓰고 상속받으려는 클래스를 씁니다. 예를 들면 다음과 같습니다.

class MyFirstClass < Object
end

이게 전부입니다. 방금 우리는 처음으로 클래스를 정의했습니다. 이 예제는 지금까지 나온 예제 중에서 가장 인위적이고 완전히 쓸모없는 클래스를 나타내는 것임을 인정합니다만, 어쨌든 우리는 클래스를 정의했습니다. Object로부터 상속을 받았다는 것을 나타냈다는 점에 주목하십시오. 이것은 완전히 필요 없는 것입니다. 루비에서는 상속 관계를 정하지 않으면 Object에서 상속 받았다고 가정합니다.

1.3.2. 메소드와 변수

클래스는 변수와 메소드를 포함할 수 있습니다. 클래스에 어떤 일을 시키려면 제일 먼저 집어넣고 싶은 것은 메소드일 것입니다. 추가해야 할 첫 번째 메소드는 initialize 메소드인데, 이것은 (클래스 이름).new 메소드를 호출할 때마다 루비에서 자동으로 호출해주는 메소드입니다. new 메소드를 호출하면 새로운 객체가 생성되고 initialize 메소드가 new에 넘겨준 매개변수를 넘겨주면서 호출되어 객체의 상태(변수, 플래그 등)를 설정합니다. 이것은 다른 언어의 생성자와 매우 비슷(사실상 동일)합니다. 예를 들어 Boogeyman이 세상에 있는 모든 아이들에게 괴상한 짓을 하려고 했던 것을 포기하기로 마음먹고 (이제 나이가 좀 들었으니까요) 대신에 (루비로 짜여진 소프트웨어로 동작하는) 로봇 군단을 만들어서 사악한 명령을 내린다고 합시다. 초기 클래스 정의와 initialize 메소드는 다음과 같을 것입니다.

class Boogeyman
  def initialize
    puts "Yes, master?"
  end
end
monster1 = Boogeyman.new
→   Yes, master?

당연히 이 메소드는 새 객체를 생성할 때 어떻게 하는지를 보여주는 것 뿐이며, initialize 메소드가 호출되었습니다. 이제 무언가 동작하도록 해 봅시다.

class Boogeyman
  def initialize(name, location)
    @name = name
    @location = location
    puts "Yes, master?"
  end
end
monster1 = Boogeyman.new("Mister Creepy", "New York, NY")
→   Yes, master?

이것은 인스턴스 변수를 설정하는 새로운 initialize 메소드입니다. 인스턴스 변수라는 것은 객체 내에서 상태를 가지고 있는 변수입니다. 인스턴스 변수를 설정하기 위해서는 좌변 앞에 @ 기호를 붙여야 합니다. 다른 언어들처럼 이 변수들을 클래스 선언부에 넣을 필요는 없습니다.

이렇게 설정된 변수는 특정 인스턴스에서 유일하게 됩니다. 이 변수들은 인스턴스 스코프 내에 있다고 합니다. 예를 들어서 특별히 넘겨주지 않는다면 인스턴스 바깥에서 이 변수들을 사용할 수 없습니다. 이전에 배웠던 스코프를 기억해 보세요. @name과 name이 다른 스코프 내에 있으므로, 우리는 모호하지 않게 이름을 중복해서 사용할 수 있습니다. (사실 별로 좋은 생각은 아닙니다.) 이제 객체의 상태를 다루는 몇 가지 메소드를 만들어 봅시다.

class Boogeyman
  def change_location(newlocation)
    @location = newlocation
    puts "I moved to #{newlocation}!"
    self.get_info
  end
  def change_name(newname)
    @name = newname
    puts "I shall be called #{newname} from now on!"
    self.get_info
  end
  def get_info
    puts "I am #{@name} in #{@location}."
  end
end
monster1 = Boogeyman.new("Loopy Lou", "Albuquerque, NM")
→   Yes, master?
monster1.change_location("Wyoming")
→   I moved to Wyoming!
→   I am Loopy Lou in Wyoming.
monster1.change_name("Beezlebub")
→   I shall be called Beezlebub from now on!
→   I am Beezlebub in Wyoming.

이 예제에서 중요한 두 가지 개념을 보여드렸습니다. 첫째, 제가 전체 클래스를 다시 다 입력하지 않았다는 것입니다. 제가 게을러서 그런 것이 아니라 루비에서 클래스는 절대로 닫히지 않습니다. 이 말은 간단히 클래스 정의를 다시 열어서 항상 어떤 메소드든지 추가하거나 다시 정의(정확한 용어로 오버라이드)할 수 있다는 것입니다. 때로는 위험할지도 모릅니다만 이것이 루비 객체 구현에서 가장 유용한 측면 중에 하나이기도 합니다. 다음 예제를 보시겠습니다.

class String
  def writesize
    puts self.size
  end
end
size_writer = "Tell me my size!"
size_writer.writesize
→   16

방금 전에도 말씀드렸듯이 클래스의 메소드를 오버라이드 하는 것이 가능합니다. 심지어는 내장 클래스들도 가능합니다. 이것은 특정 메소드나 연산자를 수정해서 모든 것을 엉망으로 만들 수 있는 위험한 점도 있지만 동시에 유용할 수도 있습니다. 루비 웹 프레임워크인 레일스는 이 개념을 특히 ActiveSupport 패키지에서 많이 활용합니다. 좀 더 복잡하고 재미있는 것을 찾으신다면 레일스 확장 기능을 살펴보시면 다양한 클래스들이 있습니다.

이 예제에서 보여드린 두 번째 개념은 self 객체의 사용법입니다. self 객체는 항상 현재의 인스턴스를 가리킵니다. (스트링 클래스의 size나 우리 클래스의 get_info 메소드 같은) 현재 인스턴스의 메소드를 호출할 때 사용할 수 있습니다. 대부분의 경우에는 이것이 필요하지 않을 것이지만 필요한 경우가 있습니다. 기본적으로 self를 생략하고 사용해도 루비는 현재 클래스에 있는 것으로 가정하지만, puts와 같은 메소드를 클래스 내에서 구현했다면 내장 메소드를 호출하지 않고 직접 작성한 클래스 내의 puts 메소드를 호출하려면 이것을 이용하여야 합니다.

1.3.3. 속성


인스턴스 변수가 유용하다고 해도 바깥 세상에는 보이지 않습니다. 처음 보았을 때에는 이것이 매우 훌륭한 상황이라는 생각이 들 겁니다. 객체의 모든 상태가 바깥 세상에서는 완전히 보이지 않고 바뀌지 않을 테니까요. 그러나 조금 지나고 보면 객체 내의 값을 가져오거나 바꾸고 싶다는 생각이 드실 겁니다. 어떻게 이것을 할 수 있을까요?! 이것은 정말 간단하게 됩니다.

class Boogeyman
  def scare_factor
    @scare_factor
  end
  def hiding_place
    @hiding_place
  end
  def scare_factor=(factor)
    @scare_factor = factor
  end
  def hiding_place=(place)
    @hiding_place = place
  end
end
monster1 = Boogeyman.new("Crazy Cal", "Nashville, TN")
monster1.scare_factor = 6000
puts monster1.scare_factor
→   6000

보여드린 예제에서 읽을 수 있는 속성을 만들기 위해서 그저 인스턴스 값을 반환하는 메소드를 만들었습니다. 메소드에서 마지막으로 사용한 값이 반환된다는 것은 기억하시겠죠? 속성은 값을 읽거나 쓰는 간단한 메소드입니다. 값을 설정할 수 있는 메소드를 만드려면 그냥 간단하게 메소드 이름 뒤에 등호 (=)를 붙여주시면 됩니다. 제가 했던 것처럼 바로 인스턴스 변수를 반환하도록 할 수도 있고 제대로 된 형태와 형식을 갖추었는지 검사하여 제대로 된 형식으로 변환하여 변수를 설정할 수도 있습니다. 클래스 내에 값을 써야 하는 것들이 많은 경우에는 좋지 않을 수도 잇씁니다. 제 말은 C#에서 변수를 클래스 외부에서 보이게 하려고 모든 변수에 "public"을 붙이는 것처럼 말입니다! 속성이 일반적으로 많이 사용되는 구성체이기 때문에 루비에는 아주 간단하게 할 수 있는 기능을 갖추고 있습니다.

class Boogeyman
  attr_reader :scare_factor, :hiding_place
  attr_writer :scare_factor, :hiding_place
end

이제 예전처럼 속성을 읽고 쓰고 하실 수 있습니다. 이런 방식으로 메소드를 만들어도 이전에 만들었던 것과 동일한 능력을 보유하고 있습니다. 이 테크닉을 이용하면 더 쉽게 메소드를 쓸 수 있지만 읽거나 쓰는 메소드를 마음대로 정의할 수 있는 유연성이 떨어집니다. 예를 들어서 두려움의 정도를 두려움그램(Fg) 단위로 출력한다고 하면 읽는 메소드를 다음과 같이 작성하여 화면에 출력할 수 있습니다.

class Boogeyman
  attr_writer :scare_factor
  def scare_factor
    return str(@scare_factor / 1000) + "Fg"
  end
end
monster1 = Boogeyman.new("Psycho Sally", "Los Angeles, CA")
monster1.scare_factor = 6000
puts monster1.scare_factor
→   6Fg

어떤 사람들은 이것을 가상 속성이라고 부르지만 제 생각에는 다른 특별한 이름이 필요한 것 같습니다. 이것을 어떻게 부르든 간에 여러분의 클래스의 구현을 숨길 수 있는 멋진 방법입니다. 외부 세계에서는 그냥 보통 속성처럼 보입니다만 여러분은 진실을 알고 있습니다! 이것이 사람이 쉽게 쓸 수 있는 작은 방법입니다.

1.3.4. 접근 제어

우리 메소드와 속성들은 무방비로 열려 있는데, 이제 우리 클래스의 부분에 접근하는 것을 제어할 수 있는 방법을 생각해 봅시다. 지금까지 initialize(이 메소드는 항상 private입니다)를 제외한 모든 메소드들은 public(클래스 내외부 모두에서 접근할 수 있음)이었습니다. 이것이 루비의 기본 동작방식이기 때문에 다음 예제와 같이 메소드를 추가해 봅시다.

class Boogeyman
  def scare(who)
    puts "I just scared the bejezus out of #{who}!"
  end
end

아무런 접근 제어를 명기하지 않았기 때문에 이 메소드는 public입니다. 이것을 보호하려는 메소드 앞에 protected라고 써 줌으로써 protected(그 자신과 상속받은 클래스에서만 접근할 수 있음)로 만들 수도 있습니다. 예제를 보시겠습니다.

class Boogeyman
  protected
    def are_you_a_monster?(whosasking)
      puts "Yes, #{whosasking}, I am a monster!"
      return true
    end
end

이제 이 메소드에 접근할 수 있는 객체는 Boogeyman 클래스의 인스턴스이거나 그 클래스를 상속받은 클래스의 인스턴스 뿐입니다. 이것은 같거나 비슷한 종류의 클래스에게만 정보를 제공하려고 할 때 유용합니다. 한편 현재 객체의 인스턴스에서만 이것을 접근할 수 있게 하려면 private으로 정의해야 합니다. protectedprivate의 차이는 근소합니다. protected는 같은 클래스나 상속받은 클래스의 다른 인스턴스에서 접근할 수 있지만 private은 현재 인스턴스에서만 접근할 수 있습니다. 이제 phone_home이라는 메소드를 Boogeyman에 추가하고 scare 메소드에서 그것을 사용하도록 재정의해 봅시다.

class Boogeyman
  private
    def phone_home(message)
      # TODO: Actually make this phone home
      # For now, we'll just write to the console
      puts message
    end
  public
    def scare(who)
      phone_home("I just scared the living poop out of #{who}!"
    end
end

이제 현재 메소드에서만 phone_home 메소드에 접근할 수 있습니다. 아무나 집에 전화해서 이 괴물처럼 보이게 하고 싶지는 않으시죠? public 키워드 역시 명시적으로 선언하여 같은 방법으로 private 메소드를 정의한 다음에 다시 모드를 바꾸고 싶을 때 사용할 수 있습니다.

1.3.5. 클래스와 스코프 내의 객체

지금까지 인스턴스, 인스턴스 변수, 인스턴스 메소드 등을 다루었지만 클래스의 인스턴스보다 클래스의 상태를 유지할 필요도 있고, 어떤 한 인스턴스에 국한되지 않은 메소드를 제공해야 할 필요가 있을 때도 있습니다. 이럴 때 사용하는 것이 클래스 상수, 변수, 메소드입니다.

클래스 상수는 값이 바뀌는 변수와는 달리 변하지 않는 값을 클래스 스코프 내에 둘 수 있는 손쉬운 메커니즘입니다. 클래스 상수를 만드려면 간단히 상수 이름과 값을 클래스 정의부에 두면 됩니다.

class Boogeyman
  MY_BOSS = 'Mr. Boogeyman'
end

이제 Boogeyman 클래스 내에 있는 모든 메소드(인스턴스와 클래스 스코프 내에 있는 것 모두)는 MY_BOSS 값에 접근할 수 있습니다.

클래스 변수를 만드려면 골뱅이 두 개(@@)를 변수 앞에 붙이면 됩니다. 인스턴스 변수와 거의 같게 동작하지만 특정 객체의 상태가 아니라 클래스의 상태가 됩니다. 예를 들어, Boogeyman이 최근에 풀려난 거류자의 이름과 위치를 알 수 있는지 요청한다면 이것을 클래스 변수로 알려줄 수 있습니다.

class Boogeyman
  # We'll redefine initialize
  def initialize(name, location)
    @name = name
    @location = location
    @@latest = @name
    @@location = @location
    puts "Yes, master?"
  end
end
monster1 = Boogeyman.new("Macabre Mac", "Seattle, WA")
→   Yes, master?
monster2 = Boogeyman.new("Gory Gary", "Reston, WV")
→   Yes, master?
puts Boogeyman.latest
→   Gory Gary

예제에서 보았듯이, 클래스 이름에 점을 찍고 변수 이름을 사용합니다. 클래스 상수처럼 이 값을 클래스 스코프 내에서나 인스턴스 스코프 내의 메소드에서 접근할 수 있스니다. @@location, @location, location은 모두 다른 스코프 내에 있다는 것을 주의하십시오. 아무 문제없이 같은 이름을 쓸 수 있습니다. 세 변수의 이름을 같게 하는 것을 권장하는 것은 아니지만 가능하기는 하고 가끔씩 유용하기도 합니다.

클래스 메소드는 인스턴스에서 특별히 필요하지 않을 것들을 인스턴스가 아니라 클래스에서 제공하는 것입니다. 이런 특징은 예를 들어 미리 만들어 놓은 클래스의 인스턴스를 제공하고 싶을 때 유용합니다. 예를 들어 Person이라는 클래스의 man이라는 이름의 메소드는 성별 필드를 설정하게 할 수도 있습니다. 만일 코드 속으로 파고들거나 스스로 루비 콘솔에서 찾아내기가 너무 귀찮아서(이제 퇴직했거든요) 마지막 로봇의 이름을 출력해주는 클래스 메소드를 작성해달라고 Boogeyman이 요청한다면 다음과 같이 작성할 수 있습니다.

class Boogeyman
  def Boogeyman.latest_monster
    puts "The latest monster is #{@@latest}."
    puts "He is in #{@@location}."
  end
end
Boogeyman.latest_monster
→   The latest monster is Gory Gary.
→   He is in Reston, WV.

latest_monster이 클래스 메소드이기 때문에, 그 스코프 내에 있는 변수들(클래스 변수들)만 접근할 수 있습니다. 매개변수의 형태로 메소드로 넘겨받거나 클래스 변수로 참조받거나 하지 않는 이상 인스턴스 변수에는 접근할 수 없다는 것입니다. 다른 클래스 스코프 내의 변수들, 클래스 메소드들은 인스턴스 스코프 내에 있는 객체와 메소드에서는 보이지 ㅇ낳습니다. 이것은 클래스 메소드를 호출할 때 전체 이름을 다 입력해야 한다는 뜻입니다. 즉, latest_monsterMY_BOSS 혹은 @@latest를 바로 호출할 수 없고 Boogeyman.latest_monster와 같이 호출해야 한다는 것입니다.

1.4. 모듈

아마도 때로는 많은 코드를 체계적으로 정리해야할 때가 있을 겁니다. 많은 코드 말입니다. 중국의 인구처럼 많은 코드 말이지요. 코드가 한 클래스 안에 들어가거나 완벽하게 연계되어 있지는 않을 것입니다. 아마도 몇가지 이슈가 있습니다. 아마도 지난 주에 grumpy_butt.rb에 있던 인자 때문에 화가 나 있을 겁니다. 어쨌든 여러분은 그것을 서로 모아서 다시 사용할 수 있게 체계적으로 정리하는 일을 맡았습니다. 보통 C나 PHP에서는 간단히 파일 내에 코드를 집어 넣고 필요한 곳에서 포함(include)할 것입니다. 그러나 이름이 같은 메소드나 상수가 있다면 어떻게 되겠습니까? 팬더 격투 게임을 만들고 있다고 합시다. 게임 내에서 죽을 수 있는 회수인, 전체 목숨의 수를 나타내는 DEATH라는 상수를 const_values.rb에 두었는데 fighter_values.rb 파일에 Death라는 이름의 플레이어의 값들을 배열로 갖고 있는 DEATH라는 상수가 또 있습니다. 두 파일이 모두 포함되어야 하는데, 이름 충돌이 생기게 됩니다. DEATH_VALUES_BECAUSE_I_CANT_INCLUDE_IT_WITHOUT_A_HUGE_NAME라는 식으로 상수 이름을 짓기보다는 간단한 이름을 짓는 것이 더 나을 것입니다. 이럴 때 모듈을 쓰면 매우 간편합니다. 상수와 메소드를 네임스페이스라는 그룹으로 논리적으로 모을 수 있고, 모호함을 피하고 코드의 논리적인 체계를 잡을 수 있습니다. 네임스페이스는 네임스페이스 바깥에 있는 코드를 건드리는 위험없이 재 사용 가능한 큰 그룹의 코드를 작성할 수 있게 해 줍니다. 이 말은 아까 전과 같은 경우에 FighterValuesConstantValues라는 네임 스페이스에 각각 그 값을 유지할 수 있다는 것입니다.

1.4.1. 모듈 만들기

모듈을 만드는 문법은 클래스를 만드는 문법과 매우 유사합니다. module 키워드를 모듈 이름 앞에 놓고, 그 다음 줄부터 end 키워드가 나올 때까지 모듈에 포함시킬 메소드와 클래스를 작성하면 됩니다. 이전에 예로 들었던 것에 대한 예제를 봅시다.

module FighterValues
  BAMBOO_HEAD = { 'life' => 120, 'hit' => 9 }
  DEATH = { 'life' => 90, 'hit' => 13 }
  KOALA = { 'life' => 100, 'hit' => 10 }
  CHUCK_NORRIS = { 'life' => 60000, 'hit' => 99999999 }
  def chuck_fact
    puts "Chuck Norris' tears can cure cancer..."
    puts "Too bad he never cries."
  end
end
module ConstantValues
  DEATH = -5   # Pandas can live PAST DEATH.
  EASY_HANDICAP = 10
  MEDIUM_HANDICAP = 25
  HARD_HANDICAP = 50
end
puts FighterValues::DEATH
→   {'life'=>90,'hit'=>13}
puts ConstantValues::DEATH
→   -5

이제 두 값이 친근한 환경에서 충돌없이 공존할 수 있게 되었습니다. 아마 "그냥 클래스를 사용하면 되지 않나요?"라고 생각하실 것입니다. 처음에 이 구조를 보았을 때 저도 같은 생각을 했습니다. 저는 그 때 단지 설계와 올바른 소프트웨어 공학을 위하여, 서로 같은 클래스에 넣을 만큼 연관성이 크지 않은 것들을 하나로 묶고 싶을 때, 모듈이라는 것이 이런 규칙을 어기지 않으면서 그렇게 할 수 있는 좋은 변명이라고 생각했습니다.

그러나 저는 더 훌륭한 면이 있다는 것을 알았습니다.

모듈은 혼합(mixin)이라는 메커니즘이 있어서 마치 원래 있던 코드인 것처럼 클래스 내로 혼합되어 들어갈 수 있습니다. 더도 말고 상속이라고 생각해 보세요. 이전에 살펴 보았듯이, 루비의 클래스는 한 번에 한 클래스로부터 상속받을 수 있습니다. 다른 클래스로부터 상속을 받아서 상속 사슬 같은 것을 이용하여야 "다중 상속"과 비슷한 것을 할 수 있습니다. 실제로는 아니지만 루비에서 할 수 있는 가장 근접한 방법입니다. 혼합은 그런 것을 할 필요없이 할 수 있습니다. 다른 클래스로부터 상속받는 클래스를 만들고 필요한 여러 모듈을 혼합해 넣을 수 있습니다. 특히 혼합하려고 하는 것이 혼합하는 것 이외에 다른 용도로 쓰이지 않는 경우에 특히 유용한 특징입니다. 예제를 봅시다.

module Movement
  def run
    puts "I'm running!"
  end
  def walk
    puts "I'm walking a bit briskly!"
  end
  def crawl
    puts "I'm so slowwww!"
  end
end
class Man
  include Movement
  def jump
    puts "I'm bipedal and I can jump like a fool!"
  end
end
class Sloth
  include Movement
  def flop
    puts "It's all a lie...all I can do is flop around."
  end
end
mister_man = Man.new
mister_man.run
→   I'm running!
mister_slothy = Sloth.new
mister_slothy.flop
→   It's all a lie...all I can do is flop around.

보시다시피 이것은 혼합할 코드의 전부를 사용할 수 있기 때문에 상속과 유사한 메커니즘입니다. 혼합하려면 간단히 모듈을 정의하고 include 키워드를 쓰고 모듈 이름을 쓰면 됩니다. 저는 분명히 모듈이라고 했습니다. include 키워드는 PHP나 C++처럼 다른 파일이나 라이브러리를 포함하는 키워드가 아닙니다. 이제부터 이 클래스는 모듈 내에 있는 모든 상수와 메소드에 접근할 수 있게 됩니다. 이 예제가 이 메커니즘의 정당성(예를 들어서 모듈 상수가 혼합되어 들어가는 것의 유용성을 보여주지는 못하고 있고 혼합되어 들어간 클래스에 별다른 영향을 끼치지 못하고 있습니다)을 보여주지 못한다는 것은 분명하지만 이것은 여러분께서 읽고 더 깊이 실험해 보시라는 뜻에서 만든 입문용 코드입니다. 나중에 배우겠지만 클래스와 혼합이 실제로 상호작용하면서 실제로 마법이 일어납니다. Singleton이나 Comparable과 같은 루비에 내장된 혼합체들은 여러분께서 작성하신 클래스와 몇몇 레일스 혼합체를 대단하게 확장할 것입니다. 그러나 그것은 이 주제에 대한 고급 주제입니다.

이제 이것이 아주 훌륭하다고 생각하시겠지만 조심하셔야 할 것이 있습니다. 혼합체를 잘 작성한 경우에는 이것이 멋지지만 개발자가 주의를 기울이지 않고 이름을 붙이는데 주의를 기울이지 않는다면 프로그램에 큰 혼란이 생기게 됩니다. 예를 들어서 파이값을 소수점 72번째 자리까지 갖고 있는 PI라는 상수가 있다고 합시다. 이것은 이 정도이 정밀도가 필요하여 직접 입력한 것입니다. 그런데 이것을 빌리 맥두퍼스씨가 작성한 삼각함수 라이브러리에 혼합하였는데 이 라이브러리에서는 소수점 5째 자리까지 구한 파이값을 PI라는 상수에 담아 두었다고 합시다. 여러분은 72번째 자리까지 정밀하게 구해야 하지만 바보같은 빌리 맥두퍼스 씨 덕분에 이 상수가 우선하게 될 수도 있습니다. 모듈 이름이나 본인 이름을 앞에 집어 넣어서 이름 지은 것을 유일하게 하셔서 다른 코드를 건드리지 않게 해야 합니다.

이제 이렇게 생각하실 것입니다. "저 멍청이가 모듈 안에 메소드를 넣어 뒀는데 메소드의 경우에는 어떤가요!? 전 그의 고양이를 죽이고 제 돈을 돌려받고 싶은데요! 제 메소드가 마찬가지로 저런 지독한 운명을 겪게 될까요?" 이런 격렬한 감정 갖겠지만 마음을 누그러뜨리시고, 좋은 친구여, 메소드는 같은 운명을 겪지 않을 겁니다. 메소드를 호출하면 루비는 처음에 원래 클래스에 있던 메소드를 먼저 봅니다. 이것이 없으면 혼합체를 보고, 없으면 상위 클래스와 그 혼합체를 봅니다. 상수의 동작 방식과는 완전히 반대입니다. 그리고 저는 왜 이런지 모르겠습니다. 이것은 축복일 수도 있고, 저주일 수도 있습니다. 여러분께서 작성하신 메소드를 대체하고 싶을 수도 있을 것입니다. 하지만 일반적으로 이것이 가장 안전한 상관 관계이빈다.

1.5. 파일

프로그램이 점점 커지면 5MB짜리 큰 파일 하나 안에 모든 코드를 다 집어넣고 싶지는 않으실 것입니다. 코드를 파일로 나누어서 담는 것은 코드를 나누는 가장 오래되고 쉬운 방법 중에 하나입니다. 이것을 가장 마지막에 다루는 이유는 코드를 나누는 방법에 대한 여러분의 대답이 "파일로 나눠서 집어 넣어!"라는 것이 아니길 바랬기 때문입니다. 루비에는 더 많고 더 좋고 적합한 방법이 있습니다. 제 생각엔 PHP 프로그래머들이 특히 이런식으로 여기 저기서 상습적으로 코드를 포함시킬 것 같습니다. 삭제 명령과 같은 벌채용 칼이 있어야 돌아다닐 수 있는 파일의 정글을 만들겠지요. 주제에서 벗어났네요. 루비에서 파일을 포함시키려면 두 가지 방법이 있습니다. load와 이것의 우아한 사촌인 require입니다. 둘의 차이점이라고 하면 load 키워드는 소스 코드 파일을 무조건 포함시키고 require는 오직 한 번만 포함시킵니다. 즉, require를 하면 변수와 메소드가 겹치지 않습니다.

load "libraries/myfile.rb"
require "/home/myaccount/code/libraries/myotherfile.rb"

각각의 키워드는 상대 경로와 절대 경로 모두를 받습니다. 루비가 상대 경로로 인식한다면 현재 디렉토리(궁금하신 분들을 위하여: 이것은 $:에 저장되어 있습니다.)에서 찾을 것입니다. require 명령은 조건문이나 순환, 그 외 다른 구성체에서도 사용할 수 있고 변수를 이용하여 경로를 지정(이것은 load로는 불가능합니다)할 수도 있습니다. 포함시킨 파일의 지역 변수는 포함한 파일의 문맥으로 새어나오지 않습니다. 이것은 PHP, C/C++의 동작과는 다릅니다. 이 변수들은 작성시의 문맥에 묶여버리게 됩니다.

1.6. 이 장에서는

코드를 더 논리적이고 사용하기 쉬운 조각으로 나누는 법을 배웠습니다. 이런 것을 배웠습니다.
  • 코드를 블록, 메소드, 클래스, 모듈, 파일을 통하여 나누는 방법
  • 변수 스코프의 동작과 이점
  • 클래스와 객체를 직접 작성하고, 다른 클래스와 객체를 변경하는 방법
  • 클래스와 코드를 섞고 클래스를 확장시킬 수 있는 모듈

ID
Password
Join
Be careful how you get yourself involved with persons or situations that can't bear inspection.


sponsored by andamiro
sponsored by cdnetworks
sponsored by HP

Valid XHTML 1.0! Valid CSS! powered by MoniWiki
last modified 2008-11-04 15:38:13
Processing time 0.0244 sec