상속(Inheritance) vs 합성(Composition)
Contents
객체지향 프로그래밍에서 재사용성을 높이는 방법은 크게 두 가지다. 부모의 기능을 물려받는 상속과, 필요한 기능을 가진 객체를 내부에 포함하는 합성이다. 현대 소프트웨어 설계에서는 왜 “상속보다는 합성(Composition over Inheritance)“을 권장하는지 그 이유를 파헤쳐 본다.
1. 상속 (Inheritance): “Is-A” 관계
상속은 하위 클래스가 상위 클래스의 특성을 그대로 이어받는 방식이다. ‘기본 클래스의 확장’이라는 개념으로 접근한다.
특징
- Is-A 관계: “강아지는 동물이다(Dog is an Animal)“와 같은 논리적 계층 구조를 가진다.
- 화이트박스 재사용: 상위 클래스의 내부 구현이 하위 클래스에 노출된다.
장점
- 코드를 그대로 물려받으므로 초기 개발 속도가 빠르다.
- 다형성을 통해 상위 클래스 타입으로 하위 클래스를 다룰 수 있다.
단점 (한계)
- 강한 결합도(Tight Coupling): 부모 클래스의 변경이 모든 자식 클래스에 영향을 미친다.
- 불필요한 기능 상속: 부모의 메서드 중 자식에게는 부적절한 기능까지 강제로 물려받게 된다.
- 유연성 부족: 컴파일 타임에 관계가 결정되므로 실행 중에 로직을 바꾸기 어렵다.
2. 합성 (Composition): “Has-A” 관계
합성은 새로운 클래스를 만들 때, 다른 객체를 필드(인스턴스 변수)로 참조하여 기능을 사용하는 방식이다.
특징
- Has-A 관계: “자동차는 엔진을 가지고 있다(Car has an Engine)“와 같은 포함 관계를 가진다.
- 블랙박스 재사용: 내부 구현이 노출되지 않으며, 인터페이스를 통해서만 소통한다.
장점
- 낮은 결합도: 포함된 객체의 내부 구현이 바뀌어도 영향을 거의 받지 않는다.
- 실행 시점의 유연성: 런타임에 포함된 객체(구현체)를 교체함으로써 동작을 동적으로 변경할 수 있다.
- 단일 책임 원칙: 각 클래스가 한 가지 역할에 집중할 수 있도록 잘게 쪼개기 유리하다.
단점
- 상속에 비해 클래스 간의 관계를 파악하는 데 더 많은 코드를 읽어야 할 수 있다.
- 객체 생성 비용이 다소 증가할 수 있다.
3. 코드 비교: 상속 vs 합성
상속 방식 (Inheritance)
부모 클래스의 결함이나 변화가 자식에게 그대로 전이된다.
|
|
합성 방식 (Composition)
이동 전략을 인터페이스로 분리하여 필요할 때 갈아 끼운다.
|
|
4. 왜 ‘합성’이 대세인가?
현대 소프트웨어는 변화에 대한 유연성을 가장 중요하게 여긴다.
- 캡슐화 파괴 방지: 상속은 부모의 내부를 자식에게 노출시키지만, 합성은 객체의 경계를 명확히 지킨다.
- 다중 상속의 문제 회피: 대부분의 언어는 다중 상속을 금지한다. 합성을 사용하면 여러 개의 기능을 자유롭게 조합할 수 있다.
- 테스트 용이성: 합성된 객체는 가짜 객체(Mock)로 갈아 끼우기 쉬워 단위 테스트 작성이 훨씬 수월하다.
결론: 언제 무엇을 쓸 것인가?
무조건 상속이 나쁜 것은 아니다. 하지만 다음 기준을 고려해야 한다.
- 상속을 쓰는 경우: 명확한 계층 구조가 있고, 부모의 모든 퍼블릭 인터페이스가 자식에게도 완벽히 부합할 때(Liskov Substitution Principle).
- 합성을 쓰는 경우: 단순히 코드의 재사용이 목적이거나, 런타임에 동적으로 기능을 바꿔야 할 때.
결국, 설계의 기본은 합성으로 잡고 상속은 꼭 필요한 경우에만 신중하게 도입하는 것이 현대적인 프로그래밍의 정석이다.