앞서, 객체지향 4가지 특성 중 상속을 간단히 정리해 봤다.
이번엔 상속과 컴포지션을 자세히 알아보고, 실무에서 언제 사용해야 할지 정리해 보며, 면접식 요약으로 마무리할 것이다.
상속
상속은 객체지향 4가지 특성(상속, 추상화, 캡슐화, 다형성)에 속한다.
자식 클래스가 부모 클래스에게 물려받으면, 부모 클래스의 자원을 사용할 수 있을 뿐만이 아니라 재정의 까지 가능하기 때문에, 확장성이 높아진다.
또한 상속 관계는 IS-A 관계라고 불린다.

class Phone {
void ringBell() {
System.out.println("기본 벨소리");
}
}
class IPhone extends Phone {
// 부모의 ringBell을 똑같이 쓰고, 내용만 바꿈 (재정의)
@Override
void ringBell() {
System.out.println("아이폰: 애플 벨소리");
}
}
class SamsungPhone extends Phone {
// 부모의 ringBell을 똑같이 쓰고, 내용만 바꿈 (재정의)
@Override
void ringBell() {
System.out.println("갤럭시폰: 갤럭시 벨소리");
}
}
public class Main {
public static void main(String[] args) {
Phone myPhone = new IPhone();
myPhone.ringBell(); // 결과: (아이폰: 벨소리)
}
}
부모 클래스는 Phone를 상속받은 두 클래스 SamsungPhone과 IPhone은 재정의(Override)를 통해, 부모 클래스가 가지고 있는 기존 코드의 ringBell() 메서드를 사용할 수 있다.
또한, 최상위 클래스인 Object 클래스는 우리가 생성한 클래스에 Extends를 안 해도 상속받기 때문에, toString(), equals() 등을 사용할 수 있다.
장점
- 재사용성을 높아진다.
- 확장성이 증가한다.
- 클래스 간의 계층적 관계를 구성함으로써 다형성(Override)을 구현할 수 있다.
단점
- 캡슐화 위반(강한 결합도): 상속은 부모 클래스의 내부 구현을 자식에게 노출시키는 행위이다.
부모의 로직이 변경되면, 자식 클래스도 함께 수정해야 하는 등 영향 범위가 커져 유지 보수가 어려워진다. - 클래스 폭발: 기능을 확장할 때마다 불필요하게 많은 클래스를 만들어야 할 수 있으며, 상속 구조가 깊어질수록 코드를 파악하기 어렵다.
- 유연성 부족(런타임 교체 불가): 상속 관계는 컴파일 시점에 결정되므로, 실행 중에 부모 객체를 다른 것으로 교체할 수 없다.
하지만, 컴포지션은 가능하다. - 단일 상속의 한계: 자바는 다중 상속을 지원하지 않기 때문에, 이미 다른 클래스를 상속받고 있다면 추가적인 상속을 받을 수 없어, 확장에 제약이 생긴다. 이를 보완하기 위해 인터페이스가 등장했다.
다중 상속을 지원하지 않는 이유는 다이아몬드 문제에 대한 모호성 때문이다.
A 클래스를 B와 C 클래스가 상속받고, 다시 D 클래스가 B와 C를 모두 상속받을 때(A -> B, A -> C, B -> D, C -> D), B와 C가 공통 조상인 A로부터 물려받은 같은 이름의 메서드를 D에서 호출하면 어떤 메서드를 실행해야 할지 알 수 없게 되는 문제
A (부모)
/ \
B C (공통 부모의 메서드를 가짐)
\ /
D (D는 B와 C를 모두 상속받음)
D 클래스에서 A의 메서드를 호출하면 B의 메서드와 C의 메서드 중 무엇이 실행되어야 하는지 결정이 모호해진다.
따라서, 자바의 인터페이스는 원래 구현부가 없었기 때문에 메서드 충돌 문제가 없기 때문에, 다중 상속을 구현할 경우 인터페이스를 사용해야 한다.
이러한 단점은 실무에서 부적합하다.
1. 결합도가 최악이다.
실무에서는 Fragile Base Class(깨지기 쉬운 기반 클래스) 문제라고 부른다.
예를 들면, 부모 클래스 BaseService를 수정하면, 자식 클래스 모두 에러가 생긴다.
2. 불필요한 기능까지 억지로 떠안아야 한다.
나는 바나나(기능 하나)만 필요했는데, 상속을 받았더니 바나나를 들고 있는 고릴라(부모 클래스)와 고릴라가 사는 정글(부모의 환경)까지 통째로 따라왔다. - 조 암스트롱 (Erlang 창시자)
List의 기능 중 add() 하나만 재사용하고 싶어서 ArrayList를 상속받았지만, 상속받은 내 클래스는 add() 뿐만이 아니라 remove(), clear(), iterator() 등 내가 원하지 않는 메서드가 따라온다.
3. 클래스 폭발

