안녕하세요, 자식들과 부모님 여러분... 아니, 루비스트(Rubyist) 여러분! 이전 글에서는 조상 체인(ancestry chain)을 파헤쳐 봤는데요. 오늘은 상속이라는 '육아' 주제를 한층 더 깊이 들어가 보겠습니다. inherited 훅 메서드를 자세히 살펴보고, 나아가 상속 자체를 막는 방법까지 알아봅니다.
자식 클래스가 상속될 때: inherited 훅 메서드
먼저 부모 관계, 즉 상속이 선언되는 순간부터 살펴보겠습니다. 루비는 어떤 클래스가 다른 클래스의 부모로 선언될 때, 해당 부모 클래스와 상호작용할 수 있는 깔끔한 방법을 제공합니다.
class Parent
def self.inherited(subclass)
puts "#{subclass} inherits from Parent"
end
end
class Child < Parent
end이 코드를 실행하면 'Child inherits from Parent'가 출력됩니다. Child 클래스가 Parent를 상속하는 순간 Parent.inherited 메서드가 자동으로 호출되기 때문입니다. 눈여겨볼 점은 이 메서드가 서브클래스를 인자로 받는다는 것입니다. 위 예제에서는 Child가 해당됩니다.
이 메커니즘을 활용하면 '누군가 상속했을 때만' 동작하는 로직을 부모 클래스에 심어줄 수 있습니다. 여기서 말하는 동작이란 상속하는 클래스 또는 상속되는 클래스의 변수, 메서드, 상수를 정의·수정·삭제하는 작업 전반을 의미합니다.
그럼 이번에는 parent_name 메서드를 즉석에서 정의해 보겠습니다.
class Parent
def self.inherited(subclass)
subclass.define_method :parent_name do
"Daddy"
end
end
end
class Child < Parent
end
Child.new.parent_name # => "Daddy"위 예제에서는 Child 클래스가 Parent를 상속하는 시점에 메서드를 정의합니다. 중요한 것은 이 메서드를 Parent 클래스에 직접 추가하지 않았다는 점입니다. 오직 다른 클래스가 Parent를 상속할 때만 정의되는 것이죠.
이론은 충분히 다뤘으니, 이제 실제 루비스트의 현업에서 만날 법한 좀 더 현실적인 예제를 살펴보겠습니다.
클래스 상속 막기
Ruby on Rails에서 데이터베이스 마이그레이션은 ActiveRecord::Migration 클래스가 담당합니다. 이 클래스를 직접 상속하면 어떻게 될까요?
class AddFirstnameToUsers < ActiveRecord::Migration
end
# => StandardError (Directly inheriting from ActiveRecord::Migration is not supported..)Rails가 이 클래스의 직접 상속을 차단하는 메커니즘을 갖추고 있기 때문에 에러가 발생합니다. 그렇다면 Rails는 대체 왜 이런 장치를 만들어 두었을까요?
핵심은 마이그레이션이 특정 Rails 버전과 강하게 결합되어 있다는 점입니다. 실제로 이 클래스가 제공하는 API는 버전 사이에서 미세하게 바뀔 수 있습니다. Rails를 업그레이드할 때 기존 마이그레이션이 깨지는 사고를 막기 위해, 프레임워크는 반드시 특정 버전의 ActiveRecord::Migration 클래스를 지정하도록 강제합니다. 이 덕분에 마이그레이션이 언제나 안정적으로 실행됩니다.
class AddFirstnameToUsers < ActiveRecord::Migration[4.2]
end위 예제에서 우리 마이그레이션은 Rails 4.2에서 제공하는 ActiveRecord::Migration API에 결합되어 있습니다. 따라서 애플리케이션을 5.0으로 업그레이드하더라도 마이그레이션은 계속 4.2 버전의 API로 실행되기 때문에 아무 문제 없이 동작합니다.
상속 방지는 어떻게 동작할까?
왜 상속을 막는지 이해했으니, 이제 어떻게 막는지 살펴보겠습니다. 비밀은 모두 ActiveRecord::Migration.inherited 메서드 안에 있습니다.
AddFirstnameToUsers 클래스가 ActiveRecord::Migration을 상속하는 순간, ActiveRecord::Migration.inherited 훅 메서드가 호출됩니다.
module ActiveRecord
class Migration
def self.inherited(subclass) #:nodoc:
super
if subclass.superclass == Migration
raise StandardError, "Directly inheriting from ActiveRecord::Migration is not supported. " \
"Please specify the Rails release the migration was written for:\n" \
"\n" \
" class #{subclass} < ActiveRecord::Migration[4.2]"
end
end
end
end보시다시피 이 훅 메서드는 서브클래스(AddFirstnameToUsers)가 ActiveRecord::Migration을 직접 상속하는지 검사하고, 그럴 경우 에러를 발생시킵니다. 상속을 통제하기에는 이만한 진입점이 없습니다.
함께 알아두면 좋은 훅 메서드들
inherited 외에도 루비는 유사한 훅 메서드들을 제공합니다. 모듈이 include될 때 호출되는 Module#included, extend될 때 호출되는 Module#extended, prepend될 때 호출되는 Module#prepended 등이 대표적입니다. 이런 훅들을 잘 조합하면 플러그인이나 DSL을 설계할 때 강력한 유연성을 얻을 수 있습니다.
마치며
오늘은 상속의 기본기와 상속을 막는 방법을 함께 살펴봤습니다. inherited 훅 메서드를 사용하면 상속하는 클래스와 상속되는 클래스를 즉석에서 다룰 수 있어 매우 유용하다는 점도 확인했습니다.
다만 실무에서 상속을 다룰 때는 항상 조심하세요. 기존 메서드나 클래스를 삭제하거나 오버라이드할 때는 신중해야 하며, 그렇지 않으면 예상치 못한 부작용이 생길 수 있습니다. 자식들이 아빠라고 불러주기를 거부하는 최악의 상황도 생길 수 있으니까요.
자, 오늘 포스팅은 여기까지 입니다!