구성(Composition)은 하나의 클래스가 다른 클래스의 인스턴스를 자신의 필드로 가지도록 하는 설계 기법입니다. 반면 상속(Inheritance)은 클래스를 확장(extends)함으로써 한 객체가 부모 객체의 속성과 동작을 물려받는 메커니즘입니다.
상속과 구성은 모두 클래스 간의 관계를 맺어 코드 재사용성을 높여 줍니다. 흥미로운 점은 구성만으로도 상속이 제공하는 기능을 대부분 구현할 수 있다는 것입니다. 그렇다면 두 방식은 어떤 점에서 다를까요? 아래 표에서 핵심 차이점을 확인해 보세요.
| 번호 | 항목 | 상속(Inheritance) | 구성(Composition) |
|---|---|---|---|
| 1 | 기본 개념 | "is-a(~이다)" 관계를 나타냅니다. | "has-a(~를 가진다)" 관계를 나타냅니다. |
| 2 | 코드 재사용 | 클래스는 하나의 클래스만 확장할 수 있으므로, 코드 재사용 범위가 단일 클래스에 국한됩니다. | 여러 클래스에서 동시에 코드를 재사용할 수 있습니다. |
| 3 | 적용 시점 | 컴파일 타임에 기능이 고정됩니다. | 런타임에 동적으로 쉽게 구성할 수 있습니다. |
| 4 | final 클래스 | final 클래스의 코드는 재사용할 수 없습니다. | final 클래스라도 코드 재사용이 가능합니다. |
| 5 | 메서드 노출 | 부모 클래스의 public 및 protected 메서드가 모두 외부에 노출됩니다. | 내부 객체가 노출되지 않으며, public 인터페이스를 통해서만 상호작용합니다. |
상속(Inheritance) 예제
class Animal{
String name = "Orio";
}
class Dog extends Animal{
String type = "Dog";
public static void main(String args[]){
Dog p = new Dog();
System.out.println("Name:" + p.name);
System.out.println("Type:" + p.type);
}
}위 예제에서 Dog 클래스는 Animal 클래스를 확장했습니다. 따라서 별도의 선언 없이도 부모 클래스의 name 필드를 그대로 사용할 수 있습니다. 이것이 바로 "is-a" 관계, 즉 개(Dog)는 동물(Animal)이다라는 관계를 코드로 표현한 것입니다.
구성(Composition) 예제
public class Student {
}
public class College {
private Student student;
public College() {
this.student = new Student();
}
}College 클래스는 Student 객체를 private 필드로 가지고, 생성자 안에서 직접 인스턴스를 생성합니다. 이처럼 다른 클래스의 인스턴스를 내부에 두고 필요한 기능만 외부에 공개하는 방식이 구성입니다. 대학(College)은 학생(Student)을 가진다는 "has-a" 관계의 전형적인 예입니다.
그렇다면 무엇을 선택해야 할까?
두 클래스 사이에 명확한 "is-a" 관계가 성립한다면 상속이 자연스러운 선택입니다. 하지만 단순히 기능을 재사용하고 싶은 것이라면 구성이 더 유연한 방법입니다. 실제로 GoF 디자인 패턴에서는 "상속보다 구성을 우선하라(Composition over Inheritance)"는 원칙을 강조하는데, 구성은 런타임에 동작을 교체하기 쉽고 부모 클래스의 변경에 덜 취약하다는 장점이 있기 때문입니다.