간단히 말하자면 특정 코드를 다른 클래스에 포함 시킬 수 있도록 하는 개념이다.
믹스인은 상속과는 달리 엄격한 단일 상속관계를 만들지 않고, 원하는 기능만 하위로 전달할 수 있다. (개념상)하위 클래스는 믹스인한 부모 모듈의 모든 기능을 사용할 수있지만, 상속처럼 부모 자식관계를 가지지 않는다.
상속은 상위 클래스의 속성, 기능을 모두 가지고있지만,
믹스인에서는 모듈이 사용하는 특정 필드나 특정 기능들만 사용하고, 그것이 있다는 전제하에 사용해야한다.
예를들어 테이블을 생각해보자. 테이블에는 식탁도 있고 책상도 있다.
테이블 이라는 개념에서 공통된 기능들이 있을텐데 이를 테이블 클래스로 잘 정의하고 이를 상속받아 식탁, 책상 클래스를 만들 수 있다.
그런데 이후 스탠딩 책상이 생겼다면? 접이식 테이블이 생겼다면?
접이식 책상 클래스에는 접는 기능을 정의해야하고, 스탠딩 책상 클래스에는 높이 조절기능을 따로 정의해야 할 것이다.
테이블 클래스는 점점 추상화가 어려워 지고 기능이 고도화 될 수록 머리는 아파진다.
상속과 달리 믹스인은 제한적인 기능만을 클래스에 포함시킨다.
테이블 클래스를 만들때는 책상 자체를 추상화 하고, 하위 클래스의 기능을 각 클래스에 따로 정의해야했다.
이번에는 책상의 기능들에 집중해 보자.
먼저 '접을 수 있는' 기능에 대한 모듈을 만든다면 그 안에 접는 기능에 대한 정의만 있으면 된다. 그리고 접이식 책상 클래스에 믹스인 한다.
'접이식 스탠딩 책상' 이 생긴다면, '접이식 기능' 모듈과, '높낮이 조절기능' 모듈을 믹스인 하면 될 것이다.
중요한 것은 이렇게 믹스인을 할때, 테이블은 상판과 다리가 있다는 전제를 갖추고 있다는 것이다.
전제를 갖추고 있다면, 테이블 뿐 아니라 의자 클래스에도 믹스인 할 수 있다.
메서드와 상수를 모아놓은 것. namespace로 메서드 상수를 분리하는 역할을 할 수 있다.
클래스처럼 인스턴스를 생성할 수 없다. 대신, 다른 클래스에서 믹스인 하여 쓸 수 있다.
클래스 생성시 ancestors 배열에 부모들을 저장해 둔다. 여기에는 상속받은 클래스들과 자기자신, 믹스인한 모듈들도 포함된다.
irb(main):001:0> Array.ancestors
=> [Array, Enumerable, Object, Kernel, BasicObject]
클래스의 인스턴스 메서드를 호출하면 ancestors의 앞에서부터 돌며 해당 메서드를 찾는다. 첫 클래스에서 찾지 못하면 다음 부모에서 찾는다.
루비의 릭스인은 include, prepend, extend
세가지를 이용할 수 있다.
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"
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 위 두 키워드와는 달리 모듈의 메서드를 클래스 메서드로 사용할 수 있게한다.(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하면 싱글톤 클래스가 확장된다)
위에서 얘기했던 모듈 가구 얘기를 다시 해 보자.
다리가 있다는 전제하에, 다리의 특정 기능을 결정하는 모듈을 믹스인 할 수 있다.
코드를 먼저 보자.
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
이라는 모듈을 믹스인하여 각 클래스 내부에 기능을 정의하지 않더라도 다리를 떼었다 붙였다 할 수 있게된다.
간단한 예를 생각해 보자.
보통 모델을 만들때 ActiveRecord::Base
를 상속받아 클래스를 생성한다.
때문에 모든 모델이 동시에 가지고 있는 특징이 있다.
속성으로 id, created_at, updated_at
를 갖고, ActiveRecord::Base
의 많은 기능들을 갖는다는 것이다.
때문에 created_at(or id)
를 가지고 필터링하는 스코프를 다루는 모듈을 만들어 필요한 모델에서 믹스인해 사용할 수도 있겠다.
좀 더 생각해 보자.
특정 모델에서 변경 사항이 있을때, 슬랙으로 항상 노티를 줘야한다고 하자.
SlackNotifiable
이라는 모듈에 해당 기능을 만들고 필요한 모델 클래스가 include 하면 되것 같다.
이것을 상속으로 해결해 본다면? 음..
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하게 코드를 작성하는데에 목적이 있는 듯 하다.
믹스인을 통해 개발의 재미를 조금 북돋았으나, 항상 적절한 상황에서, 적당히 쓰도록 신경써야 겠다.
그리고 관련 부분을 계속 생각하다보니 몇 가지 개인적인 결론이 나왔다.
결론은 언제나 바뀔 수 있지만..
다음엔 레일즈에서 Activerecord::Concern
을 이용한 믹스인에 대해 알아볼 예정이다.