컴포지션 vs 상속, “상속 쓰지 말라”는 말의 진짜 의미
객체지향을 공부하다 보면 꼭 만나는 문장이 있어요. “상속보다 컴포지션을 써라(Favor composition over inheritance).”
결론부터 말씀드리면, 이 말은 “상속을 절대 쓰지 마라”가 아닙니다. “코드 재사용 목적으로 상속을 남발하지 마라”는 뜻이에요. 상속은 여전히 유효한 도구지만, 재사용이 필요한 상황은 대부분 컴포지션이 더 안전한 선택입니다.
이 글에서는 둘의 차이가 실제 코드에서 어떻게 갈리는지, 또 언제 무엇을 골라야 하는지를 제 경험과 함께 풀어볼게요.
상속과 컴포지션, 뭐가 다른가요?
먼저 아주 단순하게 나눠볼게요.
상속은 “~은 ~이다(is-a)” 관계예요. 강아지는 동물이다, 이런 식이죠. 부모 클래스의 기능을 자식이 그대로 물려받습니다.
컴포지션은 “~은 ~을 가진다(has-a)” 관계고요. 자동차는 엔진을 가진다처럼요. 필요한 기능을 가진 객체를 안에 품고, 그 객체에게 일을 시키는 방식이죠.
코드로 보면 감이 더 옵니다. 아래는 상속으로 기능을 물려받는 방식이에요.
// 상속: Stack이 NSMutableArray의 모든 메서드를 물려받음
class Stack: NSMutableArray {
func push(_ o: Any) { add(o) }
func pop() -> Any {
let o = lastObject!
removeLastObject()
return o
}
}
문제는 이렇게 하면 Stack이 원치 않는 insert(_:at:), removeObject(at:) 같은 메서드까지 전부 외부에 노출돼요.
아래는 같은 걸 컴포지션으로 바꾼 모습입니다.
// 컴포지션: 필요한 기능만 골라서 위임
class Stack {
private var list: [Any] = []
func push(_ o: Any) { list.append(o) }
func pop() -> Any { list.removeLast() }
}
배열을 안에 품고 필요한 동작만 밖으로 열어줬어요. Stack이 진짜 하고 싶은 일만 남습니다.
“상속 쓰지 말라”는 말의 진짜 의미
이 말이 나온 배경엔 상속의 대표적인 약점 두 가지가 있어요.
첫째, 캡슐화가 깨집니다. 자식은 부모의 내부 구현에 의존하게 돼요. 부모 코드가 바뀌면 멀쩡하던 자식이 갑자기 깨지기도 합니다.
둘째, 결합도가 너무 높아집니다. 부모와 자식이 컴파일 시점에 단단히 묶여버려서, 나중에 관계를 바꾸기가 어려워요.
상속은 코드를 재사용하는 도구가 아니라, 타입을 정의하는 도구입니다.
이 문장이 핵심이에요. 단지 코드를 재사용하고 싶어서 상속을 쓰는 순간, 관계가 꼬이기 시작합니다.
유명한 예가 정사각형과 직사각형이에요. 수학적으로 정사각형은 직사각형이니까 상속하면 될 것 같죠. 그런데 직사각형의 “너비와 높이를 따로 바꾸는” 기능을 물려받는 순간, 정사각형은 더 이상 정사각형이 아니게 됩니다.
is-a 관계처럼 보여도 행동까지 완전히 대체할 수 없다면 상속은 함정이 돼요.
그럼 상속은 언제 써도 되나요?
상속이 무조건 나쁜 건 아니에요. 아래 조건을 모두 만족하면 상속이 오히려 깔끔합니다.
- 진짜 is-a 관계인가: 자식은 언제나 부모의 한 종류인가요?
- 리스코프 치환 원칙을 지키는가: 부모 자리에 자식을 넣어도 아무 문제가 없나요?
- 부모가 상속을 염두에 두고 설계됐는가: 문서화되고 확장을 위해 열려 있나요?
세 가지가 다 맞다면 상속을 써도 좋아요. UIViewController 같은 프레임워크 기반 클래스를 확장하는 경우가 대표적입니다.
반대로 하나라도 애매하면 컴포지션을 먼저 떠올리세요.
표로 간단히 비교해봤어요.
| 상황 | 추천 |
|---|---|
| 순수한 is-a 관계, 완전한 치환 가능 | 상속 |
| 단순히 코드만 재사용하고 싶을 때 | 컴포지션 |
| 런타임에 동작을 바꾸고 싶을 때 | 컴포지션 |
| 여러 기능을 조합해야 할 때 | 컴포지션 |
보시면 실무에서 마주치는 대부분의 상황이 컴포지션 쪽으로 기웁니다. “favor composition”이라는 조언이 괜히 나온 게 아니에요.
실무에서 저는 이렇게 판단해요
저는 새 클래스를 만들 때 습관적으로 “이건 부모의 한 종류인가, 아니면 부모 기능을 빌려 쓰고 싶은 건가”를 먼저 물어봐요.
빌려 쓰고 싶은 거라면 거의 항상 컴포지션으로 갑니다.
디자인 패턴을 봐도 방향은 분명해요. 전략 패턴, 데코레이터 패턴 같은 것들이 전부 컴포지션 기반이거든요.
특히 전략 패턴은 동작을 객체로 분리해서 런타임에 갈아끼웁니다. 상속으로는 흉내 내기 힘든 유연함이에요.
물론 코드가 살짝 길어지는 건 감수해야 해요. 위임 메서드를 직접 써줘야 하니까요. 그래도 나중에 구조를 바꾸기가 훨씬 편합니다.
“상속 쓰지 말라”는 말은 상속을 금지하라는 게 아니라 재사용 목적의 남용을 경계하라는 조언이에요. is-a가 확실하면 상속, 그저 기능을 빌리고 싶은 거면 컴포지션. 이 기준 하나만 챙겨도 코드가 한결 단단해집니다. 오늘도 좋은 설계 하시길 응원할게요!
