Dart 언어의 class modifier 훑어보기

S_Soo100·2024년 10월 20일

flutter

목록 보기
18/25
post-thumbnail
  • 클래스 모디파이어(클래스 수정자 혹은 제어자, Class Modifiers)란, class 혹은 mixin이 라이브러리 내/외부로 사용되는 방식을 결정합니다.

  • 모디파이어 키워드는 class 혹은 mixin 바로 앞에 배치하며 mixin은 그 자체로 제어자 키워드중 하나이기도 하며, 클래스의 사용 방식에(프로젝트 내에서 그 클래스가 사용되기를 '원하는'방식에) 직접적으로 관여합니다.


    🍀 공식 문서 : https://dart.dev/language/class-modifiers




빠르게 보는 modifier의 사용과 조합

  • Dart의 주요 class modifier에는 abstract, final, sealed, base, mixin, interface 등이 있습니다.
    에기에 class까지 해서 7개 키워드의 각 기능을 정리하면 아래와 같습니다.
    생성자, 상속, 구현, 완전성 검사등의 유용한 기능들을 modifier를 사용해서 제한하거나 특정 상황에서만 동작하게 만들 수 있습니다.
    enum, typedef, extension, extension type 등에는 사용이 불가능합니다.
Modifierconstructextendimplementmixinexhaustive
class✔✔✔
abstract✔✔
final✔
base✔✔
interface✔✔
sealed✔
mixin✔✔✔✔

(공식문서의 Valid combinations)

기능의 한줄 설명

  • construct: 클래스 인스턴스화
  • extend: 다른 클래스를 부모로 상속
  • implement: 인터페이스 메서드 구현
  • mixin: 여러 클래스에 공통사용 가능한 기능을 추가
  • exhaustive: 본인을 상속한 모든 하위 클래스를 처리하는 완전성 검사

각 기능들이 왜 중요한가요?

  • 클래스 간의 관계와 동작을 정의하기 때문입니다.
    객체 지향 프로그래밍(OOP) 에서 예를들어, 클래스의 확장과 구현은 OOP의 핵심 원리인 재사용성과 유연성을 실현하게 해줍니다.

  • 확장(extends)은 기존 클래스를 상속하는 과정을 통해 이루어지는데,
    매번 기본 기능을 추가할 필요가 없어 코드의 재사용성을 올려주며, 기본 기능의 수정을 상위클래스에서 하면 상속자들에게 모두 반영되기 때문에 유지보수성이 좋다고 할 수 있습니다.
    (물론 여기서 발생되는 다른 문제들 또한 존재합니다)


  • 구현(implements)은 구현자에게 인터페이스에 정의된 메서드 구현을 강제합니다.
    이는 OOP에서 다형성(polymorphism)을 지원하고, 일관된 동작을 제공하는 데 매우 유용합니다.

    이로 인해 클래스의 구성이 달라도 인터페이스가 같기 때문에 에러 없이 상호작용 할 수 있습니다.
    이는 상속과는 다른 방식으로 유연한 설계를 가능하게 해줍니다.

각 modifier의 원리와 사용구조

1. abstract(추상 클래스)

  • abstract는 공통된 동작이나 기능을 여러 클래스에 제공하는 목적으로 사용됩니다.
    또한 클래스가 직접 인스턴스화될 수 없도록 만들며, 반드시 상속을 통해 구현해야 하는 메서드들을 정의하는 데 사용되며 각 하위 클래스는 필요에 따라 이를 확장하거나 수정할 수 있습니다.
  • 하지만 모든 메서드를 추상화 해야 하는것은 아니며, 구현 메서드나 값 또한 포함할 수 있습니다.
  • interface와 함께 사용할 수도 있습니다. 이 경우 굉장히 pure한 인터페이스를 만들 수 있습니다.
abstract class Animal {
  void makeSound(); // 추상 메서드
  void move() { // 구현 메서드
    print("move");
  }
}

class Dog extends Animal {
  
  void makeSound() {
    print('Woof!');
  }
}

2. final

  • 변수에 사용하는 final 키워드와 달리 이는 상속을 방지하는 용도로 사용합니다.

    더이상 mixin이나 class가 상속되지 않도록 고정하며, abstract가 아니고 class로 만들었다면 인스턴스화는 가능합니다.
final class Robot {
  void turnOn() {...}
}

// class Gundam extends Robot {} // ERROR!! : Robot은 상속이 불가능합니다.
Robot myRobot = Robot(); // 인스턴스화는 가능합니다.

3. sealed

  • sealed 는 Dart 3.0에서 도입된 키워드로, 내 클래스를 같은 파일 안에서만 상속이 가능하도록 합니다.

    즉, sealed 클래스를 상속하는 모든 하위 클래스는 같은 파일에 있어야 합니다.
    이를 통해 상속 계층을 더 엄격하게 관리할 수 있습니다.
  • sealed 클래스를 상속받는 클래스는 enum과도 비슷하다고 생각 할 수 있습니다. 모든 하위 클래스를 포괄적으로 처리할 수 있는 완전성(exhaustive)을 제공하기 때문입니다.

    하위 클래스의 범위가 특정 파일 내(현재 파일 내)로 제한되기 때문에, 모든 확장 가능성을 알고 이를 처리할 수 있게 됩니다. 이를 통해 switch 구문이나 패턴 매칭에서 모든 하위 클래스를 반드시 처리할 수 있습니다.
  • 정리하면 sealed는 코드의 안전성과 유지보수성을 극대화하는 기능입니다.
  • 외부에서 직접 상속할 수 없는 클래스를 만들 때, sealed와 final이라는 두 가지 선택지가 주어집니다.
    이에 대한 비교는 별도의 글에서 다루도록 하겠습니다.

