📝 Topic (오늘의 주제)
자바 상속(Inheritance)과 컴포지션(Composition)의 차이 및 사용 기준
: 객체지향 프로그래밍(OOP)에서 코드를 재사용하는 두 가지 핵심 기법인 '상속'과 '조합(컴포지션)'의 개념을 명확히 하고, 왜 현대 개발에서 "상속보다는 컴포지션을 선호하라(Favor Composition over Inheritance)"는 원칙이 강조되는지 학습한다.
Why (왜 사용하는가? 왜 중요한가?)
- 실무: 상속은 부모-자식 간의 **결합도(Coupling)**가 매우 높아, 부모 클래스 수정 시 자식 클래스에 예기치 않은 오류(Fragile Base Class 문제)를 발생시켜 유지보수 비용을 급증시킨다.
- 구조적 의미: 컴포지션은 인터페이스를 통해 객체를 **느슨하게 결합(Loose Coupling)**하여, 런타임에 기능을 동적으로 변경하거나 캡슐화를 유지하면서 필요한 기능만 노출하는 최적화된 구조를 제공한다.
- 면접 의도: 지원자가 단순히 문법(extends)을 아는 것을 넘어, 객체지향 설계 원칙(OCP, LSP 등)을 이해하고 설계 유연성을 고려하여 코드를 작성할 수 있는지 확인하려 한다.
Core Concept (핵심 개념 정리)
| 요소 | 내용 |
| 개념 정의 | 상속: 부모 클래스의 모든 속성과 기능을 물려받아 IS-A (자식은 부모다) 관계를 형성하는 것. 컴포지션: 다른 객체의 인스턴스를 필드(부품)로 가지고 기능을 위임받아 HAS-A (A는 B를 가지고 있다) 관계를 형성하는 것. |
| 동작 방식 | 상속: extends 키워드를 사용. 컴파일 시점에 관계가 고정됨(정적). 부모의 public, protected 메서드가 자동 노출됨. 컴포지션: 클래스 내부의 private 필드로 객체를 선언. 생성자 등을 통해 주입(DI)받고, 필요한 메서드만 호출하여 사용(Delegation). |
| 장점/단점 | 상속: 코드 작성이 간편하고 다형성 구현이 쉽지만, 결합도가 높고 불필요한 기능까지 상속됨. 컴포지션: 유연하고 테스트가 용이하며 캡슐화가 보장되지만, 코드가 길어지고 래퍼(Wrapper) 메서드를 작성해야 하는 번거로움이 있음. |
| 필요 조건 | 상속: 확실한 계층 구조와 타입 호환성(IS-A)이 보장되어야 함. 컴포지션: 기능의 재사용이 목적이며, 내부 구현을 감추고 싶을 때 사용. |
| 비교 | 화이트박스 재사용(상속): 부모의 내부 구현을 자식이 훤히 앎. 블랙박스 재사용(컴포지션): 객체의 인터페이스만 알면 됨. |
Interview Answer Version (면접 답변식 요약)
"상속과 컴포지션의 가장 큰 차이는 결합도와 관계의 본질에 있습니다.
상속은 IS-A 관계로 부모의 기능을 물려받으며 결합도가 높습니다. 반면 컴포지션은 HAS-A 관계로 다른 객체를 필드로 소유하여 기능을 위임해 사용하며 결합도가 낮습니다.
저는 코드의 재사용만이 목적이라면 컴포지션을 우선적으로 고려합니다. 상속은 부모 클래스의 변경이 자식에게 치명적인 영향을 줄 수 있고, 불필요한 메서드까지 노출되어 캡슐화를 해칠 수 있기 때문입니다. 따라서 명확한 타입 계층 구조가 필요한 경우가 아니라면, 컴포지션을 통해 유연하고 유지보수하기 쉬운 설계를 지향합니다."
Practical Tip (사용시 주의할 점 or 활용 예)
1. 이걸 모르고 사용하면 생기는 문제 (Java API 실제 사례)
자바의 java.util.Stack은 대표적인 상속의 오용 사례입니다.
- 상황: Stack은 LIFO(Last In First Out) 구조여야 하므로 중간에 데이터를 넣거나 빼면 안 됩니다.
- 문제: Vector를 상속받아 구현했기 때문에, Vector의 add(index, element) 메서드가 Stack에 노출되었습니다.
- 결과: 스택의 원칙이 깨져버려, 사용자가 중간에 데이터를 삽입해도 막을 수 없습니다. (현재는 Deque 인터페이스 사용 권장)
2. 컴포지션 적용 코드 예시 (Wrapper Class 패턴)
상속 대신 필드로 객체를 가지고, 필요한 기능만 '전달(Forwarding)'합니다.
Java
// [컴포지션 활용] Set의 기능을 확장(계측)하고 싶을 때
public class InstrumentedTestedSet<E> {
private int addCount = 0;
private final Set<E> set; // 컴포지션: Set 인스턴스를 품음
public InstrumentedTestedSet(Set<E> set) {
this.set = set;
}
public boolean add(E e) {
addCount++;
return set.add(e); // 위임 (Delegation)
}
public int getAddCount() {
return addCount;
}
// Set의 다른 기능이 필요하면 추가로 위임 메서드 작성
}
- 장점: HashSet을 쓰든 TreeSet을 쓰든 생성자로 주입만 받으면 되므로 훨씬 유연합니다.
3. 설정 시 고려 포인트
- Lombok 활용: 컴포지션은 코드가 길어지는 단점이 있는데, Lombok의 @Delegate (현재는 실험적 기능이라 주의 필요) 등을 활용하거나 IDE의 'Delegate Methods' 자동 생성 기능을 활용하면 생산성을 높일 수 있습니다.
예상 꼬리질문 정리
- Q: 그렇다면 상속은 언제 써야 하나요? 반드시 써야 하는 경우가 있나요?
- A: 상위 클래스와 하위 클래스가 완벽한 IS-A 관계일 때, 그리고 상위 클래스의 코드가 변경될 때 하위 클래스도 같이 변경되는 것이 논리적으로 타당할 때 사용합니다. 또한 프레임워크(React 컴포넌트, JPA 엔티티 등)에서 설계를 위해 상속을 강제하는 경우에는 따라야 합니다.
- Q: 리스코프 치환 원칙(LSP)과 상속의 관계에 대해 설명해 주세요.
- A: LSP는 자식 클래스가 부모 클래스의 자리를 완벽하게 대체할 수 있어야 한다는 원칙입니다. 상속을 잘못 사용하면(예: 직사각형을 상속받은 정사각형), 부모의 동작 규약을 자식이 깨뜨리게 되어 LSP를 위반하게 됩니다. 이럴 때는 상속을 끊고 컴포지션을 써야 합니다.
- Q: 컴포지션과 전략 패턴(Strategy Pattern)의 관계는 무엇인가요?
- A: 전략 패턴은 컴포지션의 장점을 극대화한 디자인 패턴입니다. 객체의 행위(전략)를 인터페이스로 정의하고, 이를 필드(컴포지션)로 가짐으로써 런타임에 전략을 갈아끼울 수 있게 하여 코드 수정 없이 유연한 확장을 가능하게 합니다.
'Archive > Daily Dev Q&A' 카테고리의 다른 글
| Daily Dev Q&A: 오버로딩과 오버라이딩 (0) | 2025.12.05 |
|---|---|
| Daily Dev Q&A: 다형성 (0) | 2025.12.03 |
| Daily Dev Q&A: 객체지향언어란? & 왜 JAVA가 객체지향언어인가? (0) | 2025.12.01 |
| Daily Dev Q&A: 자바의 예외 처리(Exception) 구조 (0) | 2025.11.28 |
| Daily Dev Q&A: 자바는 컴파일 언어? 인터프린터 언어? (0) | 2025.11.28 |