1. 상속
클래스 간에 is-a 관계를 나타낼 때 사용
// 부모 클래스
open class Animal(val name: String) {
open fun sound() = println("$name makes a sound.")
}
// 자식 클래스
class Dog(name: String) : Animal(name) {
override fun sound() = println("$name barks.")
}
// 사용
fun main() {
val animal = Animal("Generic Animal")
animal.sound() // 출력: Generic Animal makes a sound.
val dog = Dog("Buddy")
dog.sound() // 출력: Buddy barks.
}
장점
- 코드 재사용: 부모 클래스의 기능을 자식 클래스에서 쉽게 사용
- 일관성 유지: 자식 클래스는 부모 클래스의 모든 특성과 동작을 가져오므로 일관성을 유지
- 확장성: 기본 동작을 상속받아 일부만 변경하면 새로운 기능을 쉽게 추가
단점
- 강한 결합: 자식 클래스가 부모 클래스의 구현에 강하게 의존합니다. 부모 클래스가 변경되면 자식 클래스도 영향
- 유연성 부족: 자식 클래스는 항상 부모 클래스를 따라야 하므로 계층 구조가 고정
- 코드 오염: 부모 클래스의 모든 메서드와 속성이 자식 클래스에 포함되기 때문에 불필요한 동작까지 상속 가능
2. 합성
클래스 간에 has-a 관계를 나타낼 때 사용
// 다른 클래스의 인스턴스를 포함
class Engine {
fun start() = println("Engine started.")
fun stop() = println("Engine stopped.")
}
class Car(val engine: Engine) {
fun drive() {
engine.start()
println("Car is driving.")
}
fun park() {
println("Car is parked.")
engine.stop()
}
}
// 사용
fun main() {
val engine = Engine()
val car = Car(engine)
car.drive()
// 출력:
// Engine started.
// Car is driving.
car.park()
// 출력:
// Car is parked.
// Engine stopped.
}
장점
- 유연성: 한 클래스의 기능을 여러 클래스에서 재사용 가능
- 느슨한 결합: 구성 요소의 내부 구현이 변경되어도 이를 사용하는 클래스에 영향 없음
- 테스트 용이성: 구성 요소를 독립적으로 테스트 가능
- 동적 기능 교체: 런타임에 객체를 교체하거나 기능을 변경 가능
단점
- 조금 더 복잡한 구현: 상속보다 더 많은 코드가 필요
- 구성 요소 관리 필요: 포함된 객체의 수명 주기를 관리
상속과 합성의 차이
- 상속: 클래스 간 강한 결합으로, 부모의 모든 동작을 자식이 상속
- 합성: 느슨한 결합으로, 동작을 재사용하려고 다른 클래스의 인스턴스를 포함
상속 vs 합성 선택 기준
- 상속은 클래스 계층 구조가 명확하고 is-a 관계가 성립할 때 사용
- 합성은 유연성과 재사용성을 높이기 위해 has-a 관계가 적합할 때 사용
상황별 선택 기준
상속을 사용하는 경우
- 클래스 간에 명확한 is-a 관계가 있을 때.
- 예: Dog는 Animal이다.
- 부모 클래스의 모든 기능이 자식 클래스에 적합할 때.
- 코드의 재사용성과 구조화가 더 중요할 때.
open class Shape {
open fun draw() = println("Drawing a shape")
}
class Circle : Shape() {
override fun draw() = println("Drawing a circle")
}
합성을 사용하는 경우
- 클래스 간에 has-a 관계가 있을 때.
- 예: Car는 Engine을 가진다.
- 코드의 유연성과 독립성이 중요한 경우.
- 재사용해야 하는 기능이 여러 클래스에 걸쳐 있을 때.
class Car(private val engine: Engine) {
fun startCar() {
engine.start()
println("Car is running")
}
}
합성이 항상 상속보다 나은 것은 아니며, 둘은 서로 다른 목적에 맞게 사용
하지만 합성이 상속보다 일반적으로 더 나은 선택이 되는 경우가 많음
왜 합성이 더 나은 선택일 수 있는가?
- 유연성: 상속은 고정된 계층 구조를 만드는데, 이는 시간이 지나면서 확장에 장애가 될 수 있음
합성은 필요에 따라 개별 기능을 조합하여 사용 가능 - 변화에 대한 내성: 합성은 구성 요소 간의 결합을 느슨하게 유지하여 시스템의 일부가 변경되어도 다른 부분에 영향을 미치지 않음
- 테스트 가능성: 개별 구성 요소를 독립적으로 테스트할 수 있으므로 더 쉽게 오류를 식별 가능
결론
- 상속은 간단한 계층 구조와 명확한 is-a 관계가 있을 때 적합
- 합성은 더 유연하고 변경 가능성이 높은 시스템에서 선호
- 상속 대신 인터페이스와 합성을 조합하여 설계하는 것이 일반적으로 더 권장
- "Favor composition over inheritance" (상속보다 합성을 선호하라)는 객체 지향 설계의 중요한 원칙