composition over inheritance

김운채·2023년 5월 20일
0

TIL

목록 보기
13/22

상속보다 조합을 권장한다는 얘기가 있다.

일단 우리가 상속을 쓰는 이유는 다음과 같다.

  1. 코드의 재사용을 통해서 중복을 줄인다.
  2. 확장성이 증가한다.
  3. 클래스간의 계층적관계를 구성함으로써 다형성을 구현할 수 있다.
  4. 개발 시간이 단축 된다.

하지만 상속을 사용하면 생기는 문제점들이 있다.

상속의 문제점

1. 캡슐화를 깨뜨린다.

상속의 정의를 다시한번 되짚어 보자.
👉 상속은 부모클래스의 기능을 자식클래스에서 확장한다.
=> 상위클래스의 구현이 하위클래스에게 노출이 된다.
=> 캡슐화가 깨진다.

따라서 자식 클래스가 부모클래스에 강하게 결합 및 의존하게 된다.
=> 강한 결합, 의존은 변화에 유연하게 대처하기 어려워진다.

이로 인해 상위 클래스가 어떻게 구현되느냐에 따라 하위 클래스의 동작이 달라질 수 있다. 상위 클래스의 내부 구현이 달라지면 코드 한 줄 건드리지 않은 하위 클래스가 오동작 할 수 있는 것이다.

🙋‍♀️ 그럼 상속받아도 메소드 오버라이딩 안하면 되는거 아녀?
👉 상속을 받으면 자식 클래스는 부모 클래스의 메서드를 오버라이딩하지 않아도 부모 클래스의 메서드를 사용할 수 있다.

예를 들어서 Car 클래스가 Engine 클래스를 상속받는다고 가정해보자.
그리고 Engine 클래스에는 start() 메서드가 있다.

class Engine {
    public void start() {
        System.out.println("Engine starting...");
    }
}

class Car extends Engine {
    // Car 클래스에서는 Engine 클래스의 메서드를 오버라이딩하지 않음
}

public class Main {
    public static void main(String[] args) {
        Car car = new Car();
        car.start(); // Engine starting...
    }
}

위 예제에서 Car 클래스는 Engine 클래스를 상속받았지만, Car 클래스에서 start() 메서드를 오버라이딩하지 않았다. 그럼에도 불구하고 car.start()를 호출하면 "Engine starting..."이라는 출력이 나타난다.

이는 Car 클래스가 Engine 클래스를 상속받았기 때문에 Car 객체에서도 Engine의 메서드를 호출할 수 있기 때문이다.

2. 부모클래스의 결함도 자식 클래스에게 넘어온다.

이건 "Stack 대신 Deque를 사용하라" 라는 예를 들어서 이해해 보자.

자바 공식문서에서는 다음과 같이 말한다.

A more complete and consistent set of LIFO stack operations is provided by the Deque interface and its implementations, which should be used in preference to this class. For example:
더욱 완전하고 일관된 LIFO 스택 작업은 Deque 인터페이스 및 해당 구현을 사용하여 구현하는 것이다. 예를들어:

Deque<Integer> stack = new ArrayDeque<Integer>();
stack.push(1);

즉, Stack 대신 Deque 의 구현체인 ArrayDeque 사용을 제안하고 있다.
이것은 Vector 의 구현 방법에 이유가 있다.

Vector는 get()과 set()역할을 하는 모든 메서드에 synchronized 키워드가 붙어 있다.
Vector의 모든 get() set() 등의 메서드에 synchronized가 붙어있는건 특정 상황에서 성능을 꽤 저하시킬 수 있다.

단순히 Vector에 Iterator를 붙여 순차적으로 item들을 탐색하기만 해도 원소탐색 시마다 get() 메서드의 실행을 위해 계속 lock을 걸고 닫으므로 Iterator연산과정 전체에 1번만 걸어주면 될 locking에 쓸데없는 오버헤드가 엄청나게 발생한다.

따라서 Vector는 특정 상황에서만 최적으로 동작하게 되고, 어떤 상황에서는 그렇지 않게 되므로 효율적인 Thread-safe 컬렉션이라고 할 수 없는 것이다.

🤷‍♀️ 근데 stack이 Vector 처럼 get() set() 메소드를 쓰는 것도 아닌데 왜 스택을 지양해야 하는 걸까?

👉 stack이 Vector의 상속을 받기 때문이다.
따라서 Vector 의 기능을 모두 사용할 수 있지만, 동시에 Vector 의 결함도 같이 물려받는다.

조합

조합은 기존클래스를 확장하는 대신에 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 하는 설계이다.

👩

즉, 기존클래스가 새로운 클래스의 구성요소를 쓰이는 것을 말한다.

조합은 새 클래스의 인스턴스 메소드들을 기존 클래스에 대응하는 메소드를 호출해서
그결과를 반환하게 되는데, 이 방식을 전달 (Forwarding) 이라고 한다.
그 새로운 클래스들의 전달 메소드들을 전달 메소드(Forwarding method)라고 부르게 된다.

조합의 장점

  1. 새로운 클래스는 기존 클래스의 내부 구현 방식의 영향에서 벗어나며, 심지어 기존 클래스에 새로운 메서드가 추가되더라도 전혀 영향을 받지 않는다.
  2. 메서드를 호출하는 방식으로 동작하기 때문에 캡슐화를 깨뜨리지 않는다.
  3. 상위 클래스에 의존하지 않기 때문에 변화에 유연하다.

즉, 조합은 캡슐화를 깨뜨리고 부모클래스에 의존하는 상속의 단점들을 모두 해결할 수가 있다. + 유연성

https://tecoble.techcourse.co.kr/post/2020-05-18-inheritance-vs-composition/
적당한 예제는여기서 잘 설명되어 있다.

상속과 조합의 차이

  • 상속은 is-a 관계에 있다. ex) coffee is caffainBeverage
  • 조합은 has-a 관계에 있다. ex) 학생은 책을 가지고있다

🤷‍♀️ 그럼 무족권 상속을 쓰지말아야 하나?
👉 아니다. 그저 상황에 따라 맞게, 제대로 쓰자는 것 !
상속이 적절하게 사용되면 조합보다 강력하고, 개발하기도 편리하다.

상속은 언제써야 할까?

  • 명확한 is-a 관계일 경우
  • API에 아무런 결함이 없는 경우, 결함이 있다면 하위 클래스까지 전파되어도 괜찮은 경우 (상위클래스가 확장할 목적으로 설계되었고 문서화도 잘되어 있는 경우)

public class 포유류 extends 동물 {
	
    protected void 숨을쉬다() {
    	'''
    }
    
    protected void 새끼를낳다() {
    	'''
    }
}

포유류가 동물이라는 사실은 변할 가능성이 거의 없고, 포유류가 숨을 쉬고 새끼를 낳는다는 행동이 변할 가능성은 거의 없다.

이처럼 확실한 is-a 관계의 상위 클래스는 변할 일이 거의 없다. 그러므로 포유류 클래스와 동물의 관계를 구현하고자 할 때 상속을 고려할 수 있다.

0개의 댓글