머가더나음?
| topics | 500-모바일개발 501 Flutter 503 Dart |
| types | 학습 실습 |
| tags |
Abstract 클래스로 다형성 JSON 처리
서버에서 type 필드에 따라 다른 구조의 JSON이 올 때, abstract 클래스를 사용해서 타입 안전하게 처리하는 방법이다.
언제 필요한가
채팅 앱처럼 여러 종류의 메시지가 있을 때:
METADATA: 세션 정보, 유저 목록 포함PENDING: 대기 중인 채팅 목록MESSAGE: 일반 채팅 메시지ENTRY: 입장 알림EXIT: 퇴장 알림
왜 이렇게 하냐면: 각 타입마다 필드가 다르다. 그냥 하나의 클래스로 처리하면 nullable 필드 투성이가 되거나, dynamic을 남발하게 된다.
구현 패턴
1. Abstract 클래스 정의
@JsonSerializable()
abstract class MessageResponse {
final String id;
final DateTime timestamp;
final String type;
MessageResponse({
required this.id,
required this.timestamp,
required this.type,
});
// factory에서 type 기준으로 분기
factory MessageResponse.fromJson(Map<String, dynamic> json) {
final type = json['type'] as String;
switch (type) {
case 'METADATA':
return MetadataMessage.fromJson(json);
case 'PENDING':
return PendingMessage.fromJson(json);
default:
throw Exception('Unknown message type: $type');
}
}
Map<String, dynamic> toJson(); // 추상 메서드
}
2. 하위 클래스 구현
@JsonSerializable()
class MetadataMessage extends MessageResponse {
final String sessionId;
final List<MyUser> users;
final List<ChatItem> chats;
MetadataMessage({
required super.id,
required super.timestamp,
required super.type,
required this.sessionId,
required this.users,
required this.chats,
});
factory MetadataMessage.fromJson(Map<String, dynamic> json) =>
_$MetadataMessageFromJson(json);
@override
Map<String, dynamic> toJson() => _$MetadataMessageToJson(this);
}
@JsonSerializable()
class PendingMessage extends MessageResponse {
final List<ChatItem> chats;
PendingMessage({
required super.id,
required super.timestamp,
required super.type,
required this.chats,
});
factory PendingMessage.fromJson(Map<String, dynamic> json) =>
_$PendingMessageFromJson(json);
@override
Map<String, dynamic> toJson() => _$PendingMessageToJson(this);
}
3. 중첩 다형성 (ChatItem)
ChatItem도 타입에 따라 다른 구조:
@JsonSerializable()
abstract class ChatItem {
final String id;
final DateTime timestamp;
final String type;
final String userId;
ChatItem({
required this.id,
required this.timestamp,
required this.type,
required this.userId,
});
factory ChatItem.fromJson(Map<String, dynamic> json) {
final type = json['type'] as String;
switch (type) {
case 'MESSAGE':
return ChatMessage.fromJson(json);
case 'ENTRY':
return EntryMessage.fromJson(json);
case 'EXIT':
return ExitMessage.fromJson(json);
default:
throw Exception('Unknown chat type: $type');
}
}
Map<String, dynamic> toJson();
}
@JsonSerializable()
class ChatMessage extends ChatItem {
final String content;
ChatMessage({
required super.id,
required super.timestamp,
required super.type,
required super.userId,
required this.content,
});
factory ChatMessage.fromJson(Map<String, dynamic> json) =>
_$ChatMessageFromJson(json);
@override
Map<String, dynamic> toJson() => _$ChatMessageToJson(this);
}
사용 예시
void handleMessage(Map<String, dynamic> json) {
final message = MessageResponse.fromJson(json);
// 패턴 매칭으로 타입별 처리
switch (message) {
case MetadataMessage(:final users, :final sessionId):
print('세션 $sessionId, 유저 ${users.length}명');
case PendingMessage(:final chats):
print('대기 메시지 ${chats.length}개');
}
}
Freezed로 더 간결하게
이 패턴을 Freezed의 Union Types로 구현하면 더 간결하다:
@freezed
sealed class ChatItem with _$ChatItem {
const factory ChatItem.message({
required String id,
required DateTime timestamp,
required String userId,
required String content,
}) = ChatMessage;
const factory ChatItem.entry({
required String id,
required DateTime timestamp,
required String userId,
}) = EntryMessage;
const factory ChatItem.exit({
required String id,
required DateTime timestamp,
required String userId,
}) = ExitMessage;
factory ChatItem.fromJson(Map<String, dynamic> json) =>
_$ChatItemFromJson(json);
}
Freezed의 장점: 보일러플레이트가 줄어들고,
when/map메서드로 exhaustive 패턴 매칭이 가능하다.
비교
| 방식 | 장점 | 단점 |
|---|---|---|
| Abstract + JsonSerializable | 세밀한 제어 가능 | 코드가 장황함 |
| Freezed Union Types | 간결, exhaustive 체크 | 러닝커브, 빌드 시간 |
관련 문서
- freezed,JsonSerialiable - Freezed 사용법
- 결과처리 - Result 패턴
- dart 3.0 - 패턴 매칭