[Dart]다양한 class modifier

한상욱·2025년 2월 19일

Dart문법

목록 보기
16/16
post-thumbnail

들어가며

최근 프로젝트를 진행하며 비슷하지만 용도가 다르게 사용되는 다양한 클래스를 정의하고 설계하면서 단순한 class 보다는 용도에 맞게끔 class를 정의하려고 고민하게 되었고 그 결과 class modifier를 사용하게 되었습니다. 오늘은 class modifier를 정리해보도록 하겠습니다.

class modifier

Dart는 여러 class modifier를 제공하고 있습니다. 그전에, class modifier가 대체 무엇인지 조금 알아보도록 하겠습니다.

class modifier는 class 또는 mixin이 자체적으로 혹은 외부에서 사용되는 방법에 대해서 제어해줍니다. 보통 class(또는 mixin) 앞에 위치시켜 적용시킬 수 있습니다. 아래에는 Dart에서 제공하는 class modifier입니다.

  • abstract
  • base
  • final
  • interface
  • sealed
  • mixin

class modifier를 지정하지 않으면 단순 class로 기존의 class와 동일하게 상속, mixin 등을 사용할 수 있습니다.

abstract class

abstract class는 구체적인 동작을 정의할 필요가 없는 class를 정의하기 위해 사용하게 됩니다. 이렇게 선언된 class는 자체 혹은 외부 라이브러리에서도 선언할 수 없고, 보통 추상적인 메소드가 정의되어 있는 경우가 대부분입니다.

// a.dart
abstract class Vehicle {
  void moveForward(int meters);
}
import 'a.dart';
// b.dart
// Error: Can't be constructed.
Vehicle myVehicle = Vehicle();

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

  
  void moveForward(int meters) {
    // ...
  }
}

// Can be implemented.
class MockVehicle implements Vehicle {
  
  void moveForward(int meters) {
    // ...
  }
}

만약 abstract class를 인스턴스화 가능하도록 보이려면 factory 생성자를 정의하라고 합니다.

base class

base class는 class의 상속이나 mixin의 구현을 강제하기 위해 사용됩니다. base class는 자체 라이브러리 외에서의 구현을 방지해줍니다. 이는 다음을 보장합니다.

  • base class의 생성자는 하위 인스턴스가 생성될 때마다 호출됩니다.
  • 구현된 모든 private member는 하위 유형에 존재합니다.
  • 모든 하위 유형은 새로운 member를 상속하므로 base class에서 새로 구현된 member는 하위 유형 제공을 중단하지 않습니다. 단, 하위 유형이 동일한 이름의 새 member를 구현하지 않는다는 전제하에서 적용됩니다.
// a.dart
base class Vehicle {
  void moveForward(int meters) {
    // ...
  }
}
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() {
    // ...
  }
}

base class는 생성이나 상속은 가능하지만, 메소드 오버라이딩같은 외부 구현은 허용하지 않습니다.

또한, base class를 상속받는 모든 하위 유형은 base, final 또는 sealed로 구현해야 합니다. 이는 외부 라이브러리가 기본 클래스 보장을 위반하는 것을 방지합니다.

interface class

인터페이스를 정의하려면 interface class를 사용할 수 있습니다. interfacce 자체 정의 라이브러리 외부의 라이브러리는 인터페이스를 구현할 수 있지만 확장할 수는 없습니다. 이는 다음을 보장합니다.

  • 클래스의 인스턴스 메서드 중 하나가 이에 대한 다른 인스턴스 메서드를 호출하면 항상 동일한 라이브러리에서 알려진 메서드 구현을 호출합니다.
  • 다른 라이브러리는 인터페이스 클래스 자체 메서드가 나중에 예상치 못한 방식으로 호출할 수 있는 메서드를 재정의할 수 없습니다. 이렇게 하면 취약한 기본 클래스 문제가 줄어듭니다.
// a.dart
interface class Vehicle {
  void moveForward(int meters) {
    // ...
  }
}
import 'a.dart';
// b.dart
// Can be constructed.
Vehicle myVehicle = Vehicle();

// ERROR: Can't be inherited.
class Car extends Vehicle {
  int passengers = 4;
  // ...
}

// Can be implemented.
class MockVehicle implements Vehicle {
  
  void moveForward(int meters) {
    // ...
  }
}

abstract interface class

interface class는 일반적으로 순수한 interface를 정의하기 위해서 사용합니다. abstract interface class는 interface와 abstract를 결합하여 사용합니다.

interface class와 마찬가지로 다른 라이브러리도 순수 interface를 구현할 수 있지만 상속할 수는 없습니다.

abstract class와 마찬가지로 순수 interface도 추상 member를 가질 수 있습니다.

final class

final class는 외부에서 더 이상의 상속을 방지하기 위해서 사용합니다. final class는 상속받을 수 없습니다. 이는 다음을 보장합니다.

  • API에 추가적인 변경 사항을 안전하게 추가할 수 있습니다.
  • Third party 하위 class에서 덮어쓰이지 않았다는 것을 알고 인스턴스 메서드를 호출할 수 있습니다.
// a.dart
final class Vehicle {
  void moveForward(int meters) {
    // ...
  }
}
import 'a.dart';

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

// ERROR: Can't be inherited.
class Car extends Vehicle {
  int passengers = 4;
  // ...
}

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

sealed class

하위 유형에 대한 집합체처럼 사용하기 위해서 sealed class를 사용합니다. 이를 통해서 하위 유형에 대해서 정적으로 철저하게 보장되는 하위 유형 switch를 만들 수 있습니다.

sealed class는 외부 라이브러리에서 상속되거나 구현되는 것을 방지합니다. 암시적으로는 abstract class의 성질을 띕니다. 메소드를 구체적으로 구현하지 않습니다.

  • sealed class 자체는 선언할 수 없습니다.
  • factory 생성자를 가질 수 있습니다.
  • 하위 유형에서 사용하기 위한 생성자를 정의할 수 있습니다.

그러나 sealed class의 하위 class는 암시적으로 추상이 아닙니다.

컴파일러는 가능한 모든 직접 하위 유형을 인식합니다. 하위 유형은 동일한 라이브러리에만 존재할 수 있기 때문입니다. 이를 통해 switch가 해당 사례에서 가능한 모든 하위 유형을 철저하게 처리하지 못하는 경우 컴파일러에서 경고할 수 있습니다.

sealed class Vehicle {}

class Car extends Vehicle {}

class Truck implements Vehicle {}

class Bicycle extends Vehicle {}

// ERROR: Can't be instantiated.
Vehicle myVehicle = Vehicle();

// Subclasses can be instantiated.
Vehicle myCar = Car();

String getVehicleSound(Vehicle vehicle) {
  // ERROR: The switch is missing the Bicycle subtype or a default case.
  return switch (vehicle) {
    Car() => 'vroom',
    Truck() => 'VROOOOMM',
  };
}

보통 bloc 패턴에서 기본이 되는 event와 state를 정의하는 용도로 많이 사용되는데 왜 sealed class를 사용하는지 알 수 있습니다.

profile
자기주도적, 지속 성장하는 모바일앱 개발자의 기록

0개의 댓글