본문 바로가기
안드로이드/etc.

[Kotlin] 상속 vs 합성

by jinwo_o 2025. 8. 29.

https://inpa.tistory.com/entry/OOP-%F0%9F%92%A0-%EA%B0%9D%EC%B2%B4-%EC%A7%80%ED%96%A5%EC%9D%98-%EC%83%81%EC%86%8D-%EB%AC%B8%EC%A0%9C%EC%A0%90%EA%B3%BC-%ED%95%A9%EC%84%B1Composition-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0

 

💠 상속을 자제하고 합성(Composition)을 이용하자

상속과 합성 개념 정리 프로그래밍을 할때 가장 신경 써야 할 것 중 하나가 바로 코드 중복을 제거하여 재사용 함으로써 변경, 확장을 용이하게 만드는 것이다. 그런 관점에서 상속과 합성은 객

inpa.tistory.com

상속(Inheritance)

  • 자식 클래스는 부모 클래스의 자원을 물려 받게 되며, 부모 클래스와 다른 부분만 추가하거나 재정의함으로서 기존 코드를 쉽게 확장할 수 있다.
  • 상속 관계를 is-a 관계로 표현하기도 한다.
  • 일반적인 클래스가 이미 구현이 되어 있는 상태에서 그보다 좀 더 구체적인 클래스를 구현하기 위해 사용되는 기법
  • 상위 클래스의 코드를 하위 클래스가 재사용할 수 있다.
  • 객체 주제는 같지만, 서로 다른 속성이나 기능들을 가지고 있을 때, 이러한 구조를 상속 관계를 통해 논리적으로 개념적으로 연관 시키는 것
  • 명확한 is - a 관계에 있는 경우, 상위 클래스가 확장할 목적으로 설게되었고 문서화도 잘되어 있는 경우에 사용하면 좋다.

 

  • 상속을 제대로 활용하기 위해서는 부모 클래스의 내부 구현에 대해 상세하게 알아야 하기 때문에 자식 클래스와 부모 클래스 사이의 결합도가 높아질 수 밖에 없다.
  • 상속 관게는 컴파일 타임에 결정되고 고정되기 때문에 코드를 실행하는 도중에 변경할 수 없다.
  • 여러 기능을 조합해야 하는 설계에 상속을 이용하게 된다면, 모든 조합별로 클래스를 하나하나 추가해주어야 한다. 이것을 클래스 폭발 문제라고 한다.
  • Java8부터는 인터페이스의 디폴트 메서드 기능이 나오며서 인터페이스 내에서 로직 구현이 가능하여 상속의 장점이 약화되었다고 할 수 있다.
  • 결과적으로 상속은 클래스 간의 관계를 한눈에 파악할 수 있고 코드를 재사용할 수 있는 쉽고 간단한 방법일지는 몰라도 우아한 방법이라고 할 수는 없다.

 

  • 현업에서도 가능하면 상속을 지양하는 편이며, 클래스 상속을 해야할 때는 정말 개념적으로 연관 관계가 있을 때만 하는 상당히 제한적으로 선택적으로 다뤄진다.
  • 상위 클래스에 결함이 있을 때 상속을 하게 되면, 부모 클래스의 결함도 자식 클래스에게 넘어오게 된다.
  • 부모 클래스와 자식 클래스 사이의 개념적인 결합으로 인해, 부모 클래스를 변경할 때 자식 클래스도 함께 변경해야 하는 문제가 있다.
  • 자식 클래스가 부모 클래스의 메서드를 오버라이딩할 때, 자식 클래스가 부모 클래스의 메서드 호출 방법에 영향을 받는 문제가 있다.
  • 단일 상속의 한계

 

합성(조합, Compoision)

  • 기존 클래스를 상속을 통한 확장하는 대신에, 필드로 클래스의 인스턴스를 참조하게 만드는 설계이다.
  • 서로 관련없는 이질적인 클래스의 관계에서 한 클래스가 다른 클래스의 기능을 사용하여 구현해야 한다면, 합성의 방식을 사용한다고 보면 된다.
  • 가령 학생이 수강하는 과목들이나, 자동차와 엔진 종류 간의 관계 같이 아주 연관이 없지는 않지만 상속 관계로 맺기에는 애매한 것들을 다루는 것으로 볼 수도 있다.
  • 합성은 객체 간의 관계가 수직 관계가 아닌 수평 관계가 된다.

 

class Car(private val engine: Engine) {

    fun drive() {
        println("${engine.type} 엔진으로 드라이브~")
    }

    fun brakes() {
        println("${engine.type} 엔진으로 브레이크~")
    }
}

class Engine(val type: String) // 디젤, 가솔린, 전기

fun main() {
    val dieselCar = Car(Engine("디젤"))
    dieselCar.drive() // 디젤 엔진으로 드라이브~

    val electricCar = Car(Engine("전기"))
    electricCar.drive() // 전기 엔진으로 드라이브~
}
  • 위의 초기화 코드에서 볼 수 있듯이, 마치 생성자에서 다른 클래스의 객체를 매개변수로 받는 형식으로 쓰여진다.
  • 즉, Car 클래스가 Engine 클래스의 기능이 필요하다고 해서 무조건 상속하지 말고, 따로 클래스 인스턴스 변수에 저장하여 가져다 쓴다는 원리이다.
  • 이 방식을 포워딩(forwarding)이라고 하며, 필드의 인스턴스를 참조해 사용하는 메서드를 포워딩 메서드(forwarding method)라고 부른다.
  • 그래서 클래스 간의 합성 관계를 사용하는 데 다른 말로 Has-A 관계라고도 한다.
  • 클래스 뿐만 아니라 추상 클래스, 인터페이스로도 가능하다.
  • 합성을 이용하면 객체의 내부는 공개되지 않고 인터페이스를 통해 코드를 재사용하기 때문에, 구현에 대한 의존성을 인터페이스에 대한 의존성으로 변경하여 결합도를 낮출 수 있다.