Ruby 메타프로그래밍은 생각보다 멋지다
게시 됨: 2022-03-11메타프로그래밍은 Ruby 닌자만 사용하는 것이며 일반 인간을 위한 것이 아니라는 말을 자주 듣습니다. 그러나 진실은 메타프로그래밍이 전혀 무서운 것이 아니라는 것입니다. 이 블로그 게시물은 이러한 유형의 사고에 도전하고 메타프로그래밍을 일반 Ruby 개발자에게 더 가까이 다가가서 혜택을 얻을 수 있도록 하는 데 도움이 될 것입니다.
메타프로그래밍은 많은 의미를 가질 수 있고 종종 매우 오용될 수 있고 사용법에 관해서는 극단에 이를 수 있으므로 모든 사람이 일상적인 프로그래밍에서 사용할 수 있는 몇 가지 실제 사례를 던지려고 노력할 것입니다.
메타프로그래밍
메타프로그래밍은 런타임에 동적으로 코드를 작성 하는 코드를 작성할 수 있는 기술입니다. 즉, 런타임 중에 메서드와 클래스를 정의할 수 있습니다. 미쳤지? 간단히 말해서, 메타프로그래밍을 사용하여 클래스를 다시 열고 수정하고, 존재하지 않는 메서드를 잡아서 즉석에서 생성하고, 반복을 피하여 DRY 코드를 생성하는 등의 작업을 수행할 수 있습니다.
기초
진지한 메타프로그래밍에 대해 알아보기 전에 기본 사항을 살펴봐야 합니다. 그리고 가장 좋은 방법은 모범을 보이는 것입니다. 하나부터 시작하여 Ruby 메타프로그래밍을 단계별로 이해해 보겠습니다. 이 코드가 무엇을 하는지 짐작할 수 있습니다.
class Developer def self.backend "I am backend developer" end def frontend "I am frontend developer" end end
두 가지 방법으로 클래스를 정의했습니다. 이 클래스의 첫 번째 메서드는 클래스 메서드이고 두 번째 메서드는 인스턴스 메서드입니다. 이것은 Ruby의 기본 사항이지만 이 코드 뒤에는 더 많은 작업을 진행하기 전에 이해해야 하는 훨씬 더 많은 일이 있습니다. Developer
클래스 자체가 실제로는 객체라는 점을 지적할 가치가 있습니다. Ruby에서는 클래스를 포함한 모든 것이 객체입니다. Developer
는 인스턴스이므로 Class
의 인스턴스입니다. Ruby 객체 모델은 다음과 같습니다.
p Developer.class # Class p Class.superclass # Module p Module.superclass # Object p Object.superclass # BasicObject
여기서 이해해야 할 한 가지 중요한 것은 self
의 의미입니다. frontend
메소드는 Developer
클래스의 인스턴스에서 사용할 수 있는 일반 메소드이지만 backend
메소드가 클래스 메소드인 이유는 무엇입니까? Ruby에서 실행되는 모든 코드는 특정 self 에 대해 실행됩니다. Ruby 인터프리터는 코드를 실행할 때 항상 주어진 라인에 대해 self
값을 추적합니다. self
는 항상 어떤 객체를 참조하지만 그 객체는 실행된 코드에 따라 변경될 수 있습니다. 예를 들어, 클래스 정의 내에서 self
는 Class
의 인스턴스인 클래스 자체를 참조합니다.
class Developer p self end # Developer
인스턴스 메서드 내에서 self
는 클래스의 인스턴스를 참조합니다.
class Developer def frontend self end end p Developer.new.frontend # #<Developer:0x2c8a148>
클래스 메서드 내에서 self
는 다음과 같은 방식으로 클래스 자체를 참조합니다(이 기사의 뒷부분에서 더 자세히 설명함).
class Developer def self.backend self end end p Developer.backend # Developer
이것은 괜찮습니다. 그러나 결국 클래스 메소드는 무엇입니까? 이 질문에 답하기 전에 싱글톤 클래스 및 고유 클래스라고도 하는 메타클래스의 존재를 언급해야 합니다. 앞서 정의한 클래스 메소드 frontend
는 Developer
객체의 메타클래스에 정의된 인스턴스 메소드일 뿐입니다! 메타클래스는 본질적으로 Ruby가 클래스 메소드를 보유하기 위해 생성하고 상속 계층 구조에 삽입하는 클래스이므로 클래스에서 생성된 인스턴스를 방해하지 않습니다.
메타클래스
Ruby의 모든 객체에는 자체 메타 클래스가 있습니다. 그것은 어떻게 든 개발자에게 보이지 않지만 거기에 있고 매우 쉽게 사용할 수 있습니다. Developer
클래스는 본질적으로 객체이기 때문에 자체 메타 클래스가 있습니다. 예를 들어 String
클래스의 개체를 만들고 해당 메타클래스를 조작해 보겠습니다.
example = "I'm a string object" def example.something self.upcase end p example.something # I'M A STRING OBJECT
여기서 우리가 한 것은 객체에 싱글톤 메서드를 추가한 something
입니다. 클래스 메서드와 싱글톤 메서드의 차이점은 클래스 메서드는 클래스 개체의 모든 인스턴스에서 사용할 수 있는 반면 싱글톤 메서드는 해당 단일 인스턴스에서만 사용할 수 있다는 것입니다. 클래스 메소드는 널리 사용되는 반면 싱글톤 메소드는 그다지 많이 사용되지 않지만 두 가지 유형의 메소드가 해당 객체의 메타클래스에 추가됩니다.
이전 예제는 다음과 같이 다시 작성할 수 있습니다.
example = "I'm a string object" class << example def something self.upcase end end
구문은 다르지만 효과적으로 동일한 작업을 수행합니다. 이제 Developer
클래스를 만든 이전 예제로 돌아가서 클래스 메서드를 정의하는 몇 가지 다른 구문을 살펴보겠습니다.
class Developer def self.backend "I am backend developer" end end
이것은 거의 모든 사람이 사용하는 기본 정의입니다.
def Developer.backend "I am backend developer" end
이것은 같은 것입니다. 우리는 Developer
에 대한 backend
클래스 메소드를 정의하고 있습니다. 우리는 self
를 사용하지 않았지만 이와 같은 메소드를 효과적으로 정의하면 클래스 메소드가 됩니다.
class Developer class << self def backend "I am backend developer" end end end
다시 말하지만, 우리는 클래스 메서드를 정의하고 있지만 String
개체에 대한 싱글톤 메서드를 정의하는 데 사용한 것과 유사한 구문을 사용합니다. 여기에서 Developer
개체 자체를 참조하는 self
를 사용했음을 알 수 있습니다. 먼저 Developer
클래스를 열어 자체를 Developer
클래스와 동일하게 만듭니다. 다음으로 class << self
를 수행하여 self 를 Developer
의 메타클래스와 동일하게 만듭니다. 그런 다음 Developer
의 메타클래스에 메소드 backend
를 정의합니다.
class << Developer def backend "I am backend developer" end end
이와 같이 블록을 정의함으로써 블록 기간 동안 self
를 Developer
의 메타클래스로 설정합니다. 결과적으로 backend
메소드는 클래스 자체가 아닌 Developer
의 메타클래스에 추가됩니다.
이 메타클래스가 상속 트리에서 어떻게 동작하는지 봅시다.
이전 예에서 보았듯이 메타클래스가 존재한다는 실제 증거는 없습니다. 그러나 이 보이지 않는 클래스의 존재를 보여줄 수 있는 약간의 해킹을 사용할 수 있습니다.
class Object def metaclass_example class << self self end end end
Object
클래스에 인스턴스 메서드를 정의하면(예, 언제든지 클래스를 다시 열 수 있습니다. 이것이 메타프로그래밍의 또 다른 장점입니다), 내부에 Object
개체를 참조하는 self
를 갖게 됩니다. 그런 다음 class << self
구문을 사용하여 현재 객체의 메타클래스를 가리키도록 현재 self 를 변경할 수 있습니다. 현재 개체는 Object
클래스 자체이므로 인스턴스의 메타 클래스가 됩니다. 이 메서드는 이 시점에서 메타클래스 자체인 self
를 반환합니다. 따라서 모든 개체에서 이 인스턴스 메서드를 호출하면 해당 개체의 메타클래스를 얻을 수 있습니다. Developer
클래스를 다시 정의하고 약간의 탐색을 시작하겠습니다.
class Developer def frontend p "inside instance method, self is: " + self.to_s end class << self def backend p "inside class method, self is: " + self.to_s end end end developer = Developer.new developer.frontend # "inside instance method, self is: #<Developer:0x2ced3b8>" Developer.backend # "inside class method, self is: Developer" p "inside metaclass, self is: " + developer.metaclass_example.to_s # "inside metaclass, self is: #<Class:#<Developer:0x2ced3b8>>"
그리고 crescendo의 경우 frontend
가 클래스의 인스턴스 메소드이고 backend
가 메타클래스의 인스턴스 메소드라는 증거를 봅시다.

