객체지향 프로그래밍에서는 코드 재사용과 구조화를 위해 상속(Inheritance)과 컴포지션(Composition)이라는 두 가지 주요 개념을 사용합니다. 이 두 가지는 클래스 간의 관계를 정의하고, 기능을 재사용하는 방법을 제공합니다.
그러나 두 개념은 본질적으로 다르며, 특정 상황에서는 하나가 다른 것보다 더 유리할 수 있습니다. 👀
이번 글에서는 컴포지션이 상속보다 더 나은 선택이 될 수 있는 이유에 대해 알아보겠습니다.📝
상속(Inheritance)란?
상속은 기존 클래스(부모 클래스, 슈퍼 클래스)의 속성과 메서드를 새로운 클래스(자식 클래스, 서브 클래스)가 물려받아 사용하는 개념입니다. 이를 통해 코드의 중복을 줄이고, 이미 작성된 기능을 재사용할 수 있습니다.
상속을 사용하여 동물(Animal) 클래스를 기본 클래스로 정의하고, 이를 상속받아 고양이(Cat)와 개(Dog) 클래스를 정의하는 예를 들어보겠습니다.
// 상위 클래스 Animal
class Animal {
void makeSound() {
System.out.println("Some generic animal sound");
}
}
// 하위 클래스 Cat
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Meow");
}
}
// 하위 클래스 Dog
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Woof");
}
}
// 사용 예시
public class Main {
public static void main(String[] args) {
Animal myCat = new Cat();
Animal myDog = new Dog();
myCat.makeSound(); // 출력: Meow
myDog.makeSound(); // 출력: Woof
}
}
상속의 장점
- 코드의 재사용성
부모 클래스의 코드가 자식 클래스에 재사용되므로, 동일한 기능을 다시 작성할 필요가 없습니다. - 확장성
기존 클래스를 확장하여 새로운 기능을 추가할 수 있습니다.
자식 클래스는 부모 클래스의 속성과 메서드를 그대로 사용할 수 있으며, 추가로 자신만의 속성이나 메서드를 정의할 수 있습니다. - 타입 계층 구조
상속을 통해 명확한 계층 구조를 만들 수 있어 코드의 구조가 더욱 체계적이 됩니다.
예를 들어, 모든 동물 클래스는 Animal이라는 부모 클래스를 상속받을 수 있습니다.
상속의 단점
- 캡슐화 저하
자식 클래스가 부모 클래스의 내부 구현에 의존하게 되면, 부모 클래스가 변경될 때 자식 클래스도 영향을 받을 수 있습니다.
이는 클래스 간의 강한 결합(tight coupling)을 초래하여 유지보수를 어렵게 만듭니다. - 다중 상속 문제
자바와 같은 언어에서는 다중 상속을 지원하지 않으며, C++과 같은 언어에서는 다중 상속을 사용할 때 발생하는 모호성 문제(다이아몬드 문제 등)가 있습니다. - 유연성 부족
상속을 사용하면 클래스가 부모 클래스로부터 물려받은 기능을 그대로 유지해야 하므로, 필요에 따라 동적으로 기능을 변경하는 것이 어려울 수 있습니다.
컴포지션(Composition)이란?
컴포지션은 객체가 다른 객체를 포함하는 방식으로 기능을 재사용하는 방법입니다.
이를 통해 큰 객체를 구성하기 위해 작은 객체들을 조립할 수 있으며, 이러한 객체들은 독립적으로 존재할 수 있습니다.
컴포지션을 사용하여 동물의 다양한 행동(Behavior)을 인터페이스로 정의하고, 이를 구현한 클래스를 사용하여 고양이(Cat)와 개(Dog)를 구성해 보겠습니다.
// 행동 인터페이스 정의
interface SoundBehavior {
void makeSound();
}
// Meow 행동을 구현한 클래스
class Meow implements SoundBehavior {
@Override
public void makeSound() {
System.out.println("Meow");
}
}
// Woof 행동을 구현한 클래스
class Woof implements SoundBehavior {
@Override
public void makeSound() {
System.out.println("Woof");
}
}
// Animal 클래스가 SoundBehavior를 가지고 있음 (컴포지션)
class Animal {
private SoundBehavior soundBehavior;
public Animal(SoundBehavior soundBehavior) {
this.soundBehavior = soundBehavior;
}
void performSound() {
soundBehavior.makeSound();
}
void setSoundBehavior(SoundBehavior soundBehavior) {
this.soundBehavior = soundBehavior;
}
}
// 사용 예시
public class Main {
public static void main(String[] args) {
Animal myCat = new Animal(new Meow());
Animal myDog = new Animal(new Woof());
myCat.performSound(); // 출력: Meow
myDog.performSound(); // 출력: Woof
// 런타임에 행동을 변경
myCat.setSoundBehavior(new Woof());
myCat.performSound(); // 출력: Woof
}
}
컴포지션의 장점
- 유연성
컴포지션을 사용하면 객체 간의 관계를 동적으로 변경할 수 있습니다.
이는 실행 시간(runtime)에 객체의 구성 요소를 변경하거나 교체할 수 있게 합니다. - 캡슐화 강화
각 클래스는 자신만의 인터페이스를 가지며, 내부 구현에 대해 외부에 노출하지 않습니다.
이를 통해 클래스 간의 결합도가 낮아져 유지보수가 용이합니다. - 다중 사용성
여러 클래스에서 동일한 기능을 공유할 수 있습니다.
예를 들어, 여러 클래스가 같은 인터페이스를 구현하고, 그 인터페이스를 활용하여 필요한 기능을 조립할 수 있습니다. - 명확한 역할 분리
객체의 책임이 명확하게 분리되어 코드의 가독성이 향상됩니다.
각 요소가 자신의 역할에 집중하여 기능을 제공할 수 있습니다.
컴포지션의 단점
- 객체 수 증가
컴포지션을 사용하면 많은 객체가 생성될 수 있습니다.
이는 메모리 사용량을 증가시킬 수 있습니다. - 구현의 복잡성 증가
상속에 비해 코드가 더 복잡해질 수 있으며, 객체 간의 관계를 명확하게 이해해야 합니다.
상속보다는 컴포지션을 선호해야 하는 이유
컴포지션은 '객체지향 프로그래밍의 유연성과 모듈성을 최대한 활용할 수 있는방식' 입니다.
상속보다는 컴포지션을 선호하라
Prefer composition over inheritance
일반적으로 널리 권장되는 이유는 아래와 같습니다.
- 유연한 설계
컴포지션은 클래스 간의 관계를 런타임에 설정할 수 있게 해 줍니다.
이는 프로그램이 실행되는 동안 객체의 구성을 동적으로 변경할 수 있는 유연성을 제공합니다.
예를 들어 다양한 행동(Behavior)을 가진 객체들을 조합하여 새로운 객체를 생성할 수 있습니다. - 강한 캡슐화
컴포지션을 사용하면 각 클래스가 자신의 데이터를 직접 관리하며, 다른 클래스에 대한 의존성이 줄어듭니다.
이는 클래스의 내부 구현이 변경되더라도 다른 클래스에 미치는 영향을 최소화하여 유지보수가 용이해집니다. - 재사용성 향상
컴포지션을 통해 클래스의 기능을 더 작은 단위로 나누어 재사용할 수 있습니다.
이는 코드의 재사용성을 극대화하고, 개발자가 필요한 기능을 쉽게 추가하거나 제거할 수 있게 합니다. - 간결한 클래스 계층 구조
상속을 과도하게 사용하면 복잡한 클래스 계층 구조가 형성될 수 있으며, 이는 코드의 이해를 어렵게 만들고 유지보수를 복잡하게 만듭니다. 반면 컴포지션을 사용하면 이러한 계층 구조를 피할 수 있습니다.
💡 상속과 컴포지션은 모두 객체지향 프로그래밍에서 중요한 개념이며, 상황에 따라 적절히 사용되어야 합니다.
상속은 계층 구조를 형성하고, 명확한 부모-자식 관계를 정의할 때 유용합니다.
그러나 컴포지션은 코드의 유연성, 재사용성, 유지보수성을 높이기 때문에 더 많은 상황에서 사용하기 적합합니다.
따라서, 객체지향 설계를 할 때는 "상속보다는 컴포지션을 선호하라"는 원칙을 염두에 두고, 클래스 간의 관계를 설계하는 것이 좋습니다. 이를 통해 보다 유연하고 확장 가능한 소프트웨어를 개발할 수 있습니다.
'Backend > Java' 카테고리의 다른 글
| [Java] 직렬화(Serialization) (1) | 2024.09.17 |
|---|---|
| [Java] 람다 표현식(Lambda Expressions) (0) | 2024.09.16 |
| [Java] JPA의 @Lock 동시성 제어 (0) | 2024.08.12 |
| [Java] for 루프와 Stream API (0) | 2024.08.10 |
| [Java] final 불변 객체를 사용해야하는 이유 (0) | 2024.08.10 |