다양한 과일이 담긴 과일 바구니에서 원하는 과일만 가져오는 것처럼 여러 Mixin 중에서 필요한 기능을 가진 Mixin은 가져올 수 있습니다. 그림 출처: copilot

Mixin

Mixin은 Dart에서 다른 클래스의 기능을 재사용하는 방법을 제공하는 도구입니다. Mixin은 클래스 자체가 되지는 않지만, 다른 클래스에 포함되어 해당 클레스에 기능을 추가할 수 있습니다. Mixin은 다중 상속과 유사한 기능을 제공하지만, 엄밀히 말하면 다중 상속과는 다르게 작동합니다. 다중 상속은 하나의 클래스가 여러 부모 클래스로부터 직접 상속하는 것인 반면, Mixin은 다른 클래스의 기능을 포함하는 방식으로 작동합니다. Mixin을 사용하면, 깊은 클래스 계층 구조를 만들지 않고도 여러 클래스에 기능을 추가할 수 있습니다.

상속과의 차이점?

상속은 클래스가 다른 클래스의 기능을 그대로 가지도록 하는 메커니즘입니다. 상속받은 클래스는 기본 클래스의 모든 속성과 메서드에 액세스할 수 있습니다. 상속은 클래스 계층 구조를 만드는 데 유용하며 서로 관련된 클래스 간의 코드 재사용을 위한 효과적인 방법입니다. Mixin은 여러 클래스에 걸쳐 코드를 재사용하는 방법입니다. 상속 없이 클래스에 추가 기능을 추가하는 메커니즘을 제공하여 다중 상속의 다이아몬드 문제를 피할 수 있습니다.

상속 다이아몬드?
상속 다이아몬드 문제는 다중 상속을 지원하는 언어에서 발생하는 문제입니다. 쉽게 말해 하나의 클래스가 여러 부모 클래스로부터 상속받을 때 동일한 메서드 이름이 있다면 충돌이 발생하는 경우를 의미합니다.

graphviz로 직접 그린 다이아몬드 상속 문제


예를 들어, 클래스 A가 있고, A를 상속받은 두 개의 클래스 B와 C가 있습니다. 그리고 클래스 B와 C를 다시 상속받는 클래스 D가 있습니다. 이 상황에서 만약 클래스 A에 someMethod라는 메서드가 있고, 클래스 B와 C에서 이 메서드를 재정의하지 않았다고 가정합시다.

만약 클래스 D에서 someMethod를 호출하면, D는 A의 someMethod를 사용하게 됩니다. 그러나 클래스 B나 C에서 someMethod를 재정의하면, D에서 someMethod를 호출할 때 어느 클래스의 메서드를 사용할지 모호해지는 문제가 발생합니다. 이 4 개의 클래스의 상속 구조가 다이아몬드 모양이 되기 때문에 이런 이름이 붙게 되었습니다. Dart에서는 Mixin이 포함되는 순서에 따라 어떤 메서드를 사용할지 결정하여 다이아몬드 문제를 해결합니다.

 

Mixins는 일반적으로 사용되는 코드를 캡슐화하여 여러 클래스에 쉽게 적용할 수 있어 코드 재사용성과 유지보수성을 촉진합니다. mixins를 사용하면 Flutter 위젯의 기능을 향상시키고 효율적으로 기능을 공유할 수 있습니다.

Mixin의 필요성

출처: Dart: What are mixins?

여기에는 Animal이라는 슈퍼클래스가 있고, 세 개의 서브클래스(Mammal, Bird, Fish)가 있습니다. 맨 아래에는 구체적인 클래스들이 있습니다. 작은 사각형들은 동작을 나타냅니다. 예를 들어, 파란 사각형은 해당 동작을 가진 클래스의 인스턴스가 수영할 수 있음을 나타냅니다.

 

몇몇 동물들은 공통적인 동작을 공유합니다. 고양이와 비둘기는 둘 다 걸을 수 있지만, 고양이는 날 수 없습니다. 이런 종류의 동작은 이 분류와는 독립적이기 때문에, 슈퍼클래스에 이 동작을 구현할 수 없습니다. 한 클래스가 둘 이상의 슈퍼클래스를 가질 수 있다면, 세 개의 다른 클래스(Walker, Swimmer, Flyer)를 만들 수 있을 것입니다. 그 후, Dove와 Cat을 Walker 클래스에서 상속받게 하면 됩니다.

 

