합성 vs 상속
Coddy Dart 여정의 객체 지향 프로그래밍 섹션에 포함된 레슨 — 110개 중 82번째.
상속은 "is-a" 관계를 만든다는 것을 배웠습니다 - Dog는 Animal입니다. 하지만 또 다른 강력한 접근 방식이 있습니다: "has-a" 관계를 만드는 합성(composition)입니다. 동작을 상속받는 대신, 클래스가 다른 클래스의 인스턴스를 포함하고 그들에게 작업을 위임합니다.
Car 클래스를 생각해 봅시다. 상속을 사용하면 Engine 클래스를 확장하려고 할 수도 있지만, 자동차는 엔진이 아니며 엔진을 가지고 있는 것입니다:
class Engine {
void start() => print('Engine started');
}
class Car {
final Engine _engine = Engine(); // 컴포지션: Car는 Engine을 가집니다
void start() {
_engine.start(); // 엔진에 위임
print('Car is ready to drive');
}
}
void main() {
var car = Car();
car.start();
}컴포지션(합성)의 핵심 장점은 유연성입니다. 상속을 사용하면 하나의 부모 클래스에 얽매이게 됩니다. 컴포지션(합성)을 사용하면 여러 컴포넌트를 결합할 수 있으며, 심지어 런타임에 이를 교체할 수도 있습니다:
class ElectricEngine {
void start() => print('Electric motor humming');
}
class HybridCar {
Engine? _gasEngine;
ElectricEngine? _electricEngine;
HybridCar({bool useElectric = false}) {
if (useElectric) {
_electricEngine = ElectricEngine();
} else {
_gasEngine = Engine();
}
}
}일반적인 가이드라인은 "상속보다 합성을 선호하라(favor composition over inheritance)"입니다. 진정한 "is-a" 관계가 있고 다형성(polymorphism)이 필요할 때 상속을 사용하세요. 강한 결합(tight coupling) 없이 동작을 재사용하고 싶거나, 객체가 여러 소스로부터 기능을 필요로 할 때 합성을 사용하세요.
챌린지
쉬움컴포지션(composition)을 사용하여 컴퓨터 시스템을 구축해 봅시다! 컴퓨터가 부품으로부터 상속받게 하는 대신, 별도의 컴포넌트 클래스를 만들고 이들을 조합할 것입니다. 왜냐하면 컴퓨터는 CPU와 메모리를 가지고 있는(has) 것이지, 컴퓨터 자체가 CPU나 메모리인 것은 아니기 때문입니다.
코드를 세 개의 파일로 구성합니다:
components.dart: 컴퓨터 부품을 나타내는 두 개의 컴포넌트 클래스를 생성합니다:String brand와double speedGHz를 가진CPU클래스.[brand] processing at [speedGHz]GHz라는 문자열을 반환하는process()메서드를 포함하세요.int sizeGB를 가진Memory클래스.Loading [sizeGB]GB of data라는 문자열을 반환하는load()메서드를 포함하세요.
computer.dart: 컴포지션을 사용하여 컴포넌트들을 결합하는Computer클래스를 생성합니다. 컴퓨터는 다음을 수행해야 합니다:String name필드를 가집니다.CPU와Memory인스턴스를 포함합니다 (컴포지션 - 컴퓨터는 이러한 부품들을 '가집니다').- 생성자를 통해 필요한 모든 값을 전달받아 내부적으로 두 컴포넌트를 생성합니다.
- 두 컴포넌트에 작업을 위임하고 세 줄을 출력하는
boot()메서드를 가집니다: 컴퓨터의 이름 뒤에starting...을 붙여 출력하고, 그 다음 CPU의 process 메서드 결과, 그 다음 Memory의 load 메서드 결과를 출력합니다. - 시스템을 설명하는 문자열을 반환하는
specs()메서드를 가집니다:[name]: [brand] [speedGHz]GHz, [sizeGB]GB RAM
main.dart: 파일을 임포트하고 컴포지션이 작동하는 모습을 보여줍니다:- 이름이
Workstation이고,3.5GHz로 작동하는IntelCPU와16GB의 메모리를 가진Computer를 생성합니다. boot()메서드를 호출합니다.specs()의 결과를 출력합니다.
- 이름이
Computer 클래스가 CPU나 Memory를 상속(extend)하지 않고, 그것들을 포함하며 작업을 위임하는 방식에 주목하세요. 이것이 바로 컴포지션이 만드는 "has-a" 관계입니다!
예상 출력:
Workstation starting...
Intel processing at 3.5GHz
Loading 16GB of data
Workstation: Intel 3.5GHz, 16GB RAM치트 시트
컴포지션(Composition)은 클래스가 다른 클래스의 인스턴스를 포함하고 작업을 상속받는 대신 해당 인스턴스에 위임하는 "has-a" 관계를 생성합니다.
기본적인 컴포지션 예시:
class Engine {
void start() => print('Engine started');
}
class Car {
final Engine _engine = Engine(); // Car HAS an Engine
void start() {
_engine.start(); // Delegate to the engine
print('Car is ready to drive');
}
}유연성을 갖춘 컴포지션 - 컴포넌트 교체하기:
class ElectricEngine {
void start() => print('Electric motor humming');
}
class HybridCar {
Engine? _gasEngine;
ElectricEngine? _electricEngine;
HybridCar({bool useElectric = false}) {
if (useElectric) {
_electricEngine = ElectricEngine();
} else {
_gasEngine = Engine();
}
}
}컴포지션 vs 상속 사용 시기:
- 다형성이 필요한 진정한 "is-a" 관계에는 상속(inheritance)을 사용하세요.
- 긴밀한 결합 없이 동작을 재사용하고 싶거나, 객체가 여러 소스로부터 기능을 가져와야 할 때는 컴포지션(composition)을 사용하세요.
- 일반적인 가이드라인: "상속보다는 컴포지션을 지향하라(favor composition over inheritance)"
직접 해보기
import 'computer.dart';
void main() {
// TODO: 이름이 "Workstation"인 Computer를 다음 조건으로 생성하세요:
// - 3.5 GHz로 작동하는 Intel CPU
// - 16 GB 메모리
// TODO: boot() 메서드를 호출하세요
// TODO: specs()의 결과를 출력하세요
}
이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.