본문 바로가기
Programming/Ruby On Rails

[Ruby On Rails] Module Mixin (2) - ActiveSupport::Concern

by 가론노미 2023. 4. 30.

2023.04.18 - [Programming/Ruby On Rails] - [Ruby On Rails] Module Mixin

 

[Ruby On Rails] Module Mixin

Ruby는 다중 상속을 지원하지 않는다. 즉 Ruby 클래스는 하나의 부모 클래스(슈퍼 클래스)만 가질 수 있다는 뜻이다. Ruby에서는 다중 상속을 모듈의 Mixin을 통해 구현이 가능하다. Mixin에 대해 알아

garonnome.tistory.com

이전에 Module Mixin에 기본적인 개념을 살펴보았다.

 

모듈을 사용하다 보면, 모듈을 하나의 클래스에 동시에 extend와 include를 하고 싶은 경우가 생기기도 한다.

 

아래와 같이 클래스 내에서 두 번 Mixin 하는 것이 문법적으로는 가능하지만,

의도와는 다르게 모듈의 메서드들이 클래스 메서드가 되는 동시에 인스턴스 메서드가 되어버린다.

module MyModule
  def included_method
    "included"
  end
  
  def extended_method
    "extended"
  end
end

class MyClass
  include MyModule
  extend MyModule
end

 

이런 경우 모듈이 mixin 될 때 호출되는 hook을 이용하여 해결할 수 있다.

hook에는 included, prepended, extended 가 있다.

 

로그 모듈을 Record 클래스에 동시에 extend, include 하고자 한다.

module Loggable
  def self.included(base)
    base.extend ClassMethods
    base.class_eval do
    	attr_accessor :enable_log
    end
  end

  module ClassMethods
    def loggable_list
      all.select { |item| item.enable_log? }
    end
  end
  
  def loggable?
    enable_log == true
  end
end

class Record < ApplicationRecord
  include Loggable
end

Record 클래스가 Loggable을 include 함으로써 다음 세 가지가 가능해진다.

  • Record.new.loggable?: 모듈이 include 되어 인스턴스 메서드가 확장된다.
  • Record.loggable_list: Record 클래스가::ClassMethods를 extend하여 클래스 메서드가 확장된다.
  • Record.new.enable_log: class_eval을 통해 Record 클래스의 컨텍스트에서 블록이 실행되고, Record 모델에 enable_log라는 인스턴스 변수에 대한 getter 및 setter 메서드를 정의한다.

Ruby는 클래스나 객체를 즉석에서 수정할 수 있는 기능을 제공하는데, 여기서 사용한 class_eval이 그 역할이다.

class_eval 블록 내부의 코드는 마치 base 클래스 내부에 작성된 것처럼 동작한다.

 

Rails 4.0부터는 위 코드와 같은 패턴을 ActiveSerport::Concern을 통해 간편하게 사용할 수 있다.

 

ActiveSerport::Concern 이란?

ActiveSerport::Concern는 모듈을 확장하는 모듈이다.

여러 클래스에 대한 공통 논리를 재사용 가능한 별도의 모듈로 분리할 수 있다.

 

Concern이라는 이름은 AOP(Aspect Oriented Programming)에서 유래했다.

AOP의 관심사는 "기능의 응집력 있는 영역"을 캡슐화 하는 것이다.

 

즉, ActiveSupport::Concern은 클래스 자체 및 클래스의 싱글톤 클래스의 ancestors(조상 목록)을 변경하고, included hook을 통해 대상 클래스를 직접 조작함으로써 대상 클래스의 동작을 확장할 수 있는 메카닉을 제공한다.

 

ActiveSerport::Concern을 extend한 모듈에서는 다음 두 블록을 사용할 수 있다.

  • included
    • self.included(base)를 대체한다.
    • 이 블록 내부 코드를 해당 모듈을 include한 클래스에 자동으로 class_eval 해주기 때문에, before_action이나 has_many, scope 등 여러 유용한 hook이나 association을 재사용하기 쉬워진다.
  • class_methods
    • base.extend ClassMethods를 대체한다.
    • 이 블록 안에서 정의된 메서드는 해당 모듈을 include한 클래스의 클래스 메서드로 확장된다.
module Loggable
  extend ActiveSupport::Concern
  
  included do
    attr_accessor :enable_log
  end

  class_methods do
    def loggable_list
      all.select { |item| item.enable_log? }
    end
  end

  def loggable?
    enable_log == true
  end
end

class Record < ApplicationRecord
  include Loggable
end

ActiveSerport::Concern은 패턴을 간편하게 줄여주는 것 뿐만 아니라 모듈간 의존성 문제도 해결해준다.

 

ActiveSerport::Concern을 사용하지 않은 예시를 먼저 알아보자.

 

MyModule이 include 될 때 연결관계를 추가하려고 한다.

그런데 MyModule에 메소드를 더 추가하고 싶어 MyModule을 include한 ExtendedModule을 만들었다.

module MyModule
  def self.included(base) 
    base.class_eval do 
      has_many :something
    end 
  end 
end 

module ExtendedModule
  include MyModule
  
  def another_method
  end
end 

class MyClass 
  include ExtendedModule 
end

MyClass에 ExtendedModule을 include 했을 때 정상적으로 동작할까?

# NoMethodError (undefined method `has_many' for MyModule:Module)

 

MyModule의 included hook에 들어온 base MyClass가 아닌 ExtendedModule이 되는데, ExtendedModule에는 has_many가 정의되어 있지 않기 때문에 에러가 발생한다.

 

ActiveSerport::Concern을 사용하면 모듈이 ActiveSerport::Concern가 포함되지 않은 클래스 및 모듈에 포함되기 전까지 hook을 지연 실행시켜 의존성 문제를 해결할 수 있다.

module MyModule
  extend ActiveSupport::Concern

  included do
    has_many :something
  end
end

module ExtendedModule
  extend ActiveSupport::Concern
  include MyModule
end

class MyClass < ApplicationRecord
  include ExtendedModule
end

Concern을 통해 MyModule의 included 블록 내부 코드는 MyClass에 추가되어 의도했던 대로 실행이 가능해졌다.

 

 

<참고>

https://www.akshaykhot.com/how-rails-concerns-work-and-how-to-use-them/

https://engineering.appfolio.com/2013/06/17/ruby-mixins-activesupportconcern/

https://spilist.github.io/2019/01/17/ruby-mixin-concern