그러나 Dart에서는 다중 상속을 지원하지 않아, 모든 클래스가 (Object를 제외하고) 하나의 슈퍼클래스만 가질 수 있습니다. 물론, Walker 클래스를 상속받는 대신에, 인터페이스처럼 구현할 수 있지만, 그렇게 하면 여러 클래스에서 동작을 구현해야 하므로 좋은 해결책이 아닙니다. 결국 문제를 해결하기 위해서는 클래스의 코드를 여러 클래스 계층 구조에서 재사용할 방법이 필요하고, 그 방법이 바로 Mixin입니다.

Mixin의 사용

사용법을 숙지하기 전에 Mixin의 제약 사항이 있다는 것을 짚고 넘어가겠습니다.

  • 생성자를 선언할 수 없습니다.
  • 클래스나 다른 믹스인을 확장할 수 없습니다.

Dart에서 믹스인은 기능을 제공하는 역할을 하기 때문에 인스턴스를 만들거나 상속 구조를 통해 계층 관계를 형성하지 못합니다.

 

믹스인을 만들기 위해서는 mixin 수식어를 사용하여 선언해야 합니다.

mixin Walker {
  void walk() {
    print("I'm walking");
  }
}

 

믹스인을 사용하려면 with 키워드를 사용하고 하나 이상의 mixin 이름을 붙이면 됩니다.

class Cat with Walker {}

class Dove with Walker, Flyer {}

 

Mixin을 사용해 추가된 기능은 인스턴스에서 사용할 수 있습니다.

void main(List<String> arguments) {
  Cat cat = Cat();
  Dove dove = Dove();

  // 고양이는 걸을 수 있습니다.
  cat.walk();

  // 비둘기는 걸을 수도 있고 날 수도 있습니다.
  dove.walk();
  dove.fly();
}

 

해당 코드는 DartPad에서 직접 실행해 볼 수 있습니다. 코드가 조금 다른 건 아래의 다른 설명을 위함입니다.

mixin의 선형화(Linearization): 다이아몬드 문제의 해결법

다음 코드를 살펴보면서 어떤 결과가 나올지 예측해 보세요.

mixin A {
  String getMessage() => 'A';
}

mixin B {
  String getMessage() => 'B';
}

class P {
  String getMessage() => 'P';
}

class AB extends P with A, B {}

class BA extends P with B, A {}

void main() {
  String result = '';

  final ab = AB();
  result += ab.getMessage();

  final ba = BA();
  result += ba.getMessage();

  print(result);
}

 

정답은 BA입니다.

직접 실행해 볼려면 DartPad에서 확인해 볼 수 있습니다.

 

with 키워드 뒤에 있는 mixin의 선언된 순서가 매우 중요합니다. Mixin 은 슈퍼클래스의 위에 믹스인의 구현을 층층이 쌓아 새로운 클래스를 생성함으로써 작동합니다. 즉, 다음에 선언된 Mixin 은 이전 Mixin 의 "옆에" 있는 것이 아니라 "위에" 있는 것이므로 가장 마지막에 선언된 Mixin의 메서드를 불러오게 됩니다. 

CSS와 비슷하지 않나요?

 

겉으로 보기에 Mixin은 다중 상속처럼 생겼습니다. 하지만 Dart는 다중 상속을 지원하지 않는 언어이며, Mixin은 엄연히 단일 상속입니다.

class AB extends P with A, B {}

위의 코드는 아래의 코드와 완전히 같은 결과를 가집니다.

class PA = P with A;
class PAB = PA with B;

class AB extends PAB {}

쉽게 말해 mixin은 아래의 다이어그램과 같은 방식으로 작동합니다.

출처: Dart: What are mixins?

AB와 P 사이에 새로운 클래스가 생성되는 겁니다. 여기에는 다중 상속이 없습니다!

믹스인은 전통적인 다중 상속을 받는 방법은 아닙니다. 대신, 연산과 상태를 추상화하고 재사용하는 방법입니다. 이는 클래스를 확장하여 얻는 재사용과 비슷하지만, 선형적인 단일 상속과 호환됩니다.

 

믹스인이 선언된 순서가 상속 체인을 나타낸다는 것을 기억하는 것이 중요합니다. 상위 슈퍼클래스에서 하위 클래스로의 순서입니다.

on 키워드: 믹스인에서 슈퍼 클레스의 메서드 사용하기

Mixin에서 on 키워드를 사용한다면 슈퍼 클래스의 메서드를 사용할 수 있습니다. 하지만 이는 동시에 제약 사항이 되는데, 이 Mixin이 Super에서 제공하는 기능을 사용하기 때문에 이 Mixin을 클래스에 적용하기 위해서는 이 클래스가 Super를 확장하거나 구현해야만 합니다. DartPad에서 테스트해 보실 수 있습니다.

