Frontend/Flutter

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

JINJINC 2025. 1. 7. 12:50
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
반응형