Favor COMPOSITION over inheritance.
상속보다 컴포지션을 선호하라!
그렇다고 상속이 무조건 나쁘고 컴포지션이 무조건 좋은 것은 아니다.
다만 너무 심한 수직 상속관계는 피하는 것이 좋다.
컴포지션?
composition : 구성
단어에 의미가 핵심이다.
상속을 하지말고 구성요소로 집어넣으라는 뜻.
언제 쓸까?
이미 상속받은 상태인데, 만들다보니 또 중복되는 기능이 있어서 빼내려고 한다.
하지만 기존의 클래스에 추가하기에는 알맞지 않는 기능이라고 판단되어 별도의 클래스를 작성하였다.
이런 경우에 어떻게 가져다 쓸 것인가?
다중상속은 불가능하고, 인터페이스로 뽑으면 매번 내용을 정의해야 하므로 알맞지 않다.
- 첫번째로 할 수 있는 접근은 수직 상속구조로 만드는 것이다.
A extends B
B extends C
C extends D
- 두번째로 할 수 있는 선택이
컴포지션
이다.
컴포지션 장점
- 복잡한 수직 상속구조를 피할 수 있게 된다!
상속의 레벨을 1~2단계로만 유지하면서 코드를 재사용 할 수 있음.
컴포지션 단점
- 클래스 컴포지션을 쓰면 클래스간에 너무 타이트하게 커플링 됨.
- 필요한 동일한 이름의 함수는 가지고 있지만 동작이 조금 다른 다른 클래스의 객체를 주입받고 싶다면 클래스를 수정해야함.
클래스 컴포지션 개선점
- 클래스 끼리 커플링 시켜서 쓰지 말고, 인터페이스로 빼내서 디커플링 시켜서 사용하자.
- 필요한 함수만 가지고 있다면 다 수용할 수 있으므로, 다른 클래스의 객체를 주입 받더라도 클래스의 코드는 변경할 필요가 없다.
컴포지션 예시
클래스 컴포지션 코드 ( 클래스끼리 타이트하게 커플링 된 경우 )
- 커피머신 클래스는 종류와 상관없이
거품내기()
가 가능한 거품기와,설탕제조()
가 가능한 설탕제조기가 필요할 뿐임.
- 컴포지션 구현 시 구체적인 클래스명을 지정하였으므로,
싸구려거품기
와싸구려설탕제조기
외에 다른 거품기와 설탕제조기를 쓰고싶다면커피머신
클래스의 코드를 수정해야함.
class 싸구려거품기 { 거품내기(): void { console.log("오이외옹잉 싸구려 거품을 낸다 ... "); } } class 싸구려설탕제조기 { 설탕제조(): void { console.log("퐢오팍팍 싸구려 설탕을 만든다 ... "); } } class 커피머신 { constructor( private 거품기: 싸구려거품기, // 컴포지션, 다른 클래스의 객체를 주입받음. private 설탕제조기: 싸구려설탕제조기 // 컴포지션, 다른 클래스의 객체를 주입받음. ) {} 커피내리기() { console.log("커피를 내리고 있습니다 ... "); } 기본커피() { this.커피내리기(); this.거품기.거품내기(); this.설탕제조기.설탕제조(); console.log("커피가 완성되었습니다."); } } const myCoffeeMachine = new 커피머신( new 싸구려거품기(), // DI(Dependency Injection) new 싸구려설탕제조기() // DI(Dependency Injection) ); myCoffeeMachine.기본커피();

인터페이스로 디커플링 시킨 코드
- 컴포지션할 객체의 구체적인 클래스명을 명시하지 않고 인터페이스로 타입을 지정한다.
- 해당 인터페이스를 구현하는 객체이기만 하면 되므로,
커피머신
클래스의 코드를 수정하지 않고도 다른 클래스의 객체를 주입받을 수 있게 되었다.
interface 거품제조기 { 거품내기(): void; } interface 설탕제조기 { 설탕제조(): void; } class 싸구려거품기 implements 거품제조기 { 거품내기(): void { console.log("오이외옹잉 싸구려 거품을 낸다 ... "); } } class 고급거품기 implements 거품제조기 { 거품내기(): void { console.log("고오오오급 거품을 낸다 ... "); } } class 싸구려설탕제조기 implements 설탕제조기 { 설탕제조(): void { console.log("퐢오팍팍 싸구려 설탕을 만든다 ... "); } } class 고급설탕제조기 implements 설탕제조기 { 설탕제조(): void { console.log("고오오오급 설탕을 만든다 ... "); } } class 커피머신 { constructor( private 거품기: 거품제조기, // 컴포지션, 거품제조기 인터페이스를 구현한 객체이기만 하면 됨. private 설탕제조기: 설탕제조기 // 컴포지션, 설탕제조기 인터페이스를 구현한 객체이기만 하면 됨. ) {} 커피내리기() { console.log("커피를 내리고 있습니다 ... "); } 기본커피() { this.커피내리기(); this.거품기.거품내기(); this.설탕제조기.설탕제조(); console.log("커피가 완성되었습니다."); } } const myCoffeeMachine1 = new 커피머신( new 싸구려거품기(), // DI(Dependency Injection) new 싸구려설탕제조기() // DI(Dependency Injection) ); const myCoffeeMachine2 = new 커피머신( new 고급거품기(), // DI(Dependency Injection) new 고급설탕제조기() // DI(Dependency Injection) ); myCoffeeMachine1.기본커피(); myCoffeeMachine2.기본커피();
