Mixin iN RUBY

HyoKwangRyu·2021년 6월 26일
2

Ruby on Rails

목록 보기
5/6

Mixin이란?

간단히 말하자면 특정 코드를 다른 클래스에 포함 시킬 수 있도록 하는 개념이다.

믹스인은 상속과는 달리 엄격한 단일 상속관계를 만들지 않고, 원하는 기능만 하위로 전달할 수 있다. (개념상)하위 클래스는 믹스인한 부모 모듈의 모든 기능을 사용할 수있지만, 상속처럼 부모 자식관계를 가지지 않는다.

상속은 상위 클래스의 속성, 기능을 모두 가지고있지만,
믹스인에서는 모듈이 사용하는 특정 필드나 특정 기능들만 사용하고, 그것이 있다는 전제하에 사용해야한다.

예를들어 테이블을 생각해보자. 테이블에는 식탁도 있고 책상도 있다.
테이블 이라는 개념에서 공통된 기능들이 있을텐데 이를 테이블 클래스로 잘 정의하고 이를 상속받아 식탁, 책상 클래스를 만들 수 있다.

그런데 이후 스탠딩 책상이 생겼다면? 접이식 테이블이 생겼다면?
접이식 책상 클래스에는 접는 기능을 정의해야하고, 스탠딩 책상 클래스에는 높이 조절기능을 따로 정의해야 할 것이다.

테이블 클래스는 점점 추상화가 어려워 지고 기능이 고도화 될 수록 머리는 아파진다.

상속과 달리 믹스인은 제한적인 기능만을 클래스에 포함시킨다.
테이블 클래스를 만들때는 책상 자체를 추상화 하고, 하위 클래스의 기능을 각 클래스에 따로 정의해야했다.

이번에는 책상의 기능들에 집중해 보자.

먼저 '접을 수 있는' 기능에 대한 모듈을 만든다면 그 안에 접는 기능에 대한 정의만 있으면 된다. 그리고 접이식 책상 클래스에 믹스인 한다.
'접이식 스탠딩 책상' 이 생긴다면, '접이식 기능' 모듈과, '높낮이 조절기능' 모듈을 믹스인 하면 될 것이다.

중요한 것은 이렇게 믹스인을 할때, 테이블은 상판과 다리가 있다는 전제를 갖추고 있다는 것이다.
전제를 갖추고 있다면, 테이블 뿐 아니라 의자 클래스에도 믹스인 할 수 있다.

Mixin IN RUBY

Module

메서드와 상수를 모아놓은 것. namespace로 메서드 상수를 분리하는 역할을 할 수 있다.
클래스처럼 인스턴스를 생성할 수 없다. 대신, 다른 클래스에서 믹스인 하여 쓸 수 있다.

Ancestors

클래스 생성시 ancestors 배열에 부모들을 저장해 둔다. 여기에는 상속받은 클래스들과 자기자신, 믹스인한 모듈들도 포함된다.

irb(main):001:0> Array.ancestors
=> [Array, Enumerable, Object, Kernel, BasicObject]

클래스의 인스턴스 메서드를 호출하면 ancestors의 앞에서부터 돌며 해당 메서드를 찾는다. 첫 클래스에서 찾지 못하면 다음 부모에서 찾는다.

Mixin

루비의 릭스인은 include, prepend, extend 세가지를 이용할 수 있다.

include

include는 모듈의 메서드를 그대로 사용한다. 그리고 ancestors에서의 위치가 중요한데, 만약 상속받은 클래스가 있고 모듈을 include했다면
모듈이 ancestors배열 상에서 부모클래스 앞에 위치한다.
-> 모듈이 부모클래스보다 우선순위를 갖는다.
-> 메서드 호출 시 메서드를 원 클래스에서 찾고, 모듈에서 찾고, 부모클래스에서 찾는다.

module Module1
  def a
    "module1"
  end
end

class Class1
  def a
    "class1"
  end
end

class MyClass < Class1
  include Module1
end

irb(main):002:0> MyClass.ancestors
=> [MyClass, Module1, Class1, Object, Kernel, BasicObject]
irb(main):003:0> MyClass.new.a
=> "module1"

prepend

include와 유사하지만 약간 다르다.
먼저 ancestors배열을 확인해 보자.

module Module1
  def a(numbers)
    p "module1: #{numbers}"
    result = super
    p "module1: #{result}"
  end
end

module Module2
  def a(numbers)
    result = super
    p "module2: #{result}"
  end
end

class MyClass
  prepend Module1
  prepend Module2

  def a(numbers)
    numbers.sum
  end
end


irb(main):001:0> MyClass.ancestors
=> [Module2, Module1, MyClass, Object, Kernel, BasicObject]

prepend 키워드의 이름 처럼 prepend된 모듈은 ancestors배열의 앞에 prepend된다.
메서드가 호출되면, 마지막으로 prepend된 모듈부터 메서드를 찾고 실행할 것이다. 때문에 super키워드를 사용하면 기존의 메서드의 앞, 뒤에 원하는 동작을 추가할 수 있다.

irb(main):002:0> MyClass.new.a([1,2])
"module1: [1, 2]"
"module1: 3"
"module2: module1: 3"
=> "module2: module1: 3"

extend

extend 위 두 키워드와는 달리 모듈의 메서드를 클래스 메서드로 사용할 수 있게한다.(include, prepend는 클래스의 인스턴스 메서드를 확장함)

module Module1
  def a
    "module1"
  end
end

class MyClass
  extend MyModule
end