단점에도 적힌 클래스 폭발은 기능이 조합될 경우, 상속을 쓰면, 클래스 개수가 기하급수적으로 늘어난다.
예를 들면, 커피 메뉴를 만들기 위해 Coffee 부모 클래스를 MilkCoffee와 SugarCoffee 자식 클래스에 상속한다.
여기서 문제는 우유와 설탕을 동시에 넣으면, 또 다른 클래스를 생성해야 한다.
심지어 재료가 추가될수록 클래스는 더욱 많아진다.
이는 새로운 옵션을 추가하는데 클래스 조합이 너무 많아져 코드가 지저분해진다.
따라서 컴포지션의 데코레이터 패턴을 사용해 하나의 클래스로 해결된다.
4. 런타임에 유연하지 않다.
상속은 컴파일 시점에 고정되므로 프로그램 실행 중에 부모를 바꿀 수 없다.
예를 들면, 게임 캐릭터를 생성하고 전직을 하면, 직업을 변경할 수 없다.
아무리 죽어도 아이템, 경험치 등 잃을 뿐, 직업은 그대로 유지된다.
따라서 새로운 직업을 하고 싶다면, 새로운 캐릭터를 키워야 한다.
이 단점들을 해결하기 위해 컴포지션을 알아보고 활용해야 한다.
합성(Composition)
컴포지션은 상속을 통한 확장 대신, 필드로 클래스의 인스턴스를 참조하게 만드는 설계이다.
또한, 컴포지션은 HAS-A이라고 불린다.
즉, 상속은 피를 물려받는 것이라면, 컴포지션은 남의 물건을 빌려와 내 것처럼 사용하는 것이다.
예를 들면, 조립식 컴퓨터와 같다.
컴퓨터를 만들기 위해 CPU, RAM, SSD를 직접 제조(상속)하지 않고 각 기업에서 생산한 제품을 사 와서 조립(컴포지션)한다.
상위 클래스인 엔진과 하위 클래스인 로봇으로 상속을 사용한 경우
class Engine {
void start() { System.out.println("엔진 가동"); }
}
class Robot extends Engine {
// start()를 공짜로 쓸 수 있지만, 로봇이 곧 엔진이 되어버림.
}
로봇은 엔진 그 자체가 아니다.
하지만, 컴포지션을 사용한 경우
class Robot {
// 핵심: 다른 객체(Engine)를 내 부품(변수)으로 가짐 -> 이것이 컴포지션
private Engine myEngine;
public Robot() {
this.myEngine = new Engine(); // 부품 장착(조립)
}
// 위임(Delegation): 내가 직접 안 하고, 부품한테 시킴
void move() {
myEngine.start();
System.out.println("로봇이 움직입니다.");
}
}
위의 코드는 생성자에서 new 생성자를 받는 형식으로 쓰인다.
즉, 로봇 클래스에서 상위 엔진 클래스의 기능이 필요하다고 무조건 상속하지 말고, 따로 클래스 인스턴스 변수에 저장하여 가져다 쓴다는 원리를 이용한다.
이 밖에도 클래스뿐만이 아니라 추상 클래스, 인터페이스로도 가능하다.
컴포지션을 사용해야 하는 이유
1. 실행 중에 부품을 갈아 끼울 수 있다.
상속에서 예시로 든 게임 캐릭터의 전직은 변경할 수 없었지만, 칼을 든 캐릭터가 setWeapon(new Bow()) 메서드 한 번만 호출하면, 활로 교체한다.
즉, 컴포지션은 상속과 달리 실행 중 마음대로 바꿀 수 있다.
2. 클래스 폭발을 막을 수 있다.
기능이 많이 질 때 상속을 사용하면, 클래스 개수가 몇 배로 늘어날 수 있다.
상속의 문제점에서 예시로 나온 커피처럼, 재료가 많아질수록, 만들어야 할 클래스가 넘쳐난다.
3. 부모 클래스의 내부를 몰라도 된다.(캡슐화 보호)
컴포지션은 마치 블랙박스처럼 아무것도 모른 상태에서 기능을 가져올 수 있다.