p developer.class.instance_methods false # [:frontend] p developer.class.metaclass_example.instance_methods false # [:backend]
그러나 메타 클래스를 얻기 위해 실제로 Object
를 다시 열고 이 해킹을 추가할 필요는 없습니다. Ruby에서 제공하는 singleton_class
를 사용할 수 있습니다. 우리가 추가한 metaclass_example
과 동일하지만 이 해킹으로 Ruby가 후드 아래에서 어떻게 작동하는지 실제로 볼 수 있습니다.
p developer.class.singleton_class.instance_methods false # [:backend]
"class_eval" 및 "instance_eval"을 사용하여 메서드 정의
클래스 메소드를 생성하는 또 다른 방법이 있습니다. 바로 instance_eval
을 사용하는 것입니다.
class Developer end Developer.instance_eval do p "instance_eval - self is: " + self.to_s def backend p "inside a method self is: " + self.to_s end end # "instance_eval - self is: Developer" Developer.backend # "inside a method self is: Developer"
이 코드 조각 Ruby 인터프리터는 인스턴스 컨텍스트(이 경우 Developer
개체)에서 평가합니다. 그리고 객체에 메소드를 정의할 때 클래스 메소드나 싱글톤 메소드를 생성합니다. 이 경우 클래스 메서드입니다. 정확히 말하면 클래스 메서드는 단일 메서드이지만 클래스의 단일 메서드이고 나머지는 개체의 단일 메서드입니다.
반면에 class_eval
은 인스턴스 대신 클래스 컨텍스트에서 코드를 평가합니다. 사실상 수업을 재개합니다. 다음은 class_eval
을 사용하여 인스턴스 메서드를 만드는 방법입니다.
Developer.class_eval do p "class_eval - self is: " + self.to_s def frontend p "inside a method self is: " + self.to_s end end # "class_eval - self is: Developer" p developer = Developer.new # #<Developer:0x2c5d640> developer.frontend # "inside a method self is: #<Developer:0x2c5d640>"
요약하자면, class_eval
메소드를 호출하면 self
가 원래 클래스를 참조하도록 변경되고 instance_eval
호출하면 self
가 원래 클래스의 메타클래스를 참조하도록 변경됩니다.
즉석에서 누락된 메서드 정의
메타프로그래밍 퍼즐의 또 다른 조각은 method_missing
입니다. 객체에서 메서드를 호출하면 Ruby는 먼저 클래스로 이동하여 해당 인스턴스 메서드를 찾습니다. 거기에서 메서드를 찾지 못하면 조상 체인을 계속 검색합니다. Ruby가 여전히 메소드를 찾지 못하면 모든 객체가 상속하는 Kernel
의 인스턴스 메소드인 method_missing
이라는 다른 메소드를 호출합니다. Ruby가 결국 누락된 메소드에 대해 이 메소드를 호출할 것이라고 확신하기 때문에 이를 사용하여 몇 가지 트릭을 구현할 수 있습니다.
define_method
는 동적으로 메소드를 생성하는 데 사용할 수 있는 Module
클래스에 정의된 메소드입니다. define_method
를 사용하려면 새 메서드의 이름과 블록의 매개변수가 새 메서드의 매개변수가 되는 블록을 사용하여 호출합니다. def
를 사용하여 메서드를 만드는 것과 define_method
를 사용하는 것의 차이점은 무엇입니까? DRY 코드를 작성하기 위해 method_missing
과 함께 define_method
를 사용할 수 있다는 점을 제외하고는 큰 차이가 없습니다. 정확히 말하면 def
대신 define_method
를 사용하여 클래스를 정의할 때 범위를 조작할 수 있지만 이는 완전히 다른 이야기입니다. 간단한 예를 살펴보겠습니다.
class Developer define_method :frontend do |*my_arg| my_arg.inject(1, :*) end class << self def create_backend singleton_class.send(:define_method, "backend") do "Born from the ashes!" end end end end developer = Developer.new p developer.frontend(2, 5, 10) # => 100 p Developer.backend # undefined method 'backend' for Developer:Class (NoMethodError) Developer.create_backend p Developer.backend # "Born from the ashes!"
이것은 def
를 사용하지 않고 인스턴스 메소드를 생성하기 위해 define_method
가 어떻게 사용되었는지를 보여줍니다. 그러나 우리가 그들과 함께 할 수 있는 것이 훨씬 더 많습니다. 이 코드 조각을 살펴보겠습니다.
class Developer def coding_frontend p "writing frontend" end def coding_backend p "writing backend" end end developer = Developer.new developer.coding_frontend # "writing frontend" developer.coding_backend # "writing backend"
이 코드는 DRY가 아니지만 define_method
를 사용하여 DRY로 만들 수 있습니다.
class Developer ["frontend", "backend"].each do |method| define_method "coding_#{method}" do p "writing " + method.to_s end end end developer = Developer.new developer.coding_frontend # "writing frontend" developer.coding_backend # "writing backend"
훨씬 나아졌지만 여전히 완벽하지는 않습니다. 왜요? 예를 들어 새로운 메소드 coding_debug
를 추가하려면 이 "debug"
를 배열에 넣어야 합니다. 하지만 method_missing
을 사용하여 이 문제를 해결할 수 있습니다.
class Developer def method_missing method, *args, &block return super method, *args, &block unless method.to_s =~ /^coding_\w+/ self.class.send(:define_method, method) do p "writing " + method.to_s.gsub(/^coding_/, '').to_s end self.send method, *args, &block end end developer = Developer.new developer.coding_frontend developer.coding_backend developer.coding_debug
이 코드 조각은 약간 복잡하므로 분해해 보겠습니다. 존재하지 않는 메소드를 호출하면 method_missing
이 실행됩니다. 여기서는 메서드 이름이 "coding_"
으로 시작하는 경우에만 새 메서드를 생성하려고 합니다. 그렇지 않으면 실제로 누락된 메서드를 보고하는 작업을 수행하기 위해 super를 호출합니다. 그리고 우리는 단순히 새로운 메소드를 생성하기 위해 define_method
를 사용하고 있습니다. 그게 다야! 이 코드 조각을 사용하여 "coding_"
으로 시작하는 말 그대로 수천 개의 새로운 메서드를 만들 수 있으며 그 사실이 코드를 DRY로 만듭니다. define_method
는 Module
전용이므로 send
를 사용하여 호출해야 합니다.
마무리
이것은 빙산의 일각에 불과합니다. 루비 제다이가 되기 위한 출발점입니다. 메타프로그래밍의 이러한 빌딩 블록을 마스터하고 그 본질을 진정으로 이해한 후에는 더 복잡한 것으로 진행할 수 있습니다(예: 고유한 DSL(Domain-specific Language) 생성). DSL은 그 자체로 주제이지만 이러한 기본 개념은 고급 주제를 이해하기 위한 전제 조건입니다. Rails에서 가장 많이 사용되는 젬 중 일부는 이러한 방식으로 구축되었으며 RSpec 및 ActiveRecord와 같이 자신도 모르게 DSL을 사용했을 것입니다.
이 기사가 메타프로그래밍을 이해하는 데 한 걸음 더 다가서고 더 효율적으로 코딩하는 데 사용할 수 있는 자체 DSL을 구축하는 데 도움이 되기를 바랍니다.