머가더나음?

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 체크 러닝커브, 빌드 시간

관련 문서