(sealed 예시코드는 역시 도형이 맛있다.)

sealed class Shape {}

class Circle extends Shape {}
class Square extends Shape {}

Shape shape = getShape();

switch (shape) {
  case Circle:
    // Circle 처리
    break;
  case Square:
    // Square 처리
    break;
  // 모든 하위 클래스를 처리하므로 오류 방지 가능
}

4. base

  • 클래스 또는 믹스인 구현의 상속을 강제하는 모디파이어 입니다.
  • base 클래스는 상속받을 수는 있지만 구현(implement)하거나 mixin사용은 불가능합니다.(명시적인 상속 제한이라 부릅니다.)
  • 파일 경계를 넘어 상속이 가능하며, 상속받는 모든 클래스는 base, final, sealed중 하나의 키워드를 표시해야 합니다.
-----a.dart
base class Vehicle {
  void moveForward(int meters) {
    // ...
  }
}
-----b.dart
import 'a.dart';

// Can be constructed.
Vehicle myVehicle = Vehicle();

// Can be extended.
base class Car extends Vehicle {
  int passengers = 4;
  // ...
}

// ERROR: Can't be implemented.
base class MockVehicle implements Vehicle {
  
  void moveForward() {
    // ...
  }
}

5. interface

  • 본래 Dart에서는 Java등과 다르게 interface라는 별도의 키워드가 없었으며 모든 클래스는 자동으로 인터페이스 역할을 했습니다.
    interface 키워드는 Dart 3.0 업데이트와 함께 추가되어 이를 통해 명시적으로 인터페이스로 사용할 클래스를 정의할 수 있습니다.
  • 이 키워드는 클래스 자체를 인터페이스로 사용하도록 구성하여 implements를 통해 내부 메서드들을 구현하도록 강제합니다.
  • 상속을 방지하여 내부 로직이 의도치 않게 변경되는 것을 방지하고 fragile base class problem를 줄일 수 있습니다.(이 또한 다른 글에서 다루겠습니다)
  • 추상 메서드가 아니라 구현 메서드도 내부에 포함할 수 있습니다.
interface class Vehicle {
  void moveForward(int meters) {
    // ...
  }
}

// 생성자 사용 가능
Vehicle myVehicle = Vehicle();

// ERROR!!: 상속 불가
class Car extends Vehicle {
  int passengers = 4;
  // ...
}

// 구현 가능
class MockVehicle implements Vehicle {
  
  void moveForward(int meters) {
    // ...
  }
}

6. mixin(믹스인)

  • 상속, 구현과는 별개로 여러 클래스 위계에서 재사용 될 수 있는 코드를 만듭니다.
    특정 클래스 상속이 불가능하며 생성자 선언도 불가능합니다. 마찬가지로 자체 인스턴스 화도 불가능합니다.
  • 기능을 재사용하고 코드 중복을 줄이는데 큰 도움을 주며,
    여러 클래스에서 사용할 내용을 하나의 mixin에서 한 번에 제공하도록 설계되어 있습니다.
  • with 키워드로 클래스 명 혹은 부모 클래스 명 뒤에 기재하며, 복수 사용이 가능합니다.
  • 추상 메서드/추상 변수를 가질 수 있으며, 믹스인 사용시 꼭 이를 구현해야 합니다.
  • mixin class로 구성해서 상속과 믹스인 두 가지 용도로 사용하는 것 또한 가능합니다.
mixin class Both {}

class UseAsMixin with Both {}
class UseAsSuperclass extends Both {}

modifier combinations

  • modifier는 여러개를 조합해서 붙일 수 있습니다. 그중 아래 표의 조합만이 가능하며, 이는 클래스의 기능을 더 명확하게 제한해줍니다.
  • 아까보다 조금 더 긴 표를 보겠습니다. 아래 조합만 valid하며, 표기되지 않은 조합(mixin abstract등)은 사용이 불가능 합니다.
  • 이 조합을 통해 우리는 각각의 클래스가 상속, 구현, 믹스인이 가능한지 불가능한지를 정의합니다.

    abstract interface class등을 통해 구현만 가능한 pure한 인터페이스를 정의하는 것도 가능합니다.

Class TypeConstructExtendImplementMixinExhausive
class✔✔✔
base class✔✔
interface class✔✔
final class✔
sealed class✔
abstract class✔✔
abstract base class✔
abstract interface class✔
abstract final class
mixin class✔✔✔✔
base mixin class✔✔✔
abstract mixin class✔✔✔
abstract base mixin class✔✔
mixin✔✔
base mixin✔
profile
Terra-AI

0개의 댓글