Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

루비(Ruby) inherited 훅 메서드와 클래스 상속 제어 방법

안녕하세요, 자식들과 부모님 여러분... 아니, 루비스트(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 훅 메서드를 사용하면 상속하는 클래스와 상속되는 클래스를 즉석에서 다룰 수 있어 매우 유용하다는 점도 확인했습니다.

다만 실무에서 상속을 다룰 때는 항상 조심하세요. 기존 메서드나 클래스를 삭제하거나 오버라이드할 때는 신중해야 하며, 그렇지 않으면 예상치 못한 부작용이 생길 수 있습니다. 자식들이 아빠라고 불러주기를 거부하는 최악의 상황도 생길 수 있으니까요.

자, 오늘 포스팅은 여기까지 입니다!