본문 바로가기
카테고리 없음

메서드 기능 확장하기5

by 경자꿈사 2024. 10. 23.

이번 글에서는 위임(delegation)을 사용해서 Fibonacci 클래스의 기능을 확장해 보자.
이미 '위임'의 뜻을 알고 있겠지만, 이전 글에서 만들었던 dic.rb 프로그램을 사용해서 '위임'을 검색해 보면 아래와 같이 나온다.

객체 지향 프로그래밍에서 말하는 '위임'도 어떠한 객체가 자신의 일(메서드 호출 시의 처리)을 다른 객체가 처리하도록 맡기는 것으로서 그 의미가 다르지 않다.

 

그리고 이미 이전에 '스피드 연산 게임 만들기' 글의 Game 클래스에서 문제 생성을 위해 위임을 사용해본 경험이 있다. 
아래 Game 클래스의 exam 메서드를 보면 make_quiz 메서드를 호출하여 문제와 답을 돌려받고 있는데, make_quiz 메서드는 그 작업을 단순히 @quiz 인스턴스 변수가 참조하는 객체에 대해 make 메서드를 호출하여 처리하고 있는 걸 볼 수 있다.

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
  
  ...생략
end


그러면 먼저, 위임을 통해 Fibonacci 클래스의 get 메서드에 메모이제이션 기능을 추가해 보자.

참고를 위해 Fibonacci 클래스의 코드도 다시 가져왔다.

class Fibonacci
  def get(n)
    return n if n <= 1
    get(n - 2) + get(n - 1)
  end
end

 

아래 코드를 보면 Fibonacci 클래스를 상속받아 만드는 것은 동일하지만, 객체를 생성할 때 Fibonacci 클래스의 객체를 하나 받도록 되어 있다.
하지만 엄밀히 말하면 타입 검사가 없으므로 그 객체가 반드시 Fibonacci 클래스의 객체일 필요는 없고, 단지 인수를 하나 받을 수 있는 get 메서드를 갖고 있는 객체이기만 하면 된다.
그리고 get 메서드의 코드를 보면 이전 글에서 만들었던 MemoizableFibonacci 클래스의 get 메서드에서는 super를 통해 부모 클래스(Fibonacci)의 get 메서드를 호출하도록 하였는데, 여기서는 객체 생성 시 인수로 받아 인스턴스 변수 @fib에 할당해 놓았던 객체 대해 get 메서드를 호출하고 있다.

앞에서 잠깐 봤던 Game 클래스의 경우와는 다르게 추가 기능(메모이제이션)을 위한 작업이 아닌 본연의 작업(피보나치 수 계산) 처리를 위해 위임을 사용하고 있다.

require './fibonacci'

class MemoizableFibonacci < Fibonacci
  attr_reader :memo

  def initialize(fib)
    @fib = fib
    @memo = {}
  end

  def get(n)
    @memo[n] ||= @fib.get(n)
  end
end

테스트를 위해 D:/blog/ruby/extend 폴더 아래 delegation 폴더를 새로 만들고 delegation 폴더 아래 기존 fibonacci.rb 파일을 복사해 넣자.
그리고 앞의 MemoizableFibonacci 클래스의 코드도 delegation 폴더 아래 memoizable_fibonacci.rb 파일에 저장하자.
이제 delegation 폴더에서 irb 를 실행한 후 아래 코드를 입력하고 결과를 확인해 보자.