class Super {
  void method() {
    print("Super");
  }
}

mixin Mixin on Super {
  @override
  void method() {
    super.method();
    print("Sub");
  }
}

class Client extends Super with Mixin {}

void main() {
  Client().method();
}

상속과 mixin의 사용처

Mixins는 서로 관련 없는 클래스 간에 코드를 재사용할 수 있게 해줍니다. 클래스 간에 "is-a" 관계를 생성하지 않고 기능을 공유할 수 있습니다. 반면, 상속은 클래스 간에 부모-자식 관계를 생성합니다.

is-a 관계
클래스나 객체가 다른 클래스나 객체의 종류(타입)를 나타내는 관계를 의미합니다. 예를 들어 예를 들어, 클래스 Animal이 있고, 이를 상속받은 Dog 클래스가 있다면, "Dog is-a Animal" 관계가 성립합니다.

 

서브클래스는 슈퍼클래스의 속성과 메소드를 상속받아 기능을 전문화하고 확장할 수 있습니다. 상속은 계층 구조를 제공하여 서브클래스를 슈퍼클래스와의 관계에 따라 분류하고 조직화합니다.

 

서로 관련 없는 클래스 간의 코드의 재사용을 위해 mixin을 사용합니다. 위의 예시 코드와 같이 Walk는 서로 관련이 없는 Cat과 Dove 모두에 적용하여 코드를 재사용하는 경우입니다. 반면 관련 있는 클래스 간에 코드를 재사용해야 할 때는 상속을 사용합니다. 예를 들어 Dove는 Bird를 상속받습니다. 이제 왜 DartPad에서 Dove가 왜 Bird는 상속을 받고, Walk는 mixin 하였는지 이해가 가시나요?

물론 타조나, 닭처럼 날지 못하는 새를 고려한다면 Bird가 Flyer를 가지는 건 올바르지 않을 수도 있습니다. 차라리 Fish를 예로 들걸 그랬습니다.

mixin을 사용할 때의 Best Practices

  1. Mixin 이름에 "Mixin" 접미사 사용하기
    코드의 가독성을 높이고 클래스가 Mixin임을 명확히 하기 위해, Mixin의 이름을 "Mixin" 접미사와 함께 짓는 것이 좋습니다. 예를 들어, 로깅 기능을 위한 Mixin이 있다면 LoggingMixin과 같이 이름을 지어주세요.
  2. Mixin을 명확하고 일관되게 유지하기
    Mixin은 하나의 명확하고 잘 정의된 책임을 가지도록 만들어야 합니다. 이는 코드 재사용성을 촉진하고 Mixin을 이해하고 유지보수하기 쉽게 만듭니다. 여러 개의 무관한 기능을 처리하려는 너무 크거나 복잡한 Mixin 생성을 피하세요.
  3. 기존 클래스를 개선하는 코드에 Mixin 사용하기
    Mixin은 기존 클래스의 기능을 추가하거나 동작을 수정하는 데 특히 유용합니다. 이를 통해 상속 계층을 수정하지 않고도 클래스의 기능을 확장할 수 있습니다.
  4. Mixin 자체의 독립적인 상태에 의존하지 않기
    일반적으로 Mixin은 자신의 상태를 클래스에서 액세스하거나 제어할 수 없어야 합니다. 대신 클래스에서 재사용 가능한 메소드나 동작을 제공하는 데 집중하세요.
  5. 복잡한 동작에는 Mixin 대신composition 사용하기
    원하는 동작이 복잡한 조합이나 여러 상호작용 컴포넌트로 구성되어 있는 경우, Mixin에만 의존하지 않고composition을 사용하는 것이 더 명확한 제어와 더 나은 구성을 제공할 수 있습니다.
  6. Mixin을 독립적으로 테스트하기
    Mixin을 사용할 때는 독립적으로 테스트하여 기능이 예상대로 작동하는지 확인하는 것이 좋습니다. Mixin 을격리시키고 이를 위한 단위 테스트를 작성하여 다양한 시나리오에서의 동작을 검증하세요.

본문에 있는 내용인 무관한 클래스 간에 코드를 공유 때 Mixin 사용하기 Mixin 순서 주의하기도 당연히 신경써야합니다. 문서화도 마찬가지입니다.

참고 자료

Dart: What are mixins?
Flutter Mixins Vs. Inheritance: Choosing the Right Approach for Code Reuse

+ Recent posts