본문 바로가기
Flutter

[Flutter 18] Dart에서 추상 클래스와 동적 바인딩 활용

by JINJINC 2025. 1. 7.
728x90
반응형

이번 글에서는 Dart에서 추상 클래스동적 바인딩을 활용한 객체 지향 설계를 예제로 설명합니다.
추상 클래스의 개념부터 동적 바인딩을 사용해 유연하고 확장 가능한 코드를 설계하는 방법까지 살펴보겠습니다.


1. 팀장의 요청: 동물의 행동 구현

1-1. 초기 설계

처음에는 각각의 동물 클래스에 독립적인 메서드를 정의했습니다. 하지만, 물고기(Fish) 클래스에 다른 메서드(hungry())를 사용하면서 일관성이 깨졌습니다.

 

class Dog {
  void performAction() {
    print('멍멍 배고파');
  }
}

class Cat {
  void performAction() {
    print('야옹 배고파');
  }
}

class Fish {
  void hungry() {
    print('뻐금뻐금 배고파');
  }
}

void main() {
  Dog d = Dog();
  Cat c = Cat();
  d.performAction();
  c.performAction();

  Fish f = Fish();
  f.hungry(); // Fish 클래스는 메서드가 다르므로 혼란 발생
}

문제점

  • 각 클래스에서 메서드 이름이 통일되지 않아 코드의 일관성이 부족합니다.
  • 동적 바인딩이나 다형성을 사용할 수 없어 확장성이 떨어집니다.

1-2. 추상 클래스 사용으로 문제 해결

Animal 추상 클래스를 정의하여 모든 동물이 일관된 인터페이스를 따르도록 설계합니다.

 

abstract class Animal {
  void performAction(); // 추상 메서드
}

class Dog implements Animal {
  @override
  void performAction() {
    print('멍멍 배고파');
  }
}

class Cat implements Animal {
  @override
  void performAction() {
    print('야옹 배고파');
  }
}

class Fish implements Animal {
  @override
  void performAction() {
    print('뻐금뻐금 배고파');
  }
}

// 동적 바인딩 활용
void start(Animal animal) {
  animal.performAction();
}

void main() {
  start(Dog());
  start(Cat());
  start(Fish());
}

 

출력 결과

 
멍멍 배고파
야옹 배고파
뻐금뻐금 배고파

동적 바인딩의 장점

  • start() 함수는 Animal 타입의 참조를 받아, 실제 객체 타입에 따라 적절한 메서드를 실행합니다.
  • 코드의 확장성과 유지보수성이 높아집니다.

2. 도형의 넓이 계산 프로그램

2-1. 초기 설계: 추상 클래스와 동적 바인딩

도형(원, 직사각형)의 넓이를 계산하는 프로그램을 설계합니다.
모든 도형은 Shape 추상 클래스를 구현하며, 넓이를 계산하는 공통된 메서드(getArea)와 출력 메서드(displayArea)를 제공합니다.

 

import 'dart:math';

// 추상 클래스
abstract class Shape {
  void displayArea(); // 추상 메서드
  double getArea();   // 추상 메서드
}

// 원 클래스
class Circle implements Shape {
  final double radius;

  Circle(this.radius);

  @override
  void displayArea() {
    double area = getArea();
    print('원의 넓이 : ${area}');
  }

  @override
  double getArea() {
    return radius * radius * pi;
  }
}

// 직사각형 클래스
class Rectangle implements Shape {
  final double width;
  final double height;

  Rectangle(this.width, this.height);

  @override
  void displayArea() {
    double area = getArea();
    print('직사각형의 넓이 : ${area}');
  }

  @override
  double getArea() {
    return width * height;
  }
}

// 동적 바인딩을 활용한 전역 함수
void calculateArea(Shape s) {
  print(s.getArea());
  s.displayArea();
}

void main() {
  Shape myCircle = Circle(5.0);
  Shape myRectangle = Rectangle(4.0, 6.0);

  calculateArea(myCircle);   // 원의 넓이 계산 및 출력
  calculateArea(myRectangle); // 직사각형의 넓이 계산 및 출력
}

78.53981633974483
원의 넓이 : 78.53981633974483
24.0
직사각형의 넓이 : 24.0

 

2-3. 동적 바인딩 활용

  • calculateArea()는 Shape 타입을 받아, 런타임에 실제 객체 타입(Circle 또는 Rectangle)에 따라 동작합니다.
  • 새로운 도형 클래스(Triangle, Square 등)를 추가해도 기존 코드를 수정하지 않고 확장 가능합니다.

3. 요약 및 결론

추상 클래스와 동적 바인딩의 장점

  1. 일관성:
    추상 클래스는 공통된 인터페이스를 제공하여 코드의 일관성을 유지합니다.
  2. 확장성:
    동적 바인딩을 활용하면 새로운 클래스 추가 시 기존 코드를 수정할 필요 없이 확장할 수 있습니다.
  3. 유지보수성:
    공통 로직을 추상 클래스에서 정의하고, 세부 구현은 하위 클래스에서 처리하여 코드 재사용성을 높입니다.

코드 설계의 개선 과정

  • 초기 설계에서는 메서드 이름과 구조가 일관되지 않아 코드 유지보수가 어려웠습니다.
  • 추상 클래스와 동적 바인딩을 도입하여 일관성, 유연성, 확장성을 확보했습니다.