MemoizableFibonacci 클래스의 객체를 사용해도 전혀 속도가 향상되지 않고 결괏값이 느리게 나오는 것을 볼 수 있다.
Fibonacci 클래스에서 정의한 get 메서드는 재귀 호출 구조로 되어 있고, 그 재귀 호출은 현재 객체(self)에 대해 다시 get 메서드를 호출하는 것이므로 @fib가 참조하는 객체(여기서는 Fibonacci 클래스의 객체)에 대해 직접 get 메서드를 호출하게 되면, @fib가 참조하는 객체가 self가 되므로 재귀 호출 역시 @fib가 참조하는 객체를 대상으로 이루어진다.
그래서 메모이제이션 기능이 제대로 동작하지 않는 것이다. 실제 젤 아래 fib.memo의 결괏값을 보면 @memo 해시에 40에 대한 값만 저장되어 있는 걸 볼 수 있다.
이에 반해 이전 글의 코드에서처럼 super를 통해 부모 클래스(여기서는 Fibonacci 클래스)의 get 메서드를 호출하게 되면, self는 MemoizableFibonacci 객체 그대로 유지되므로 재귀 호출 역시 MemoizableFibonacci 객체를 대상으로 이루어진다.
따라서, 재귀 호출이 포함된 메서드를 위임을 사용해서 기능을 확장시키려고 할 때에는, 재귀 호출이 원래의 메서드를 그대로 호출해도 상관없는지(또는 그게 맞는지)를 생각해 봐야 한다.
메모이제이션이 제대로 동작하기 위해서는 재귀 호출이 원래의 메서드가 아닌 메모이제이션 기능을 추가하여 재정의한 메서드를 호출해야 하므로, 위임이 아닌 상속만을 사용해 구현했던 MemoizableFibonacci 클래스의 코드를 그대로 유지해야 할 것 같다.
D:/blog/ruby/extend/memoizable_fibonacci.rb 파일을 delegation 폴더에 다시 복사해 넣자.

이제 아래 코드처럼 '시간 측정' 기능을 추가해 주는 TimeMeasurableFibonacci 클래스를 작성해 보자.
객체 생성 시 받은 인수를 @fib 인스턴스 변수에 할당해 놓고 get 메서드가 호출되었을때, '시간 측정'과 관련된 작업을 제외한 나머지 처리는 @fib 객체에 대해 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 클래스의 코드를 D:/blog/ruby/extend/delegation 폴더 아래 time_measurable_fibonacci.rb 파일로 저장한 후 irb를 실행하여 아래 그림처럼 테스트를 진행해 보자.
time_fib 변수에 할당한 TimeMeasurableFibonacci 클래스의 객체는 생성 시 Fibonacci 클래스의 객체를 인수로 주었기 때문에 time_fib.get(40)을 실행했을 때 시간이 4초 이상 걸렸고 그 시간이 잘 출력되었다.
그에 반해 time_memo_fib 변수에는 MemoizableFibonacci 객체를 인수로 주고 생성한 TimeMeasurableFibonacci 클래스의 객체를 할당했기 때문에 동일하게 get(40)을 실행했을 때 메모이제이션 기능으로 빠르게 결괏값이 나오는 것을 볼 수 있다. 물론, 메서드 실행에 걸린 시간도 잘 출력된다. 

이전 글에서 단순히 상속만을 사용해서 만든 TimeMeasurableMemoizableFibonacci 클래스를 테스트했을 때는 get 메서드 호출 시 '소요 시간'이 여러 줄에 걸쳐 출력됐었는데, 

재귀 호출 시 TimeMeasurableMemoizableFibonacci 클래스의 get 메서드가 호출되었기 때문이다.
위임을 사용해 만든 TimeMeasurableFibonacci 클래스의 get 메서드는 실제 실행에 소요된 전체 시간을 딱 한 번만 출력해 주기 때문에 어떤 면에서는 더 좋을 수도 있을 것 같다.
그리고 위의 테스트 코드를 보면 위임을 사용하면 기능을 조합하기가 쉽다는 걸 알 수 있는데, 실제  TimeMeasurableFibonacci 객체 생성 시 어떤 객체를 인수로 주냐에 따라 get 메서드의 동작 방식이 달라지는 걸 볼 수 있다.

이제 '로깅' 기능을 추가할 수 있도록 LoggableFibonacci 클래스를 만들고, 여러 기능 조합으로 테스트를 진행해 보자.
아래 위임을 사용해 만든 LoggableFibonacci 클래스의 코드가 있는데, 이전 글에서 만든  LoggableTimeMeasurableMemoizableFibonacci 클래스의 코드와 큰 차이가 없다. 
다만 super를 사용하지 않고 @fib 인스턴스 변수가 참조하는 객체에 처리를 위임한다는 것이 중요한 차이점이다.

require './fibonacci'

class LoggableFibonacci < Fibonacci
  def initialize(fib)
    @fib = fib
  end

  def get(n)
    result = @fib.get(n)
    puts "#{self.class}#get(#{n}) => #{result}"
    result
  end
end

