지난 글 '메서드 기능 확장하기5' 에서는 위임을 사용해서 Fibonacci 클래스의 기능을 확장시켜 보았다.
아래 get 메서드의 코드를 다시 보면 새로 추가하려는 기능인 '실행 시간 측정' 관련 작업은 직접 처리하고 있지만, 피보나치 수를 계산하는 본연의 작업은 다른 객체에게 위임하고 있는 걸 볼 수 있다.
require './fibonacci'
class TimeMeasurableFibonacci < Fibonacci
def initialize(fib)
@fib = fib
end
def get(n)
start_time = Time.now
result = @fib.get(n)
puts "Time: #{Time.now - start_time}"
result
end
end
오늘은 클래스가 기능을 확장시키거나 클래스 자신의 구현을 위해 위임을 사용하는 경우를 살펴보려고 한다.
(그런데, TimeMeasurableFibonacci 클래스도 어쨌든 구현을 위해 위임을 사용하고 있긴 하다.)
먼저 '스피드 연산 게임 만들기' 글에서 작성했던 Game 클래스의 코드를 다시 한번 살펴보자.
exam 메서드는 사용자에게 문제 하나를 보여주고 답을 입력 받아 채점까지 하는 역할을 맡고 있는데, 문제와 정답 관련 데이터는 make_quiz 메서드 호출을 통해 가져온다.
그리고 make_quiz 메서드를 보면 어떠한 다른 추가적인 작업 없이 단순히 @quiz 객체(quiz getter 메서드가 반환)에게 모든 걸 맡기고 있다.
require 'timeout'
class Game
attr_accessor :quiz, :time
def initialize(quiz = Gugudan.new, time = 10)
@quiz = quiz
@time = time
@quiz_cnt = 0
@correct_cnt = 0
end
def make_quiz
quiz.make
end
def exam
question, answer = make_quiz
print question
@quiz_cnt += 1
input = STDIN.gets
if answer == input.to_i
@correct_cnt += 1
puts "정답"
else
puts "오답"
end
end
def play
@quiz_cnt = 0
@correct_cnt = 0
begin
Timeout.timeout(time) do
while true
exam
end
end
rescue
end
puts "\n총 #{@quiz_cnt}문제 중 #{@correct_cnt}문제를 맞췄습니다."
end
private :make_quiz, :exam
end
위의 코드를 다시 보면 make_quiz 메서드는 private으로 지정했기 때문에 Game 클래스의 객체를 사용하는 곳에서는 make_quiz 메서드를 직접 접근할 수는 없다.
즉, Game 클래스가 단순히 외부에 제공하는 기능을 늘리기 위해서 위임을 사용한 게 아니라 자신이 실제 책임져야할 기능(play) 구현을 위해 위임을 사용하는 것이다.
그리고 (어차피 private이므로) make_quiz 메서드를 따로 정의하지 않고 exam 메서드 안에서 바로 @quiz 객체를 통해 문제와 답을 생성해도 되지만, exam 메서드가 문제와 답을 어떻게 생성하는지 모르게 하기 위해 make_quiz 메서드를 통하도록 한 것이다.
지금처럼 Game 클래스 안에서 문제와 답을 돌려주는 역할을 맡고 있는 make_quiz 메서드가 있다면, 추후 문제와 답 생성을 위해 어떠한 작업이 필요해지는 상황이 오더라도 exam 메서드는 그것을 전혀 신경 쓸 필요가 없다.
그것은 make_quiz 메서드가 책임지고 알아서 할 일이므로 단지 make_quiz 메서드만 수정하면 되는 것이다.
현재 Game 클래스의 코드 안에서 '문제와 답'을 생성해서 사용하는 곳은 exam 메서드 하나뿐이지만, 만약 여러 곳에서 '문제와 답'을 생성해서 사용하거나 앞으로 늘어날 가능성이 있다면 별도의 메서드로 역할과 책임을 나누는 것은 더욱 중요하다.
다음으로 위임을 사용해서 클래스가 외부에 제공하는 메서드의 수를 늘려 기능을 확장시키는 경우를 살펴보자.
아래 코드처럼 간단한 사칙 연산 기능을 정의한 Calculator 클래스가 있다고 했을 때, 여기에 피보나치 수를 계산해 주는 fib 메서드를 추가하고 싶다고 해보자.
class Calculator
def add(a, b)
a + b
end
def sub(a, b)
a - b
end
def mul(a, b)
a * b
end
def div(a, b)
a / b.to_f
end
end
그냥 Calculator 클래스에 필요한 기능을 직접 구현해 넣을 수도 있겠지만, 이미 해당 기능을 구현한 클래스를 만들어 놓았으므로 중복해서 만들 필요 없이 코드를 재사용하는 것이 현명한 방법이다.
그래서 피보나치 관련 클래스의 코드를 재사용할 수 있도록 아래처럼 Calculator 클래스의 코드를 수정하였다.
class Calculator
def initialize(fib)
@fib = fib
end
def add(a, b)
a + b
end
def sub(a, b)
a - b
end
def mul(a, b)
a * b
end
def div(a, b)
a / b.to_f
end
def fib(n)
@fib.get(n)
end
end
수정한 코드가 잘 동작하는지 테스트를 먼저 진행해 보자.
D:/blog/ruby 폴더 아래 delegation 이라는 폴더를 먼저 만들고, 그 안에 D:/blog/ruby/extend 폴더에 있는 fibonacci.rb 파일과 memoizable_fibonacci.rb 파일을 복사해 오자.
그리고 위의 Calculator 클래스의 코드 역시 D:/blog/ruby/delegation 폴더에 calculator.rb 파일로 저장하자.
이제 테스트할 준비를 마쳤으니 D:/blog/ruby/delegation 폴더에서 irb를 실행하여 아래 그림처럼 입력하고 결과를 확인해 보자.
Calculator 클래스에서 직접 구현한 사칙연산 메서드들뿐만 아니라 위임을 사용해 만든 fib 메서드도 잘 동작하는 걸 볼 수 있다.
그리고 Calculator 객체 생성 시 MemoizableFibonacci 클래스의 객체를 넘겨주면 메모이제이션 기능으로 피보나치 수를 빠르게 계산해 주는 것도 볼 수 있다.
그런데, 이렇게 특정 객체에 단순히 위임하는 방법으로 클래스의 기능을 확장하려고 할 때, 추가해서 만들어야 할 메서드의 수가 많다면 동일한 패턴의 반복적인 코드가 늘어나 클래스의 코드가 괜히 복잡해진다.
아래 코드를 보면 Foo 클래스에서 직접 로직을 구현한 메서드는 foo 메서드 하나뿐이고 나머지 메서드들은 단순히 Bar 클래스의 객체에 위임을 하도록 만들었다.
class Foo
def initialize(bar)
@bar = bar
end
def foo
puts "작업 처리를 위한 로직을 직접 구현했다"
end
def aaa
@bar.aaa
end
def bbb
@bar.bbb
end
def ccc
@bar.ccc
end
end
생각해 보면, 그냥 보통의 클래스를 구현할 때도 (필요하다면) 속성에 대한 getter와 setter를 위해 동일한 패턴의 반복적인 코드를 작성해야 한다.
그런데 이러한 작업을 attr_reader, attr_writer, attr_accessor를 사용하면 쉽게 할 수 있는 것처럼, 단순한 위임의 경우에도 Forwardable 모듈을 사용하면 쉽게 해결이 가능하다.
아래 그림을 보면 Foo 클래스 안에서 Forwardable 모듈을 extend로 믹스인한 후 def_delegators 메서드를 사용하여 aaa, bbb, ccc 세 메서드를 @bar 인스턴스 변수의 객체에게 위임하도록 했다.
프로그램을 간단히 하기 위해 Bar 클래스의 aaa, bbb, ccc 메서드는 모두 attr_reader를 사용하여 정의했고 해당 인스턴스 변수를 초기화하지 않았으므로 호출 시 nil을 반환해야 한다.
실제 Foo 클래스의 객체를 생성하여 aaa, bbb, ccc 세 메서드를 호출해 보면 이상없이 모두 잘 호출된다.
그리고 젤 아래 Foo.instance_methods의 호출 결과를 보면 Foo 클래스의 인스턴스 메서드 목록에 aaa, bbb, ccc 메서드가 포함되어 있는 게 보인다.
그러면, Calculator 클래스의 코드를 Forwardable 모듈을 사용하도록 수정한 후 다시 테스트를 진행해 보자.
앞선 예제에서 사용한 def_delegators 메서드는 한 번에 여러 메서드에 대한 위임을 설정할 수 있지만, 위임 대상 객체의 메서드 이름을 그대로 사용해야 한다.
그에 반해 아래 def_delegator 메서드는 한 번에 하나의 메서드에 대해서만 위임을 설정할 수 있지만, 메서드 이름을 다르게 지정할 수 있다.
Calculator 클래스에서는 해당 객체에 대한 fib 메서드 호출을 @fib 객체의 get 메서드 호출로 전달(forward)하도록 설정하였다.
class Calculator
extend Forwardable
def_delegator :@fib, :get, :fib
def initialize(fib)
@fib = fib
end
def add(a, b)
a + b
end
def sub(a, b)
a - b
end
def mul(a, b)
a * b
end
def div(a, b)
a / b.to_f
end
end
Calculator 객체에 대해 fib 메서드 호출이 정상적으로 잘 되며, Calculator 클래스의 인스턴스 메서드 목록에도 fib 메서드가 포함되어 있는 것을 확인할 수 있다.
See you again~~