irb(main):001:0> MyClass.ancestors
=> [MyClass, Object, Kernel, BasicObject]
irb(main):002:0> MyClass.new.a
NoMethodError (undefined method `a' for #<MyClass:0x00007fca7c20f7a8>)
irb(main):003:0> MyClass.a
=> "module1"
irb(main):004:0> MyClass.singleton_class
=> #<Class:MyClass>
irb(main):005:0> MyClass.singleton_class.ancestors
=> [#<Class:MyClass>, Module1, #<Class:Object>, #<Class:BasicObject>, Class, Module, Object, Kernel, BasicObject]

위처럼 extend는 인스턴스 메서드가 아닌, 클래스 메서드를 확장한다.(클래스 메서드는 싱글톤 클래스 안에 정의되고, 모듈을 extend하면 싱글톤 클래스가 확장된다)

Prerequisite

위에서 얘기했던 모듈 가구 얘기를 다시 해 보자.
다리가 있다는 전제하에, 다리의 특정 기능을 결정하는 모듈을 믹스인 할 수 있다.

코드를 먼저 보자.

module Dividable
  def divide
    @a / @b
  end
end

module Multipliable
  def multiply
    @a * @b
  end
end

class Calc
  include Dividable
  include Multipliable

  attr_accessor :a, :b

  def initialize
    @a = 4
    @b = 2
  end

  def sum
    @a + @b
  end
end

irb(main):001:0> Calc.new.divide
=> 2
irb(main):002:0> Calc.new.multiply
=> 8

모듈 내부에는 속성이 선언되어 있지 않지만, 클래스 내의 인스턴스 메서드 처럼 각 속성에 접근 가능하다.
이걸 이용해 모듈을 잘 만들어 놓으면, 다른 클래스에서도 그 모듈의 기능이 필요할때 믹스인하여 이용할 수 있다. 단, 모듈에서 접근하는 속성들이 있다는 전제 하에.

가구얘기로 다시 와 보면,
책상, 침대, 의자 등 다리가 있는 모든 클래스들은 필요하다면 Detachable이라는 모듈을 믹스인하여 각 클래스 내부에 기능을 정의하지 않더라도 다리를 떼었다 붙였다 할 수 있게된다.

Mixin iN RAILS

언제

간단한 예를 생각해 보자.
보통 모델을 만들때 ActiveRecord::Base를 상속받아 클래스를 생성한다.
때문에 모든 모델이 동시에 가지고 있는 특징이 있다.
속성으로 id, created_at, updated_at를 갖고, ActiveRecord::Base의 많은 기능들을 갖는다는 것이다.
때문에 created_at(or id) 를 가지고 필터링하는 스코프를 다루는 모듈을 만들어 필요한 모델에서 믹스인해 사용할 수도 있겠다.

좀 더 생각해 보자.
특정 모델에서 변경 사항이 있을때, 슬랙으로 항상 노티를 줘야한다고 하자.
SlackNotifiable이라는 모듈에 해당 기능을 만들고 필요한 모델 클래스가 include 하면 되것 같다.

이것을 상속으로 해결해 본다면? 음..

iN RAILS

ActiveRecord::Base
의 코드를 확인해 보면

module ActiveRecord
  class Base
    extend ActiveModel::Naming

    extend ActiveSupport::Benchmarkable
    extend ActiveSupport::DescendantsTracker

    extend ConnectionHandling
    extend QueryCache::ClassMethods
    extend Querying
    extend Translation
    extend DynamicMatchers
    extend DelegatedType
    extend Explain
    extend Enum
    extend Delegation::DelegateCache
    extend Aggregations::ClassMethods

    include Core
    include Persistence
    include ReadonlyAttributes
    include ModelSchema
    include Inheritance
    include Scoping
    include Sanitization
    include AttributeAssignment
    include ActiveModel::Conversion
    include Integration
    include Validations
    include CounterCache
    include Attributes
    include Locking::Optimistic
    include Locking::Pessimistic
    include AttributeMethods
    include Callbacks
    include Timestamp
    include Associations
    include ActiveModel::SecurePassword
    include AutosaveAssociation
    include NestedAttributes
    include Transactions
    include TouchLater
    include NoTouching
    include Reflection
    include Serialization
    include Store
    include SecureToken
    include SignedId
    include Suppressor
    include Encryption::EncryptableRecord
  end

  ActiveSupport.run_load_hooks(:active_record, Base)
end

레일즈도 클래스 내부에 기능을 직접 정의하지 않는다.
여러 모듈을 믹스인하여 쓰고 있다.

마무리

루비, 레일즈에서 드라이한 코드나 젬 내부 구현들에 관심을 갖다보면 결국 믹스인에 도달한다.
스위프트의 프로토콜은 전제 조건을 강력하게 지키도록 하고 내부 구현은 자유롭게(각각 다르게) 구현할 수 있게 하는데 반해(인터페이스 맞추기),
루비의 믹스인은 메서드 자체를 공유하여 좀더 dry하게 코드를 작성하는데에 목적이 있는 듯 하다.

믹스인을 통해 개발의 재미를 조금 북돋았으나, 항상 적절한 상황에서, 적당히 쓰도록 신경써야 겠다.

그리고 관련 부분을 계속 생각하다보니 몇 가지 개인적인 결론이 나왔다.
결론은 언제나 바뀔 수 있지만..

  1. 모듈은 되도록 작은 크기의 단일 기능으로 정의 하자.
  2. 상속은 데이터(속성들), 모듈을 행동에 집중하자.

다음엔 레일즈에서 Activerecord::Concern을 이용한 믹스인에 대해 알아볼 예정이다.

profile
Backend Developer

0개의 댓글