[Capstone-Start] Dart ํ์ต โ Generics, Sealed Class, freezed
๐ ์ผ์: 2026. 05. 21
๐ฌ ๋ด์ฉ: Generics์ Dart 3์ sealed class, freezed ์ฝ๋ ์์ฑ์ผ๋ก ๋ง๋ Result<T>์ AppError ์ฝ๋ ํ๊ณ
์ฝ์ด ์ธํ๋ผ์ ์๋ฌ ๋ชจ๋ธ ๋ถ๋ถ์ ํ์ต ๋
ธํธ๋ก ์ ๋ฆฌํ์ต๋๋ค. ๊ตฌ์ฒด์ ์ผ๋ก๋ core/errors/์ ๋ง๋ค์ด ๋ AppError sealed ํด๋์ค์ Result<T> ํ์
์ด Dart Generics์ Dart 3์ sealed class, ๊ทธ๋ฆฌ๊ณ freezed ์ฝ๋ ์์ฑ์ด๋ผ๋ ์ธ ๊ฐ์ง ๊ธฐ๋ฅ์ ํฉ์ณ ๋ง๋ ๊ฒฐ๊ณผ๋ฌผ์ด๋ผ, ๊ฐ ์์๋ฅผ ์ ๋ฆฌํด ๋ก๋๋ค.
๊ฒ์ํด์ ๋ค์ด์ค์ ๋ถ๋ค๊ป ๋์์ด ๋ ๋งํ ์์๋ก ์ ๋ฆฌํฉ๋๋ค.
- Generics โ bounded์ variance
- sealed class (Dart 3)
- freezed ํจํค์ง โ ์ ์ฐ๋๊ฐ
- ์ค์ ์ฝ๋:
AppError+Result<T> - ํ๊ณ
1. Dart์ Generics
C++ ํ ํ๋ฆฟ์ด๋ Java/Kotlin์ generic์ฒ๋ผ ํ์ ํ๋ผ๋ฏธํฐ๋ฅผ ์ ์ธํด ์ฝ๋๋ฅผ ์ฌ์ฌ์ฉํ ์ ์๊ฒ ๋ง๋๋ ๊ธฐ๋ฅ์ ๋๋ค. Dart์ generic์ invariant๊ฐ ๊ธฐ๋ณธ์ด๊ณ , ํค์๋๋ก covariance๋ฅผ ์ฝํ์ํฌ ์ ์๋ค๋ ์ ์ด ๋ค๋ฅธ ์ธ์ด์ ๋น์ทํฉ๋๋ค.
๊ธฐ๋ณธ ์ฌ์ฉ
1
2
3
4
5
6
7
class Box<T> {
Box(this.value);
final T value;
}
final intBox = Box<int>(42);
final stringBox = Box<String>('hello');
Bounded generics โ extends
ํ์
ํ๋ผ๋ฏธํฐ์ ์ํ์ ๋ ์ ์์ต๋๋ค. Java์ T extends Comparable<T>์ ๊ฐ์ ์๋ฏธ์
๋๋ค.
1
2
3
4
5
6
T maxOf<T extends Comparable<T>>(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
maxOf(3, 5); // OK โ int๋ Comparable<int>
maxOf('apple', 'b'); // OK โ String์ Comparable<String>
Generic ํจ์์ ์ถ๋ก
ํ์ ํ๋ผ๋ฏธํฐ๋ ํธ์ถ ์์ ์ ์ธ์๋ก๋ถํฐ ์ถ๋ก ๋ฉ๋๋ค. ๋ช ์์ ์ผ๋ก ๋ฐ๋ ๊ฒฝ์ฐ๋ ์ถ๋ก ์ด ์ ๋ ๋๋ง์ ๋๋ค.
1
2
3
4
List<T> wrap<T>(T value) => [value];
final ints = wrap(1); // List<int>๋ก ์ถ๋ก
final strings = wrap<String>(null as dynamic); // ๋ช
์ ํ์
2. Sealed Class โ Dart 3์ Sum Type
sealed๋ Dart 3์์ ์ ์ ๋์
๋ ํค์๋๋ก, ํ์ ํด๋์ค๋ฅผ ๊ฐ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ(ํ์ผ) ์์์๋ง ์ ์ํ ์ ์๊ฒ ์ ํํฉ๋๋ค. Kotlin์ sealed class, Rust์ enum(๋ฐ์ดํฐ๋ฅผ ๊ฐ์ง ์ ์๋ enum), TypeScript์ discriminated union๊ณผ ๊ฐ์ sum type ๊ฐ๋
์
๋๋ค.
์ ํ์ํ๊ฐ
์ผ๋ฐ ์ถ์ ํด๋์ค๋ ์ธ๋ถ์์ ์์๋ก ์์๋ ์ ์์ด์, ๋ชจ๋ ๊ฒฝ์ฐ๋ฅผ ํ ์๋ฆฌ์์ ๋ง๋ผํ๋ ํจํด ๋งค์นญ์ด ์ปดํ์ผ๋ฌ ์ฐจ์์์ ๋ณด์ฅ๋์ง ์์ต๋๋ค. sealed๋ ํ์ ์ข
๋ฅ๊ฐ ๋ซํ ์์ด switch๋ก ๋ถ๊ธฐํ ๋ ์ปดํ์ผ๋ฌ๊ฐ exhaustiveness(๋ชจ๋ ์ผ์ด์ค๋ฅผ ๋ค ์ฒ๋ฆฌํ๋์ง)๋ฅผ ๊ฒ์ฌํด ์ค๋๋ค.
1
2
3
4
5
6
7
8
9
sealed class Shape {}
class Circle extends Shape { final double r; Circle(this.r); }
class Square extends Shape { final double a; Square(this.a); }
double area(Shape s) => switch (s) {
Circle(:final r) => 3.14 * r * r,
Square(:final a) => a * a,
// ์ผ์ด์ค๋ฅผ ๋น ๋จ๋ฆฌ๋ฉด ์ปดํ์ผ ์๋ฌ
};
์ฌ๊ธฐ์ switch (s) { ... } ํํ๋ switch expression์
๋๋ค(๋ด์ผ ๋ณ๋ ๊ธ๋ก ์ ๋ฆฌํ ์์ ์
๋๋ค). ํต์ฌ์ Circle/Square ๋ ์ผ์ด์ค๋ฅผ ๋ชจ๋ ๋ค๋ฃจ์ง ์์ผ๋ฉด ์ปดํ์ผ๋ฌ๊ฐ ๋ง์์ค๋ค๋ ์ ์
๋๋ค.
3. freezed โ ์ ์ฐ๋๊ฐ
AppError ๊ฐ์ sealed ๊ณ์ธต์ ์ง์ ์์ผ๋ก ์ง๋ฉด ๋ณด์ผ๋ฌํ๋ ์ดํธ๊ฐ ๋ง์ต๋๋ค.
- ๊ฐ ๋ณํ(variant)๋ง๋ค ๋ณ๋ ํด๋์ค ์ ์ธ
==/hashCode์ง์ ๊ตฌํcopyWith์ง์ ๊ตฌํtoString๊ฐ๋ ์ฑ์ฉ override- JSON ์ง๋ ฌํ๊ฐ ํ์ํ๋ฉด fromJson/toJson
freezed ํจํค์ง๋ ์ด๋ ธํ ์ด์ ์ ๋ถ์ด๋ฉด ์์ ๋ณด์ผ๋ฌํ๋ ์ดํธ๋ฅผ ๋ชจ๋ ์ฝ๋ ์์ฑ์ผ๋ก ๋ง๋ค์ด ์ค๋๋ค. ์บก์คํค์์๋ ๋๋ฉ์ธ ๋ชจ๋ธ๊ณผ ์ํ(state) ํํ ๊ฑฐ์ ๋ชจ๋์ freezed๋ฅผ ์ ์ฉํ ๊ณํ์ ๋๋ค.
์์กด์ฑ ์ถ๊ฐ
1
2
3
4
5
6
7
8
dependencies:
freezed_annotation: ^3.0.0
json_annotation: ^4.9.0
dev_dependencies:
build_runner: ^2.4.13
freezed: ^3.0.0
json_serializable: ^6.8.0
*_annotation์ ๋ฐํ์ ์์กด์ฑ(์ด๋
ธํ
์ด์
ํด๋์ค๋ง ๋ค์ด ์์), ๋ณธ์ฒด๋ dev_dependencies๋ก ๋ค์ด๊ฐ์ ๋น๋ ์์ ์๋ง ๋์ํฉ๋๋ค.
์ฝ๋ ์์ฑ ์คํ
1
dart run build_runner build --delete-conflicting-outputs
*.g.dart, *.freezed.dart ํ์ผ์ด ์๋์ผ๋ก ์์ฑ๋ฉ๋๋ค. watch ๋ชจ๋(build_runner watch)๋ก ๋๋ฉด ํ์ผ์ด ๋ฐ๋ ๋๋ง๋ค ์๋ ์ฌ์์ฑ๋ฉ๋๋ค.
4. ์ค์ ์ฝ๋ โ AppError + Result<T>
์ด์ ์ง core/errors/์ ํต์ฌ ๋ ํ์
์ ์ ๋ฆฌํฉ๋๋ค. ๋ ํ์
๋ชจ๋ sealed + generics + freezed์ ์กฐํฉ์
๋๋ค.
AppError โ ๋๋ฉ์ธ ์๋ฌ sealed union
1
2
3
4
5
6
7
8
9
10
11
import 'package:freezed_annotation/freezed_annotation.dart';
part 'app_error.freezed.dart';
@freezed
sealed class AppError with _$AppError {
const factory AppError.network(String message) = NetworkError;
const factory AppError.notFound(String resource) = NotFoundError;
const factory AppError.unauthorized() = UnauthorizedError;
const factory AppError.unknown(Object error, [StackTrace? trace]) =
UnknownError;
}
๊ฐ factory๋ ๋ณ๋์ variant๊ฐ ๋ฉ๋๋ค. NetworkError, NotFoundError, UnauthorizedError, UnknownError๊ฐ ๋ชจ๋ AppError์ ํ์ ํ์
์ด๊ณ , freezed๊ฐ ์๋์ผ๋ก ๋ง๋ค์ด ์ค๋๋ค.
ํธ์ถ๋ถ์์๋ ๋ค์์ฒ๋ผ ๋ณํ์ ํจํด ๋งค์นญ์ผ๋ก ๋ถ๊ธฐํฉ๋๋ค.
1
2
3
4
5
6
String describe(AppError e) => switch (e) {
NetworkError(:final message) => '๋คํธ์ํฌ ์ค๋ฅ: $message',
NotFoundError(:final resource) => '$resource์(๋ฅผ) ์ฐพ์ ์ ์์ต๋๋ค',
UnauthorizedError() => '๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค',
UnknownError(:final error) => '์ ์ ์๋ ์ค๋ฅ: $error',
};
์ variant๋ฅผ ์ถ๊ฐํ๋ฉด ์ด switch๊ฐ ์ปดํ์ผ๋ฌ ์๋ฌ๋ก ๋งํ์ ๋ชจ๋ ์ฌ์ฉ์ฒ๋ฅผ ๊ฐ์ ๋ก ์ ๋ฐ์ดํธํ๊ฒ ๋ฉ๋๋ค. ์๋ฌ ์ฒ๋ฆฌ ๋๋ฝ์ ์ปดํ์ผ๋ฌ๊ฐ ์ก์์ฃผ๋ ํจํด์ด๋ผ ์์ ํฉ๋๋ค.
Result<T> โ Either์ ์ฌ๋ฆผ ๋ฒ์
Either / Result ํจํด์ ํจ์ํ ์ธ์ด์์ ์์ฃผ ๋ณด๋ โ์ฑ๊ณต ๋๋ ์คํจ๋ฅผ ๊ฐ์ผ๋ก ํํโ ํธ๋ฆญ์
๋๋ค. Dart์๋ ํ์ค Result ํ์
์ด ์์ด์, sealed + generics๋ก ์ง์ ๋ง๋ค์์ต๋๋ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
sealed class Result<T> {
const Result();
}
class Ok<T> extends Result<T> {
const Ok(this.value);
final T value;
}
class Err<T> extends Result<T> {
const Err(this.error);
final AppError error;
}
์ฌ๊ธฐ์ T๋ generic ํ์
ํ๋ผ๋ฏธํฐ์
๋๋ค. Result<User>, Result<List<Meal>>์ฒ๋ผ ์์์ ๋๋ฉ์ธ ํ์
์ ๋ด์ ์ ์์ต๋๋ค.
Controller(Riverpod notifier)์์์ ์ฌ์ฉ
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class DietController extends StateNotifier<AsyncValue<DietDay>> {
DietController(this._repository) : super(const AsyncLoading());
final DietRepository _repository;
Future<void> load() async {
state = const AsyncLoading();
final result = await _repository.fetchToday();
state = switch (result) {
Ok(:final value) => AsyncData(value),
Err(:final error) => AsyncError(error, StackTrace.current),
};
}
}
switch๋ก ๋ ์ผ์ด์ค๋ฅผ ํ ์๋ฆฌ์์ ๋ถ๊ธฐํ๋, repository๊ฐ ๋์ง ์ ์๋ ๋ชจ๋ ๊ฒฐ๊ณผ๋ฅผ controller์์ ๋ช
์์ ์ผ๋ก ๋ค๋ฃจ๊ฒ ๋ฉ๋๋ค. try/catch๋ก ํ๋ฉด ์ด๋์ ์ด๋ค ์์ธ๊ฐ ์ด๋๋ก ํ๋ฌ๊ฐ๋์ง ์ถ์ ์ด ์ด๋ ค์ด๋ฐ, sealed + Result๋ก ๋ช
์ํ๋ฉด ๋ฐ์ดํฐ ํ๋ฆ์ด ํ๋์ ๋ณด์
๋๋ค.
5. ํ๊ณ โ sum type์ด ํฌ์ค์ผ์ด ๋๋ฉ์ธ์ ์ ๋ง๋ ์ด์
์บก์คํค์ ๋ฐ์ดํฐ ๋ชจ๋ธ์ ๋ณด๋ฉด sum type์ด ์์ฐ์ค๋ฝ๊ฒ ๋ฑ์ฅํ๋ ์๋ฆฌ๊ฐ ๋ง์ต๋๋ค.
- ์ด๋ ์ข ๋ฅ โ ์ ์ฐ์ / ๊ทผ๋ ฅ / ์คํธ๋ ์นญ (์ธ ๊ฐ์ง๊ฐ ๋ชจ๋ ๋ค๋ฅธ ํ๋๋ฅผ ๊ฐ์ง)
- ์๋ฆผ ์ข ๋ฅ โ ์์ฌ / ์ด๋ / ์ ๊ธฐ ์ ๊ฒ (๊ฐ ํ์ด๋ก๋๊ฐ ๋ค๋ฆ)
- ๊ฑด๊ฐ ์ํ๋ โ ์ ์ / ๊ฒฝ๊ณ / ์ํ (๊ตฌ๊ฐ๋ณ ๋ฉ์์ง ๋ค๋ฆ)
- ๋ถ์ ๊ฒฐ๊ณผ ์ํ โ Pending / Success / Failure
์ด ๋ชจ๋ ์ผ์ด์ค๋ฅผ String ํ๋ ํ ์ค๋ก ํํํ๋ฉด, ์ฝ๋ ๊ณณ๊ณณ์ if (kind == 'cardio') ...๊ฐ ํฉ์ด์ง๋ฉด์ ์ ์ข
๋ฅ๋ฅผ ์ถ๊ฐํ์ ๋ ๋ฌด์์ ์์ ํด์ผ ํ๋์ง๊ฐ ํ๋์ ์ ๋ณด์
๋๋ค. sealed class๋ก ๋ชจ๋ธ๋งํ๋ฉด ์ปดํ์ผ๋ฌ๊ฐ ๋น ๋จ๋ฆฐ ๊ณณ์ ์ฝ ์ง์ด์ฃผ๊ธฐ ๋๋ฌธ์, ๋๋ฉ์ธ์ด ํ์ฅ๋ ๋ ๋ฆฌ์คํฌ๊ฐ ๋ฎ์์ง๋๋ค.
6. ์ ๋ฆฌ / ๋ค์ ํ์ต ๊ณํ
์ค๋ ์ ๋ฆฌํ ํต์ฌ์ ๋ค์ ์ธ ๊ฐ์ง์ ๋๋ค.
- Generics๋ invariant๊ฐ ๊ธฐ๋ณธ์ด๊ณ ,
extends๋ก ์ํ์ ๋ ์ ์์ต๋๋ค.Result<T>์ฒ๋ผ ์์์ ๋๋ฉ์ธ ํ์ ์ ๋ด๋ ์ปจํ ์ด๋ ๋ง๋ค ๋ ํ์์ ๋๋ค. - sealed class๋ Dart 3์ sum type์ด๊ณ , switch ๋ถ๊ธฐ์์ ์ปดํ์ผ๋ฌ๊ฐ exhaustiveness๋ฅผ ๊ฒ์ฌํด ์ค๋๋ค. ๋๋ฉ์ธ ์๋ฌ / ์๋ฆผ ์ข ๋ฅ / ์ด๋ ์นดํ ๊ณ ๋ฆฌ ๊ฐ์ ๋ซํ ์งํฉ ํํ์ ์ ํฉํฉ๋๋ค.
- freezed๋ sealed + immutable data class + copyWith + ๋๋ฑ์ฑ ๋น๊ต๋ฅผ ํ ๋ฒ์ ์๋ ์์ฑํด ์ค๋๋ค. ์บก์คํค์ ๋ชจ๋ ๋๋ฉ์ธ ๋ชจ๋ธ์ ์ ์ฉํ ๊ณํ์ ๋๋ค.
๋ค์ ํ์ต์ ์ค๋ ์ฝ๋์ ์ด์ง ๋ฑ์ฅํ switch expression๊ณผ ํจํด ๋งค์นญ โ Dart 3์์ ์๋ก ๋ค์ด์จ ๊ธฐ๋ฅ๋ค์ ๋ณธ๊ฒฉ์ ์ผ๋ก ์ ๋ฆฌํ๋ ค ํฉ๋๋ค. ์บก์คํค ํ๋ฐ์ ๋ณธ๊ฒฉ์ ์ผ๋ก ์ฐ๊ฒ ๋ ๊ฑฐ๋ผ ๋ฏธ๋ฆฌ ํ์ต ๋
ธํธ๋ก ๋จ๊ฒจ ๋ ์์ ์
๋๋ค.
๐ ์์ฝ
AppError sealed์ Result<T> generic ํ์
์ ๋ง๋ค๋ฉด์ ํ์ตํ ๋ด์ฉ์ ์ ๋ฆฌํ์ต๋๋ค. Generics๋ ์์ ๋๋ฉ์ธ ํ์
์ ๋ด๋ ์ปจํ
์ด๋๋ฅผ ๋ง๋ค ๋ ์ฐ๊ณ , sealed class๋ ์ปดํ์ผ๋ฌ๊ฐ exhaustiveness๋ฅผ ๊ฒ์ฌํด ์ฃผ๋ ๋ซํ ํฉ ํ์
์ด๋ฉฐ, freezed๋ ์ด๋
ธํ
์ด์
ํ ์ค๋ก immutable data class + copyWith + ๋๋ฑ์ฑ์ ์๋ ์์ฑํฉ๋๋ค. ์ธ ๊ฐ์ง๋ฅผ ํฉ์น๋ฉด ํฌ์ค์ผ์ด ๋๋ฉ์ธ์ ๋ซํ ๋ณํ๋ค(์๋ฌยท์ด๋ ์ข
๋ฅยท์ํ๋ ๋ฑ)์ ์์ ํ๊ฒ ๋ชจ๋ธ๋งํ ์ ์๊ณ , ์ ๋ณํ์ ์ถ๊ฐํ์ ๋ ์ปดํ์ผ๋ฌ๊ฐ ์ฌ์ฉ์ฒ๋ฅผ ๊ฐ์ ๋ก ์
๋ฐ์ดํธํ๊ฒ ๋ง๋ค์ด ์ค๋๋ค.