Backend/Java

[Java] 상속(Inheritance)보다는 컴포지션(Composition)

누구세연 2024. 8. 24. 16:12

객체지향 프로그래밍에서는 코드 재사용과 구조화를 위해 상속(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
    }
}

상속의 장점

  1. 코드의 재사용성
    부모 클래스의 코드가 자식 클래스에 재사용되므로, 동일한 기능을 다시 작성할 필요가 없습니다.
  2. 확장성
    기존 클래스를 확장하여 새로운 기능을 추가할 수 있습니다.
    자식 클래스는 부모 클래스의 속성과 메서드를 그대로 사용할 수 있으며, 추가로 자신만의 속성이나 메서드를 정의할 수 있습니다.
  3. 타입 계층 구조
    상속을 통해 명확한 계층 구조를 만들 수 있어 코드의 구조가 더욱 체계적이 됩니다.
    예를 들어, 모든 동물 클래스는 Animal이라는 부모 클래스를 상속받을 수 있습니다.

상속의 단점

  1. 캡슐화 저하
    자식 클래스가 부모 클래스의 내부 구현에 의존하게 되면, 부모 클래스가 변경될 때 자식 클래스도 영향을 받을 수 있습니다.
    이는 클래스 간의 강한 결합(tight coupling)을 초래하여 유지보수를 어렵게 만듭니다.
  2. 다중 상속 문제
    자바와 같은 언어에서는 다중 상속을 지원하지 않으며, C++과 같은 언어에서는 다중 상속을 사용할 때 발생하는 모호성 문제(다이아몬드 문제 등)가 있습니다.
  3. 유연성 부족
    상속을 사용하면 클래스가 부모 클래스로부터 물려받은 기능을 그대로 유지해야 하므로, 필요에 따라 동적으로 기능을 변경하는 것이 어려울 수 있습니다.

 

컴포지션(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
    }
}

컴포지션의 장점

  1. 유연성
    컴포지션을 사용하면 객체 간의 관계를 동적으로 변경할 수 있습니다.
    이는 실행 시간(runtime)에 객체의 구성 요소를 변경하거나 교체할 수 있게 합니다.
  2. 캡슐화 강화
    각 클래스는 자신만의 인터페이스를 가지며, 내부 구현에 대해 외부에 노출하지 않습니다.
    이를 통해 클래스 간의 결합도가 낮아져 유지보수가 용이합니다.
  3. 다중 사용성
    여러 클래스에서 동일한 기능을 공유할 수 있습니다.
    예를 들어, 여러 클래스가 같은 인터페이스를 구현하고, 그 인터페이스를 활용하여 필요한 기능을 조립할 수 있습니다.
  4. 명확한 역할 분리
    객체의 책임이 명확하게 분리되어 코드의 가독성이 향상됩니다.
    각 요소가 자신의 역할에 집중하여 기능을 제공할 수 있습니다.

컴포지션의 단점

  1. 객체 수 증가
    컴포지션을 사용하면 많은 객체가 생성될 수 있습니다.
    이는 메모리 사용량을 증가시킬 수 있습니다.
  2. 구현의 복잡성 증가
    상속에 비해 코드가 더 복잡해질 수 있으며, 객체 간의 관계를 명확하게 이해해야 합니다.

 

상속보다는 컴포지션을 선호해야 하는 이유

컴포지션은 '객체지향 프로그래밍의 유연성과 모듈성을 최대한 활용할 수 있는방식' 입니다.

상속보다는 컴포지션을 선호하라
Prefer composition over inheritance

 

일반적으로 널리 권장되는 이유는 아래와 같습니다.

  1. 유연한 설계
    컴포지션은 클래스 간의 관계를 런타임에 설정할 수 있게 해 줍니다.
    이는 프로그램이 실행되는 동안 객체의 구성을 동적으로 변경할 수 있는 유연성을 제공합니다.
    예를 들어 다양한 행동(Behavior)을 가진 객체들을 조합하여 새로운 객체를 생성할 수 있습니다.
  2. 강한 캡슐화
    컴포지션을 사용하면 각 클래스가 자신의 데이터를 직접 관리하며, 다른 클래스에 대한 의존성이 줄어듭니다.
    이는 클래스의 내부 구현이 변경되더라도 다른 클래스에 미치는 영향을 최소화하여 유지보수가 용이해집니다.
  3. 재사용성 향상
    컴포지션을 통해 클래스의 기능을 더 작은 단위로 나누어 재사용할 수 있습니다.
    이는 코드의 재사용성을 극대화하고, 개발자가 필요한 기능을 쉽게 추가하거나 제거할 수 있게 합니다.
  4. 간결한 클래스 계층 구조
    상속을 과도하게 사용하면 복잡한 클래스 계층 구조가 형성될 수 있으며, 이는 코드의 이해를 어렵게 만들고 유지보수를 복잡하게 만듭니다. 반면 컴포지션을 사용하면 이러한 계층 구조를 피할 수 있습니다.

 

 

💡 상속과 컴포지션은 모두 객체지향 프로그래밍에서 중요한 개념이며, 상황에 따라 적절히 사용되어야 합니다.
상속은 계층 구조를 형성하고, 명확한 부모-자식 관계를 정의할 때 유용합니다.
그러나 컴포지션은 코드의 유연성, 재사용성, 유지보수성을 높이기 때문에 더 많은 상황에서 사용하기 적합합니다.

따라서, 객체지향 설계를 할 때는 "상속보다는 컴포지션을 선호하라"는 원칙을 염두에 두고, 클래스 간의 관계를 설계하는 것이 좋습니다. 이를 통해 보다 유연하고 확장 가능한 소프트웨어를 개발할 수 있습니다.