객체지향

상속과 합성

코딩공부 2025. 1. 5. 22:20

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.
}

 

장점

  1. 코드 재사용: 부모 클래스의 기능을 자식 클래스에서 쉽게 사용
  2. 일관성 유지: 자식 클래스는 부모 클래스의 모든 특성과 동작을 가져오므로 일관성을 유지
  3. 확장성: 기본 동작을 상속받아 일부만 변경하면 새로운 기능을 쉽게 추가

단점

  1. 강한 결합: 자식 클래스가 부모 클래스의 구현에 강하게 의존합니다. 부모 클래스가 변경되면 자식 클래스도 영향
  2. 유연성 부족: 자식 클래스는 항상 부모 클래스를 따라야 하므로 계층 구조가 고정
  3. 코드 오염: 부모 클래스의 모든 메서드와 속성이 자식 클래스에 포함되기 때문에 불필요한 동작까지 상속 가능

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.
}

 

장점

  1. 유연성: 한 클래스의 기능을 여러 클래스에서 재사용 가능
  2. 느슨한 결합: 구성 요소의 내부 구현이 변경되어도 이를 사용하는 클래스에 영향 없음
  3. 테스트 용이성: 구성 요소를 독립적으로 테스트 가능
  4. 동적 기능 교체: 런타임에 객체를 교체하거나 기능을 변경 가능 

단점

  1. 조금 더 복잡한 구현: 상속보다 더 많은 코드가 필요
  2. 구성 요소 관리 필요: 포함된 객체의 수명 주기를 관리

상속과 합성의 차이

  • 상속: 클래스 간 강한 결합으로, 부모의 모든 동작을 자식이 상속
  • 합성: 느슨한 결합으로, 동작을 재사용하려고 다른 클래스의 인스턴스를 포함

 

상속 vs 합성 선택 기준

  1. 상속은 클래스 계층 구조가 명확하고 is-a 관계가 성립할 때 사용
  2. 합성은 유연성과 재사용성을 높이기 위해 has-a 관계가 적합할 때 사용

상황별 선택 기준

상속을 사용하는 경우

  1. 클래스 간에 명확한 is-a 관계가 있을 때.
    • 예: Dog는 Animal이다.
  2. 부모 클래스의 모든 기능이 자식 클래스에 적합할 때.
  3. 코드의 재사용성과 구조화가 더 중요할 때.
open class Shape {
    open fun draw() = println("Drawing a shape")
}

class Circle : Shape() {
    override fun draw() = println("Drawing a circle")
}

 

합성을 사용하는 경우

  1. 클래스 간에 has-a 관계가 있을 때.
    • 예: Car는 Engine을 가진다.
  2. 코드의 유연성과 독립성이 중요한 경우.
  3. 재사용해야 하는 기능이 여러 클래스에 걸쳐 있을 때.
class Car(private val engine: Engine) {
    fun startCar() {
        engine.start()
        println("Car is running")
    }
}

 

합성이 항상 상속보다 나은 것은 아니며, 둘은 서로 다른 목적에 맞게 사용

하지만 합성이 상속보다 일반적으로 더 나은 선택이 되는 경우가 많음

 

왜 합성이 더 나은 선택일 수 있는가?

  1. 유연성: 상속은 고정된 계층 구조를 만드는데, 이는 시간이 지나면서 확장에 장애가 될 수 있음
    합성은 필요에 따라 개별 기능을 조합하여 사용 가능
  2. 변화에 대한 내성: 합성은 구성 요소 간의 결합을 느슨하게 유지하여 시스템의 일부가 변경되어도 다른 부분에 영향을 미치지 않음
  3. 테스트 가능성: 개별 구성 요소를 독립적으로 테스트할 수 있으므로 더 쉽게 오류를 식별 가능 

 

결론

  • 상속은 간단한 계층 구조와 명확한 is-a 관계가 있을 때 적합
  • 합성은 더 유연하고 변경 가능성이 높은 시스템에서 선호
  • 상속 대신 인터페이스합성을 조합하여 설계하는 것이 일반적으로 더 권장
  • "Favor composition over inheritance" (상속보다 합성을 선호하라)는 객체 지향 설계의 중요한 원칙

 

 

'객체지향' 카테고리의 다른 글

상속과 인터페이스  (0) 2025.01.05
객체지향  (1) 2025.01.05