Flutter/Dart

[DART] OOP 연관관계와 Mixin

연화 2025. 1. 7. 20:00

 

객체지향 프로그래밍(OOP)의 중요한 개념 중 하나는 연관관계(Association)입니다.
연관관계는 객체 간의 관계를 정의하며, 특히 객체들의 생명주기와 소유권에 따라 구분됩니다.
이 글에서는 연관관계의 두 가지 주요 형태인 컴포지션(Composition)집합(Aggregation) 관계를 Dart 언어를 사용하여 구현하고, 더 나아가 코드 재사용성을 높이는 Mixin에 대해 살펴보겠습니다.

 

1. 연관관계란?

연관관계는 객체 간의 상호작용과 관계를 나타냅니다. 연관관계는 소유권생명주기에 따라 다음과 같이 분류됩니다.

1.1. 컴포지션 관계 (Composition)

  • 강한 소유 관계
  • 부분-전체 관계에서 **전체(Whole)**와 **부분(Part)**의 생명주기가 밀접하게 연관되어 있습니다.
  • 전체 객체가 소멸되면 부분 객체도 함께 소멸됩니다.
  • ex) 차/엔진, 사람/심장

1.2. 집합 관계 (Aggregation)

  • 약한 소유 관계
  • 부분-전체 관계에서 전체와 부분의 생명주기가 독립적입니다.
  • 전체 객체가 소멸되어도 부분 객체는 독립적으로 존재할 수 있습니다.
  • ex) 부서/소속사원

2. 컴포지션 관계 구현

컴포지션 관계를 Dart로 구현하려면 전체 객체가 부분 객체를 소유하는 형태로 설계합니다. 아래 코드는 Car 클래스가 Engine 객체를 소유하며, 차가 소멸되면 엔진도 함께 소멸되는 관계를 나타냅니다.

시나리오 코드1-1 (컴포지션 관계 구현)
class Engine {
    final String type;
    Engine(this.type);
}

class Car {
	final Engine engine;
	Car(this.engine);
}

void main() {
  	Engine v8engine = Engine('v8');
	Car car1 = Car(v8engine);
}
💡 그냥 엔진을 받아서 생성자 초기화를 해주는 식으로 설계하면 메인 함수에서 차를 인스턴스화시킬 때 엔진을 만들어 직접 넣어주어야 함
시나리오 코드 1-2 (개선된 컴포지션 관계 구현)
class Engine {
  final String type;
  Engine(this.type);
}

class Car {
  final Engine engine;

  // 생성자 코드이다 1. 축약 버전 --> 생성자 바디 부분을 생략했다.
  Car(String engineType) : engine = Engine(engineType);
  // 생성자 코드이다 2. 비축약 버전
  Car(String engineType) : engine = Engine(engineType) {
    print('생성자 호출시 내부 스택 메모리가 호출된다.');
  }
}

void main() {
  // Engine v8engine = Engine('v8');
  // Car car1 = Car(v8engine);

  Car car1 = Car('v8');
  // 누군가 참조하고 있지 않다면 gc(garbage collection) 대상이 된다.
}
💡 이니셜라이저 리스트를 활용해 Car 클래스를 호출하여 객체로 만들 때 엔진의 이름을 넣어주면 생성자 내부에서 엔진을 호출해서 만들어준다
시나리오 코드 1-3 (집합 관계 구현)
class Employee {
  final String name;

  Employee(this.name);

  void displayEmployeeInfo() {
    print('직원 이름: ${name}');
  }
}

class Department {
  final String deptName;
  final List<Employee> employees;

  Department(this.deptName) : employees = [] {
    print('=== Department 생성자 내부 스택 호출 ===');
  }

  void addEmployee(Employee emp) {
    employees.add(emp);
  }

  void displayDepartmentInfo() {
    print('------------------------');
    print('부서 이름 : ${deptName}');
    for (var emp in employees) {
      emp.displayEmployeeInfo();
    }
  }
}

