이번 글에서는 객체지향 프로그래밍에서 전통적인 방법으로 사용되는 상속을 통해 Fibonacci 클래스의 기능을 확장시켜보자.
Fibonacci 클래스의 get 메서드를 메모이제이션과 시간 측정 그리고 로깅이 가능하도록 기능을 확장시키기 위해 추가되는 기능별로 상속을 사용하는 것이 좀 지나칠 수도 있겠지만, 우선 진행을 해보도록 하겠다.
이러한 예제를 통해 상속을 언제 어떻게 사용하는 것이 더 적절하게 잘 사용하는 것인지도 배울 수 있을 것이기 때문이다.
아래 Fibonacci 클래스의 코드를 다시 가져왔다. 따지고보면, Fibonacci 클래스에서 정의한 'get' 인스턴스 메서드가 어떤 속성에 따라 결과가 달라지는 것도 아니기 때문에 '인스턴스 메서드'가 아니라 '클래스 메서드'로 정의하는 것이 더 맞을 수도 있다. 하지만 상속을 사용한 기능 확장 예제를 위해 아래 코드 그대로 진행하겠다.
class Fibonacci
def get(n)
return n if n <= 1
get(n - 2) + get(n - 1)
end
end
우선, get 메서드에 메모이제이션 기능을 추가하기 위해 아래 코드처럼 Fibonacci 클래스를 상속하여 MemoizableFibonacci 클래스를 정의해보자.
새로 만들 클래스 이름은 적당한 이름이 떠오르지 않아서, 그냥 이전 글에서 만들었던 모듈 이름을 앞에 붙이는 걸로 하였다.
get 메서드의 코드를 보면 이전 글의 Memoizable 모듈에서 define_method로 재정의하는 부분과 거의 비슷하다.
Memoizable 모듈의 코드에서는 별칭을 통해 원래의 메서드를 호출하였지만 여기서는 super 키워드를 사용하여 부모 클래스인 Fibonacci의 get 메서드를 호출하도록 변경하였다.
인수 없이 super 키워드를 사용하면 super를 호출하는 메서드(여기서는 get)가 받은 인수 그대로를 상위 클래스(여기서는 Fibonacci)의 get 메서드 호출 시 전달하게 된다.
실제 내부 동작 방식은 다르겠지만, super나 별칭 모두 현재 재정의하는 메서드의 이전 버전을 실행하기 위해 사용한 것은 동일하다.
require './fibonacci'
class MemoizableFibonacci < Fibonacci
def initialize
@memo = {}
end
def get(n)
@memo[n] ||= super
end
end
테스트를 위해 MemoizableFibonacci 클래스의 코드를 D:/blog/ruby/extend 폴더 아래 memoizable_fibonacci.rb 파일에 저장하고, 같은 폴더에서 irb를 실행하여 아래 그림처럼 입력하고 결과를 확인해 보자.
기존 Fibonacci 클래스의 객체를 사용하여 get(40)을 실행하면 5초 이상 걸리지만, MemoizableFibonacci 클래스의 객체를 사용하면 상당히 빨리 결과가 나오는 것을 볼 수 있다.
젤 아래 코드 MemoizableFibonacci.ancestors의 결과를 보면 MemoizableFibonacci 클래스의 바로 위 부모로 Fibonacci 클래스가 자리하고 있는 것을 볼 수 있다.
그럼, 이제는 아래 코드처럼 메모이제이션 기능에 이어 '시간 측정' 기능까지 추가되도록 MemoizableFibonacci 클래스를 상속하여 TimeMeasurableMemoizableFibonacci 클래스를 정의해 보자.
지금도 클래스 이름이 많이 길지만 아직 '로깅 기능' 하나가 더 남아 있다는 걸 기억하자--;
아래 코드를 보면 super 키워드를 사용한다는 것만 제외하면 TimeMeasurable 모듈에서 define_method로 재정의하는 부분과 동일함을 알 수 있다.
이전 글에서 별칭과 메서드 재정의를 통해 메서드에 두 가지 이상의 기능을 추가시킬 때, 이후에 만드는 별칭이 이전 별칭을 덮어쓰지 않도록 조심했어야 했다.
그런데, 상속을 사용할 때는 그런 걱정 없이 그냥 super 키워드를 사용하면 되는데, 이것은 이미 상속을 통해 super 키워드가 어느 클래스의 인스턴스 메서드를 호출할지가 정해졌기 때문이다.
물론, 상속을 받은 클래스가 include를 통해 어떤 모듈을 인클루드하게 되면 super 키워드는 부모 클래스가 아닌 인클루드 한 모듈에서 해당 인스턴스 메서드를 먼저 찾아 호출하게 된다.
인클루드를 통한 믹스인과 관련된 좀 더 자세한 설명은 '클래스와 모듈' 글을 참고하길 바란다.
require './memoizable_fibonacci'
class TimeMeasurableMemoizableFibonacci < MemoizableFibonacci
def get(n)
start_time = Time.now
result = super
puts "Time: #{Time.now - start_time}"
result
end
end
이제 TimeMeasurableMemoizableFibonacci 클래스를 테스트해 보자.
코드를 D:/blog/ruby/extend 폴더 아래 time_measurable_memoizable_fibonacci.rb 파일에 저장하고, 역시 같은 폴더에서 irb를 실행하여 아래 그림처럼 입력하고 결과를 확인해 보자.
MemoizableFibonacci 클래스를 상속받았으니 당연히 결과는 빠르게 나올 것이고, 다만 출력되는 양이 너무 많은데 이것은 '로깅' 기능까지 추가한 후에 다시 살펴 보도록 하자.
마지막으로, '로깅' 기능 추가를 위해 TimeMeasurableMemoizableFibonacci 클래스를 또다시 상속받아 그 이름도 긴 LoggableTimeMeasurableMemoizableFibonacci 클래스를 만들어 보자.
아래 코드 역시 Loggable 모듈에서 define_method로 재정의하는 부분과 거의 비슷한 것을 알 수 있다.
따로 설명이 필요할 만한 코드는 없으니 바로 테스트를 진행해 보자.
class LoggableTimeMeasurableMemoizableFibonacci < TimeMeasurableMemoizableFibonacci
def get(n)
result = super
puts "#{self.class}#get(#{n}) => #{result}"
result
end
end
코드를 D:/blog/ruby/extend 폴더 아래 loggable_time_measurable_memoizable_fibonacci.rb 파일에 저장하고, 역시 같은 폴더에서 irb를 실행하여 아래 그림처럼 입력하고 결과를 확인해 보자.
이전 글에서도 Loggable 모듈을 사용하여 '로깅' 기능을 추가한 후에 실행 흐름과 테스트를 통해 출력된 내용을 비교해 보았었는데, 이번에 상속을 사용하여 다시 구현해 보았으니 한번 더 실행 흐름을 따라가 보면서 출력된 내용을 확인해 보도록 하자.
LoggableTimeMeasurableMemoizableFibonacci 클래스의 객체 fib에 대해 get(5)를 호출하면 먼저 super를 통해 부모 클래스인 TimeMeasurableMemoizableFibonacci 클래스의 get 메서드를 호출하게 되고,
다시 그 안에서 super를 통해 MemoizableFibonacci 클래스의 get 메서드를 호출하게 된다. 그리고 아직 @memo 해시에 5에 대한 값은 저장되어 있지 않으므로 결국, super를 통해 Fibonacci 클래스의 get 메서드까지 호출이 이어지게 된다.
Fibonacci 클래스의 get 메서드를 보면 인수의 값이 1보다 크면 재귀 호출을 하도록 되어 있는데, 이때 재귀 호출은 현재 객체(즉 self가 가리키는 객체)에 대해 호출이 되는 것이므로 fib 변수가 참조하는 객체에 대해 get 메서드가 호출이 된다.
다시말해 Fibonacci 클래스의 인스턴스 메서드인 get이 아니라 LoggableTimeMeasurableMemoizableFibonacci 클래스의 인스턴스 메서드인 get이 재귀 호출되는 것이다!
어쨌든, get(3)이 재귀 호출 되면 다시 super를 따라 Fibonacci 클래스의 get 메서드가 호출되는데, 여기서 get(3 - 2) + get(3 - 1)의 실행에 따라 다시 fib 객체에 대해 get(1)이 재귀 호출되고, 이 호출 역시 super를 따라 Fibonacci 클래스의 get 메서드 호출까지 이어지지만, 또다른 재귀 호출 없이 결괏값을 바로 반환해 준다. 그래서 출력된 내용을 보면 get(1)의 호출 정보(로깅)가 젤 먼저 나오는 것이다.
그 다음부터는 천천히 직접 따라가 보길 바란다. 화면에 출력되는 순서는 이전 글에서 Fibonacci 클래스에 Memoizable, TimeMeasurable, Loggable 모듈을 모두 적용했을 때와 동일함을 알 수 있다.
지금까지 만든 세 개의 클래스 이름만 보더라도, 이런 식으로 상속을 사용하지 말아야겠다는 생각이 들지도 모르겠다.
그러나, 클래스 이름은 좀 더 고민해 보면 명확하면서도 조금은 더 짧은 이름을 찾을 수도 있을 것이다.
이런식의 상속이 적절하지 않은 더 큰 이유 중 하나는 앞서 만들었던 세 개의 클래스말고도 다른 기능 조합이 필요할 경우 그때마다 클래스를 새로 만들어야 한다는 것이다.
class Fibonacci
...
end
# 메모이제이션
class MemoizableFibonacci < Fibonacci
...
end
# 시간 측정
class TimeMeasurableFibonacci < Fibonacci
...
end
# 로깅
class LoggableFibonacci < Fibonacci
...
end
# 메모이제이션 + 시간 측정
class TimeMeasurableMemoizableFibonacci < MemoizableFibonacci
...
end
# 메모이제이션 + 로깅
class LoggableMemoizableFibonacci < MemoizableFibonacci
...
end
# 시간 측정 + 로깅
class LoggableTimeMeasurableFibonacci < TimeMeasurableFibonacci
...
end
# 메모이제이션 + 시간 측정 + 로깅
class LoggableTimeMeasurableMemoizableFibonacci < TimeMeasurableMemoizableFibonacci
...
end
만약 조합되는 기능들의 순서가 중요하다면 가능한 조합은 더 많아지고 그만큼 필요한 클래스의 개수도 더 늘어나게 될 것이다.
상속은 단순히 특정 기능을 확장시키기 위해서라기보다는, 부모 클래스의 몇몇 메서드를 구현 또는 재정의함으로써 어떠한 역할을 담당할 새로운 객체 타입을 만들어 내는데 의미가 있다고 생각된다.
'스피드 연산 게임 만들기7' 글을 보면, 메뉴 이름(속성)과 메뉴 선택 시 처리(메서드)를 Menu 클래스로 정의하였고, Menu 클래스를 상속하여 상위 메뉴 이동을 위한 ExitMenu 클래스와 하위 메뉴를 포함하는 메뉴를 위한 MenuList 클래스를 만들었다.
'스피드 연산 게임 만들기7' 글을 참고하면 상속의 쓰임새를 이해하는 데 조금은 도움이 될 것 같다.
다음 글에서는 특정 기능을 확장시키기 위한 목적에 더 적합한 방법인 '위임'을 사용하여 피보나치 예제를 풀어나가 보겠다.
See you again~~