상속(화이트박스): 부모 클래스의 코드가 어떻게 짜여 있는지 다 알아야 제대로 상속받을 수 있다.
또한 부모 클래스가 바뀌면, 자식도 바꿔야 한다.
컴포지션(블랙박스): 부모 클래스의 코드가 어떻게 돌아가든지 몰라도 호출만 하면 된다.
상속과 컴포지션 차이점
- 관계의 형태
- 상속은 부모 클래스와 자식 클래스 계층 구조를 만들어내는 일종의 IS-A 관계이지만,
컴포지션은 객체 안에 다른 객체를 포함하고 있는 일종의 HAS-A 관계이다.
- 상속은 부모 클래스와 자식 클래스 계층 구조를 만들어내는 일종의 IS-A 관계이지만,
- 객체 간의 결합도
- 상속은 부모 클래스의 수정이 자식 클래스에 영향을 미칠 수 있기 때문에 상속 구조가 복잡해질수록 유지보수가 어려워진다. 반면, 컴포지션은 객체 간의 결합도가 낮기 때문에 수정이 쉽고 유연하다.
그럼 상속을 사용하지 말아야 할까?
상속은 명확한 IS-A 관계에 있는 경우 상위 클래스가 확장할 목적으로 설계되어 있고 문서화가 잘 되었있다면 사용하면 된다.
이 말은 조슈아 블로치(Joshua Bloch)의《이펙티브 자바 (Effective Java)》핵심 철학을 정확하게 인용한 것이다.
면접 답변식 요약
상속과 컴포지션의 가장 큰 차이는 관계의 형태와 결합도입니다. 상속은 부모와 자식이 'IS - A(~은 ~이다.)' 관계로 맺어지며, 부모의 내부 구현을 자식이 알게 되는 강한 결합을 가집니다. 이 때문에 부모가 변경되면 자식에게도 영향이 가는 등 유지보수가 어려워질 수 있습니다.
반면, 컴포지션은 객체가 다른 객체를 'HAS-A(~은 ~을 가진다)' 관계로 포함하는 것으로, 인터페이스를 통해 기능만 가져다 쓰는 느슨한 결합(Black-box Reuse)을 유지합니다. 덕분에 캡슐화가 깨지지 않고 의존성이 낮아집니다.
그래서 저는 단순한 코드 재사용이 목적이거나, 실행 중에 기능을 유연하게 변경해야 할 때는 반드시 컴포지션을 사용합니다.
상속은 명확한 계층 구조가 필요하거나, 완벽한 IS-A 관계가 성립할 때만 제한적으로 사용하는 것을 원칙으로 하고 있습니다.
'Daily Dev Q&A 정리 템플릿' 카테고리의 다른 글
| 25.12.04 오버로딩과 오버라이딩에 대해서 구체적으로 설명하라는 질문에 대한 대답은? (0) | 2025.12.05 |
|---|---|
| 25.12.03 다형성을 좀 더 구체적으로 설명해주세요! 라는 질문에 대비하기 (0) | 2025.12.03 |
| 25.12.01 객체지향 언어 개념과 자바는 어떤 언어인가 (0) | 2025.12.01 |
| 25.11.28 자바의 예외 처리(Exception)에 대하여 (0) | 2025.11.30 |
| 25.11.28 자바는 컴파일러일까? 인터프리터일까? (0) | 2025.11.28 |