void main() {
    Department dept1 = Department('개발팀');
    Department dept2 = Department('디자인팀');
    Employee emp1 = Employee('홍길동');
    Employee emp2 = Employee('김철수');
    Employee emp3 = Employee('야스오');

    dept1.addEmployee(emp1);
    dept1.addEmployee(emp2);
    dept2.addEmployee(emp3);

    dept1.displayDepartmentInfo();
    dept2.displayDepartmentInfo();

  // 부서가 소멸되어도 직원은 독립적으로 존재
}

 

객체 지향 프로그래밍 패러다임을 따를 때, 연관관계를 활용하는 것은 효과적입니다.
Dart에서는 OOP의 상속, 조합과 같은 연관성 구현을 보완하거나 대체할 수 있는 도구로  Mixin 문법을 제공하여, 컴포지션을 사용하지 않고도 여러 클래스에서 코드 조각을 재사용할 수 있습니다.

지금부터 Dart에서 지원하는 Mixin 문법에 대해 알아보도록 하겠습니다.

 

 

시나리오 코드 2 - mixin 사용
// 믹스인 사용해보기
// 여러 계층에서 코드를 재사용할 수 있도록 하는 코드 조각

mixin Engine {
  int power = 5000;
}

mixin Wheel {
  String wheelName = '4륜구동 바퀴';
}

class BMW with Engine, Wheel {}

void main() {
  // 인스턴스화 시킴
  BMW bmw = BMW();
  print(bmw.power);
  print(bmw.wheelName);

  //상속구조를 사용하면 단일상속만 허용한다. 믹스인 사용하면 여러 계층에서
  // 코드의 조각들을 편하게 가져올 수 있다.
  // 단 믹스인 mixin Engine 이 인스턴스화 되는 것은 아니다!
  // Wheel wheel1 = Wheel(); 믹스인은 인스턴스화 시킬 수 없다.
}

 

시나리오 코드 3 - mixin을 인스턴스화 시키고 싶다면?
// mixin을 인스턴스화 시킬 때 사용하는 문법
mixin class Engine {
  int power = 5000;
}

mixin class Wheel {
  String wheelName = '바퀴';
}

class BMW with Engine, Wheel {}

void main() {
  BMW b = BMW();
  Engine e = Engine(); // 믹스인 클래스는 인스턴스화 가능하다
  Wheel w = Wheel();
  print(b.power);
}

 

객체 지향 프로그래밍(OOP)에서 연관관계는 객체 간의 관계를 표현하고, 현실 세계를 모델링하는 데 중요한 도구입니다.
Dart에서는 컴포지션과 집합 관계를 통해 강한 소유와 약한 소유를 각각 구현할 수 있습니다.
하지만 Dart가 제공하는 Mixin 문법은 연관관계를 구현하지 않더라도 코드 조각을 여러 클래스에서 재사용할 수 있는 방법을 제공합니다. Mixin은 특히 다중 상속의 단점을 회피하면서도 유연한 설계를 가능하게 해줍니다.

OOP의 연관관계와 Mixin은 서로 다른 목적을 가지고 있지만, 상황에 따라 이 둘을 적절히 활용한다면 더 깔끔하고 효율적인 객체 지향 코드를 작성할 수 있을 것 같습니다.

 

🔗 Dart의 객체 지향 패러다임에 대해 더 알고 싶다면?

 

[DART] 상속(Inheritance)이란 무엇인가요?

상속(Inheritance)은 객체 지향 프로그래밍(OOP)의 핵심 개념 중 하나로, 부모 클래스가 가진 속성(상태)과 메서드(행위)를 자식 클래스가 물려받는 것을 의미합니다. 이를 통해 코드 재사용성을 높이

dev-yeonwha.tistory.com

 

아래의 문헌을 참고하여 작성된 포스팅입니다.
최주호, 김근호, 이지원(공저) 『만들면서 배우는 플러터 앱 프로그래밍』, 앤써북, 2023.