Dart 3.0 에 추가된 새로운 기능으로 Sealed Class
가 나왔다.
Q. 이 Sealed Class는 왜 나왔는가.
Sealed Class
에 대해서 자세히 알기 위해서는 "Union Types" 와 "Pattern-matching" 의 용어에 대해서 알 필요가 있다.
왜 알아야 하나면, Union Types 은 Sealed Class이 달성하려는 목표를 일부 달성하고 있다. Pattern-matching은, Sealed Class를 통해서 구현물이다. 그래서 이 둘을 알 필요가 있다.
Union Type은 여러 개의 타입을 합친 타입이다. 그래서 하나의 여러 개의 타입을 다룰 수 있도록 해준다. 그런데 Dart 에는 없다.
먼저 Union
이란, 합집합을 의미한다.
A ∪ B
그러므로, Union Type 은 합집합 성질이 있는 타입을 의미하는 것이다.
Typescript 공식문서에서는 다음과 같이 설명하고 있다.
union type은 두 개 이상의 다른 유형으로 구성된 유형으로, 이러한 유형 중 하나가 될 수 있는 값을 나타냅니다. 이러한 각 유형을 유니온의 멤버라고 합니다.
// 여기에 "number | string" 이 두 개의 타입을 모두 포함하는 합집합 타입이 적용되었다.
function printId(id: number | string) {
console.log("Your ID is : " + id);
}
// OK
printId(101);
// OK
printId("202");
// Error
// Argument of type '{ myID: number; }' is not assignable to parameter of type 'string | number'.
printId({ myID: 22342 });
number
타입과 string
타입을 모두 파라미터로 전달 받을 수 있는 id 파라미터가 union type
의 대표적인 예시이다.그러나, Dart 에서는 union types
을 지원하지 않는다. 그래서 Sealed Class
를 통해서 union type 의 목적을 달성한다.
사용법에 대해서, 간단히 알아보고, 패턴매칭을 통한 사용법도 알아보고자 한다.
Sealed class 를 정의하는 방법은 간단하다.
sealed class Vehicle {}
그리고 상속하거나 구현할 수도 있다.
// 상속
class Car extends Vehicle {}
// 구현
class Truck implements Vehicle {}
이 코드가 아래 처럼 되어 있으면 문제가 없다.
// 가정: 아래 코드는 vehicle.dart 라는 하나의 .dart 파일에 모두 있다.
sealed class Vehicle {}
// 상속
class Car extends Vehicle {}
// 구현
class Truck implements Vehicle {}
만약 Truck
클래스만 다른 파일에 있다면, 에러가 발생한다.
// truck.dart 파일에 이 코드만 따로 있다.
// ERROR: The class ‘Truck’ can’t be extended, implemented, or mixed in outside of its library because it’s a sealed class.
// 구현
class Truck implements Vehicle {}
다른 파일에서
sealed class
에 접근할 수 없다.
이 특징으로 인해서, pattern-matching
이라는 기능을 구현할 수 있게 된 것이다. 마치 Union type처럼 동작시킬 수 있는 것이다.
cf) dart docs : patterns 에 가보면, 다양한 패턴 매칭 방법을 소개하고 있다.
Sealed class의 놀라운 기능은 Pattern matching
을 구현하게 될 때, 하나라도 구현하지 않으면 컴파일 에러를 발생시킨다.
이게 무슨뜻인지는 코드를 보면 이해가 쉽다.
// sealed class 정의
sealed class Vehicle {
void go() {}
}
// 서브 클래스 상속
class Car extends Vehicle {}
// 서브 클래스 채택
class Truck implements Vehicle {
void go() {}
}
// 서브 클래스 상속
class Bicycle extends Vehicle {}
String getVehicleSound(Vehicle vehicle) {
// ERROR: 만약 sealed class에서 특정 서브 클래스 타입을 빼먹었다면(여기서 Biycle)
// 에러가 발생한다.
return switch (vehicle) {
case Car():
return '부릉부릉';
case Truck():
return '그르르를르릉';
case Bicycle():
return '띠링띠링';
};
}
switch statement
와 다르게 생겼다. case 문이 아니라, 인스턴스 생성의 형태로 되어 있다.Bicycle()
부분이 주석되어 있어서 아래와 같은 컴파일 에러가 발생한다.The type 'Vehicle' is not exhaustively matched by the switch cases since it doesn't match 'Bicycle()'.
Try adding a wildcard pattern or cases that match 'Bicycle()'.
지금 상황은 클래스를 상속했을 뿐인데, 상속했던 모든 클래스를 인지하고 있고, 컴파일 에러를 보여주는 상황이다. 개신기하다.
학이시습지 불역열호(學而時習之 不亦說乎) 라는 말이 있듯이 배웠으면 써먹는 것이 기쁨을 얻는 것이라고 배웠다.
상황을 하나 가정해보고자 한다. 앱을 구현하다보면 제일 많이 하는 구현이 아마 REST API 연동일 것이다. 과정은 3 단계로 볼 수 있다.
앞으로 이 3 가지 상태를 ListViewState 라는 이름으로 묶을 것이다. 아래 코드를 보자.
// Sealed class를 정의한다.
sealed class ListViewState {}
// LoadingState를 정의한다.
class LoadingState extends ListViewState {}
// LoadingState를 정의한다.
class LoadedState extends ListViewState {
final String data;
LoadedState(this.data);
}
class ErrorState extends ListViewState {
final String message;
ErrorState(this.message);
}
void main() {
final ListViewState state = LoadedState('완료된 데이터');
switch(state) {
case LoadingState():
print('로딩중');
case LoadedState():
print('로딩 완료');
case ErrorState():
print('에러 발생!');
}
}
// 실행결과
// 로딩 완료
위 코드에서 만약 하나만 제거하더라도 컴파일에러가 발생한다.
void main() {
final ListViewState state = LoadedState('완료된 데이터');
switch(state) { // ERROR
case LoadingState():
print('로딩중');
case LoadedState():
print('로딩 완료');
}
}
ERROR: The type 'ListViewState' is not exhaustively matched by the switch cases since it doesn't match 'ErrorState()'. Try adding a default case or cases that match 'ErrorState()'
이렇게 하나라도 빠지면, 바로 에러가 발생한다.
기존 Switch Statement 가 아니라, Switch Expression을 활용하면, 변수를 할당하기도하고, 전달할 수도 있다.
cf) Statement(문장) 과 Expression(표현식) 에 대한 차이 관련 글
add(num) => print(num);
이것은 정의한 것이다. 그래서 문장이다.const price = 500;
이것은 값을 할당하고 있는 표현식이다.void main() {
final ListViewState state = LoadedState('완료된 데이터');
final message = switch (state) {
LoadingState() => '로딩중...',
LoadedState() => '로딩완료',
ErrorState() => '에러발생!'
// 아래처럼 작성도 가능하다.
// LoadedState() || ErrorState() => '에러발생!'
};
print(message); // 로딩완료
}
// Expression
final message = switch (state) {
LoadingState() => "로딩중...",
LoadedState(message: String msg) => '다운로드중: $msg',
ErrorState(message: String msg) => 'ERROR: ${msg}',
};
// Statement
switch (state) {
case LoadingState():
print('로딩중');
case LoadedState(message: String msg):
print('다운로드중: $msg');
case ErrorState(message: String msg):
print('ERRPR: $msg');
}
이것 말고도 Sealed Class 는 사용 방식은 더 있다.
TextData
/ ImageData
/ VideoData
Naver
/ Kakao
/ Google
/ Apple
사실 Enum의 상위 호환이라고 생각해도 될 것 같다.
Enum은 간단하게 정의하는 개념이라면, Sealed Class 는 Class의 파워를 추가한 Enum 이라고 보면 될 것 같다.