적용 가능한 상황

  • 동물의 행동, 도형의 넓이 계산과 같은 다양한 객체 지향 설계 상황에서 활용할 수 있습니다.
  • 실시간 데이터 처리, 이벤트 기반 시스템 등에서도 동적 바인딩은 강력한 도구입니다.

Dart의 추상 클래스와 동적 바인딩을 활용해 보다 유연하고 확장 가능한 프로그램을 설계해보세요!

 

 

Dart에서 **제너릭(Generic)**을 사용하여 추상 클래스의 리턴 타입을 지정하려면, 추상 클래스 정의 시 제너릭 타입 매개변수를 선언하고 이를 메서드의 리턴 타입으로 활용하면 됩니다. 이렇게 하면 리턴 타입이 호출 시점에 명확하게 지정되므로, 더욱 유연하고 타입 안정성이 높은 코드를 작성할 수 있습니다.


1. 제너릭 추상 클래스 정의

abstract class Animal<T> {
  T performAction(); // 제너릭 타입을 리턴 타입으로 지정
}
  • <T>: 제너릭 타입 매개변수를 선언합니다.
  • performAction(): 제너릭 타입 T를 리턴 타입으로 사용합니다.

2. 제너릭 추상 클래스를 구현

구체적인 타입으로 추상 클래스를 구현하여 제너릭 메서드를 정의합니다.

 

class Dog implements Animal<String> {
  @override
  String performAction() {
    return '멍멍 배고파';
  }
}

class Cat implements Animal<String> {
  @override
  String performAction() {
    return '야옹 배고파';
  }
}

class Fish implements Animal<int> {
  @override
  int performAction() {
    return 42; // 물고기 소리 대신 숫자를 반환
  }
}

 

  • DogCat 클래스는 String을 리턴 타입으로 지정합니다.
  • Fish 클래스는 int를 리턴 타입으로 지정합니다.

 

3. 동적 바인딩과 제너릭 추상 클래스 활용

void start<T>(Animal<T> animal) {
  T result = animal.performAction();
  print(result);
}

void main() {
  start(Dog()); // 출력: 멍멍 배고파
  start(Cat()); // 출력: 야옹 배고파
  start(Fish()); // 출력: 42
}

start<T>():

  • 제너릭 함수를 정의하여 다양한 Animal 타입의 객체를 받아 처리합니다.
  • performAction() 호출 후 결과를 출력합니다.

 

4. 제너릭 리턴 타입을 활용한 추가 예제

다양한 타입을 처리할 수 있는 프로그램을 설계할 때, 제너릭 리턴 타입을 활용하면 재사용성을 극대화할 수 있습니다.

추상 클래스

abstract class Repository<T> {
  T fetchById(int id); // 특정 ID로 데이터를 가져옴
}

 

구체적인 구현

class UserRepository implements Repository<String> {
  @override
  String fetchById(int id) {
    return 'User_$id'; // 사용자 이름 반환
  }
}

class ProductRepository implements Repository<Map<String, dynamic>> {
  @override
  Map<String, dynamic> fetchById(int id) {
    return {'id': id, 'name': 'Product_$id'}; // 제품 정보 반환
  }
}

제너릭 활용

void main() {
  Repository<String> userRepo = UserRepository();
  Repository<Map<String, dynamic>> productRepo = ProductRepository();

  print(userRepo.fetchById(1)); // 출력: User_1
  print(productRepo.fetchById(2)); // 출력: {id: 2, name: Product_2}
}

5. 실생활 적용: API 응답 처리

제너릭 추상 클래스를 사용하여 다양한 데이터 타입을 처리할 수 있는 API 클라이언트를 설계할 수 있습니다.

abstract class ApiResponse<T> {
  T processResponse(Map<String, dynamic> response);
}

class UserResponse implements ApiResponse<String> {
  @override
  String processResponse(Map<String, dynamic> response) {
    return 'User: ${response['name']}';
  }
}

class ProductResponse implements ApiResponse<Map<String, dynamic>> {
  @override
  Map<String, dynamic> processResponse(Map<String, dynamic> response) {
    return {'id': response['id'], 'name': response['name']};
  }
}

void main() {
  ApiResponse<String> userResponse = UserResponse();
  ApiResponse<Map<String, dynamic>> productResponse = ProductResponse();

  Map<String, dynamic> userApiData = {'id': 1, 'name': 'Alice'};
  Map<String, dynamic> productApiData = {'id': 101, 'name': 'Laptop'};

  print(userResponse.processResponse(userApiData)); // 출력: User: Alice
  print(productResponse.processResponse(productApiData)); // 출력: {id: 101, name: Laptop}
}

6. 요약

제너릭 추상 클래스의 장점

  1. 유연성: 다양한 데이터 타입에 대해 동작할 수 있습니다.
  2. 타입 안전성: 컴파일 타임에 타입 검사를 통해 안전성을 보장합니다.
  3. 코드 재사용: 여러 클래스에서 동일한 로직을 재사용하면서 타입에 따라 유연하게 동작합니다.

제너릭 추상 클래스 활용 패턴

  1. 공통 인터페이스 제공: 다양한 타입을 처리하는 로직을 통합합니다.
  2. 다형성과 결합: 제너릭과 동적 바인딩을 결합하여 확장 가능한 구조를 설계합니다.
  3. 실생활 적용: 데이터베이스, API 클라이언트, 컬렉션 처리 등 다양한 상황에서 활용 가능합니다.

Dart에서 제너릭과 추상 클래스를 활용하여 더 유연하고 강력한 코드를 작성해보세요! 😊

728x90
반응형

댓글