테스트를 위해 LoggableFibonacci 클래스의 코드를 D:/blog/ruby/extend/delegation 폴더 아래 loggable_fibonacci.rb 파일로 저장한 후 irb를 실행하여 아래 그림처럼 테스트를 진행해 보자.

앞의 그림을 보면 Fibonacci 클래스의 객체 하나와 MemoizableFibonacci 클래스의 객체 하나를 미리 생성해서 변수에 담아 놓고, 그것을 사용해서 '로깅', '로깅 + 메모이제이션', '로깅 + 시간 측정', '로깅 + 시간 측정 + 메모이제이션' 등의 
기능 조합이 되도록 LoggableFibonacci 객체를 생성하여 테스트한 것을 볼 수 있다.
TimeMeasurableFibonacci 클래스에서와 마찬가지로 LoggableFibonacci 클래스의 get 메서드가 재귀 호출되는 것이 아니므로 메서드 호출 정보는 한 번만 출력된다.
만약, '시간 측정'보다 '로깅'을 먼저 하고 싶다면 젤 아래 코드처럼 순서를 바꿔 LoggableFibonacci 객체를 인수로 주고 TimeMeasurableFibonacci 객체를 생성하기만 하면 된다.
결과를 보면 원하는 대로 메서드 호출 정보가 먼저 출력된 후에 소요 시간이 출력된 것을 볼 수 있다.
끝으로, LoggableFibonacci 클래스와 TimeMeasurableFibonacci 클래스 모두 Fibonacci 클래스를 상속받고 있는데, Fibonacci 클래스로부터 실제 상속받아 사용하는 메서드도 없고 Fibonacci 클래스의 하위 클래스인지를 검사하는 부분도 없으므로 굳이 Fibonacci 클래스를 상속받지 않아도 프로그램은 문제없이 잘 돌아간다.
하지만, 메서드를 상속받아 사용해야 하거나 Java와 같이 타입을 중요하게 다루는 언어에서 다형성을 위해서는 이런 경우 Fibonacci 클래스를 상속받아 구현해야 한다.

다형성은 객체 지향 프로그래밍(OOP)의 중요한 특징 중 하나로서, 같은 메서드 호출이라도 대상 객체에 따라 다르게 동작하는 것을 말한다. 즉, feb 변수에 할당된 객체가 Fibonacci 클래스의 객체인지 아니면 MemoizableFibonacci 클래스의 객체인지에 따라 get 메서드의 호출이 다르게 동작한다는 것이다. 동일한 코드지만 다르게 동작할 수 있다는 것은 프로그램을 좀 더 유연하게 만들 수 있게 해주는 큰 장점이다. 말 그대로 소프트웨어가 소프트하도록 해준다.

 

그런데, 프로그래밍 언어에 따라 같은 변수에 할당할 수 있는 객체의 타입에 제한이 있을 수 있다.

Ruby와 같은 동적 타입의 언어라면 변수 feb에 어떠한 클래스의 객체라도 할당할 수 있고, 실제 그 객체가 get메서드 호출에 응할 수 있는지를 (그 객체가 어떤 클래스의 객체 인지보다) 중요하게 생각하지만, Java와 같은 정적 타입의 언어에서는 변수 feb에 할당할 수 있는 객체의 클래스 타입이 정해져 있다. 만약 변수 feb를 Fibonacci 클래스 타입으로 선언했다면 변수 feb에는 Fibonacci 클래스 또는 그 하위 클래스의 객체만 할당할 수 있다.

따라서, Java에서 Fibonacci 타입의 변수를 선언하고 그 변수에 할당된 객체에 따라 호출되는 get 메서드가 다르게 동작(메모이제이션, 시간 측정 + 메모이제이션 등등)하길 원한다면 Fibonacci 클래스를 상속받아 구현해야 한다!


이번 '메서드 기능 확장하기' 글에서는 기존 코드를 변경하지 않고 기능을 추가시키기 위해 제일 먼저, 루비에서 제공하는 alias_method와 define_method를 사용해 보았고, 이어서 객체 지향 프로그래밍(OOP)에서 전통적으로 사용해 오는 방법인 상속과 위임을 통해서도 구현해 보았다.
이것을 기본으로 그때그때 상황에 맞는 가장 적절한 방법을 고민해 보면 좋을 것 같다.
See you again~~