이번 포스트에서는 Dart의 Mixin에 대해 정리하겠습니다.
Mixin이란?
Mixin
이란 여러 클래스 계층에서 재사용할 수 있는 코드를 정의하는 방법입니다. 클래스에 with 키워드를 사용하여 Mixin을 추가하면 해당 클래스는 mixin에서 제공하는 필드와 메소드를 사용할 수 있습니다. 반면, extends 키워드는 단일 상속만 지원하므로 하나의 클래스만 상속할 수 있습니다.
물론, implements 키워드를 사용하여 여러 인터페이스를 구현할 수 있지만, implements를 사용하면 해당 인터페이스에 정의된 모든 메소드를 반드시 오버라이드로 구현해야 한다는 점에서 with 키워드를 사용하는 것과는 차이가 있습니다.
Mixin을 여러번 사용하면 다중 상속일까?
Mixin
은 다중 상속과 유사한 기능을 제공하지만, 엄밀히 말하면 다중 상속과는 다른 기능입니다. 또한, 애초에 mixin의 개념 자체가 상속과는 다른 개념입니다.
다중 상속
은 하나의 클래스가 여러 부모 클래스로부터 직접 상속하는 것을 말합니다. 이로 인해 클래스 간의 복잡한 상속 계층 구조와 Diamond Problem 문제가 발생할 수 있습니다. 그래서 다중 상속을 사용할 경우 명시적인 규칙과 충돌 문제를 해결하기 위한 메커니즘이 필요합니다.
Mixin
은 이러한 다중 상속의 한계를 극복하기 위해 만들어진 개념이며, 클래스에 특정 기능을 추가하거나 확장하기 위해 사용되는 일종의 재사용 가능한 코드를 말합니다. 따라서, mixin은 다중 상속과 비슷한 효과를 내지만 명시적으로 다중 상속을 지원하는 것이 아니라 다중 상속으로 인한 문제를 피하기 위해 활용하는 방법 중 하나이며, mixin을 사용하면 코드 재사용 및 확장성을 높일 수 있습니다.
Mixin의 추가되는 순서의 중요성
예를 들어, 아래의 코드를 보겠습니다.
mixin AMixin {
void hello() {
print('hello A');
}
}
mixin BMixin {
void hello() {
print('hello B');
}
}
class AB with AMixin, BMixin {}
class BA with BMixin, AMixin {}
void main() {
AB ab = AB();
ab.hello();
BA ba = BA();
ba.hello();
}
위 코드의 출력 결과는 어떻게 나올까요?
hello B
hello A
AMixin, BMixin 순서로 추가된 AB 클래스는 B가 출력되었고
BMixin, AMixin 순서로 추가된 BA 클래스는 A가 출력되었습니다.
위의 케이스처럼, 생성한 mixin의 메소드의 이름이 중복된다면 추가된 순서에 따라 마지막에 선언된 mixin의 메소드가 우선권을 가집니다. 따라서 마지막에 추가된 mixin의 메서드가 호출되는 결과가 나온 것입니다.
이유는 뭘까요?
Mixin
은 다중 상속의 개념이 아니라 '혼합'이라는 이름 그대로 혼합하는 형태로 동작하기 때문입니다. 만약, 다중 상속이었다면 부모 클래스의 메소드를 그대로 이어받아 충돌 문제가 발생할 것입니다. 반면, mixin은 다음에 추가된 mixin을 혼합하는 형태로 동작하여 여러 mixin을 동시에 사용할 수 있으므로 오히려 클래스 확장에 더 가깝습니다. 그래서 마지막에 추가된 mixin이 출력되는 결과가 나오는 것입니다.
사용 예시
예시를 위해 걷는 동작을 할 수 있는 Dog 클래스와 Cat 이라는 클래스를 만들고 싶다고 가정하겠습니다. 그래서 코드의 유연성을 위해서 walk() 메소드는 Mammal 클래스에 생성하고 Dog가 Mammal을 extends 키워드를 통해서 상속하도록 만들 수 있습니다.
그래서 아래와 같은 다이어그램으로 나타낼 수 있습니다.
위 다이어그램은 아래의 코드로 작성할 수 있습니다.
class Mammal {
void walk() {
print('Walking');
}
}
class Dog extends Mammal {}
class Cat extends Mammal {}
그런데 만약, 위 코드 상태에서 Eagle 클래스를 만들어야 할 일이 생겼습니다. Eagle은 walk(), fly()를 할 수 있어서 Bird의 특성을 가지고 있다는 것을 알 수 있습니다. 또한, Mammal 클래스와 walk()라는 중복 코드가 발생하므로 Animal이라는 상위 클래스를 만들어서 중복 코드 또한 제거할 수 있다는 것을 알 수 있습니다.
다이어 그램으로 보면 아래와 같이 나타낼 수 있습니다.
위 다이어그램은 아래의 코드로 작성할 수 있습니다.
class Animal {
void walk() {
print('Walking');
}
}
class Mammal extends Animal {}
class Bird extends Animal {
void fly() {
print('Flying');
}
}
class Dog extends Mammal {}
class Cat extends Mammal {}
class Eagle extends Bird {}
Animal 클래스의 상속을 통해서 walk() 메소드의 중복코드를 제거하고 요구 사항의 모든 클래스를 작성했습니다.
이제 위 코드에서 Salmon, Duck 두 클래스의 추가를 요청받았습니다.
-> Fish는 걸을 수 없으므로 Animal의 walk() 메소드를 제거하고 Mammal, Bird에 각각 walk() 메소드를 추가해주었습니다. 그리고 Duck클래스를 추가하고 swim() 메소드를 추가하였습니다.
하지만, 이렇게 코드를 변경함으로써 두 가지 상황이 추가된다면 문제점은 두 가지가 존재합니다.
상황 1. Salmon 클래스를 추가했습니다. Salmon은 swim()을 할 수 있으므로 Fish의 특성을 가지고 있다는 것을 알 수 있습니다.
-> Salmon 클래스를 추가하게 되면, Animal 클래스에서 walk() 메소드를 제거해서 Fish 클래스는 walk()를 가지지 않게 만들어야 합니다. 결국, Mammal과 Bird 클래스에 동일한 동작을 하는 walk()를 추가할 수밖에 없으므로 중복코드가 다시 발생했습니다. WalkingAnimal이라는 클래스를 만들어서 상속해주는 방법도 고민할 수는 있겠지만, Bird 클래스는 FlyingAnimal 클래스도 상속하게 만들어야 하는데 다중 상속을 지원하지 않으므로 이 방법도 불가능합니다.
상황 2. Duck 클래스를 추가했습니다. Duck은 walk(), fly() 할 수 있는 Bird의 특성을 가지고 있다는 것을 알지만 마찬가지로 Fish의 특성도 가지고 있다는 것을 알고 있습니다.
-> 이제는 기껏 만들어 놓은 Fish 클래스도 재사용할 수 없게 되었습니다. 마찬가지로 다중 상속을 지원하지 않으므로 Fish 클래스를 상속할 수 없습니다. 결국, Duck 클래스에 swim() 함수를 추가할 수밖에 없으므로 중복코드가 또 발생했습니다.
결국 아래와 같은 다이어그램으로 나타낼 수 있습니다.
위 다이어그램은 아래의 코드로 작성할 수 있습니다.
abstract class Animal {}
class Mammal extends Animal {
void walk() { // 중복 코드 발생!
print('Walking');
}
}
class Bird extends Animal {
void walk() { // 중복 코드 발생!
print('Walking');
}
void fly() {
print('Flying');
}
}
class Fish extends Animal {
void swim() { // 중복 코드 발생!
print('Swimming');
}
}
class Dog extends Mammal {}
class Cat extends Mammal {}
class Eagle extends Bird {}
class Duck extends Bird {
void swim() { // 중복 코드 발생!
print('Swimming');
}
}
class Salmon extends Fish {}
이 문제를 해결하려면 여러 클래스 계층에서 코드를 재사용할 수 있는 방법이 필요합니다. 생각나는 해결 방법은 다중 상속이긴 하지만 Diamond Problem에 대한 위험성도 있을뿐더러 어차피 Dart에서는 지원하지 않습니다.
그래서 이제 드디어 Mixin
을 사용하면 됩니다.
기존에 클래스에서 직접 선언했던 walk(), fly(), swim() 메소드를 제거하고 WalkerMixin, FlyerMixin, SwimmerMixin을 만들어줍니다. 생성한 mixin은 각각의 클래스에 with 키워드를 이용하여 추가한 뒤에 mixin에서 제공하는 필드와 메소드를 사용할 수 있습니다.
Duck 클래스에 Mixin은 아래와 같은 과정으로 추가됩니다.
Duck 클래스를 생성하기 위해 먼저, Bird 클래스에 with 키워드로 WalkerMixin, FlyerMixin을 추가해 줍니다.
그리고 Duck 클래스에서 Bird 클래스를 extends 키워드로 상속해 준 뒤, with 키워드를 이용하여 SwimmerMixin을 추가해 줍니다.
그러면 최종적으로 Duck 클래스는 Bird 클래스에 추가해 준 WalkerMixin, FlyerMixin과 Duck 클래스에 추가해준 SwimmerMixin을 가지게 됩니다.
이제 fly(), swim() 기능을 가지고 있는 FlyingFish 클래스도 손 쉽게 만들 수 있게 되었습니다.
다이어그램으로 나타내면 아래 그림과 같이 나타낼 수 있습니다.
위 다이어그램은 아래의 코드로 작성할 수 있습니다.
abstract class Animal {}
mixin WalkerMixin {
void walk() {
print('Walking');
}
}
mixin FlyerMixin {
void fly() {
print('Flying');
}
}
mixin SwimmerMixin {
void swim() {
print('Swimming');
}
}
abstract class Mammal extends Animal with WalkerMixin {}
abstract class Fish extends Animal with SwimmerMixin {}
abstract class Bird extends Animal with WalkerMixin, FlyerMixin {}
class Dog extends Mammal {}
class Cat extends Mammal {}
class Duck extends Bird with SwimmerMixin {}
class Eagle extends Bird {}
class Salmon extends Fish {}
class FlyingFish extends Fish with FlyerMixin {}
void main() {
var dog = Dog();
dog.walk();
var cat = Cat();
cat.walk();
var eagle = Eagle();
eagle.walk();
eagle.fly();
var duck = Duck();
duck.walk();
duck.fly();
duck.swim();
var salmon = Salmon();
salmon.swim();
var flyingFish = FlyingFish();
flyingFish.swim();
flyingFish.swim();
}
이제 Mixin으로의 변경을 통해서 재사용 가능하면서도 각각의 동작이 필요한 클래스에서 사용할 수 있게 되는 확장 가능한 구조로 변경되었습니다!
on 키워드
on 키워드
를 사용하면 mixin이 적용될 수 있는 클래스를 제한할 수 있습니다.
mixin FlyerMixin on Animal {
void fly() {
print('Flying');
}
}
abstract class Animal {}
abstract class Bird extends Animal with FlyerMixin {}
abstract class Stuff {}
abstract class Book extends Stuff {}
class Eagle extends Bird with FlyerMixin {
// Eagle 클래스는 Animal을 상속했으므로 FlyerMixin을 사용 가능합니다.
}
class EffectiveDart extends Book with FlyerMixin {
// EffectiveDart 클래스는 Animal을 상속하지 않았으므로 FlyerMixin을 사용할 수 없습니다.
}
예를 들면, FlyerMixin에 on 키워드를 이용해서 Animal 클래스만 적용할 수 있도록 제한할 수 있습니다.
mixin FlyerMixin on Animal {}
그러면 Eagle 클래스는 Animal 클래스를 상속했으므로 FlyerMixin을 사용할 수 있지만, Animal 클래스를 상속하지 않은 EffectiveDart 클래스는 FlyerMixin을 사용할 수 없습니다.
mixin FlyerMixin on Animal {
@override
void eat() {
super.eat();
// Animal 클래스로 제한되었으므로 Animal 클래스의 eat() 메소드 오버라이딩 가능
}
}
abstract class Animal {
void eat() {}
}
abstract class Bird extends Animal with FlyerMixin {}
또한, FlyerMixin이 Animal 클래스로 범위가 제한되었으므로, Animal 클래스에 eat() 메소드를 추가한다면 FlyerMixin에서 Animal 클래스의 eat() 메소드를 오버라이딩 할 수 있습니다..
References
https://medium.com/flutter-community/dart-what-are-mixins-3a72344011f3
https://dart.dev/language/mixins