서론

코드 재사용을 목적으로 상속을 사용하면 변경하기 어렵고 유연하지 못한 설계에 이를 확률이 높아진다. 상속은 타입 계층을 구조화하기 위해 사용해야 한다.

<aside> ❓ Q. 여기서 중요한 것은 “타입 계층을 구조화하기 위해서”라고 했는데 타입계층 구조화는 어떻게 하는거고, 어떤 의미인거지?

A.

타입 계층을 구조화한다는 것은 객체 지향 프로그래밍에서 매우 중요한 개념입니다. 이는 클래스들 간의 관계를 명확하게 정의하고, 이를 통해 코드의 재사용성과 유지보수성을 높이는 방법을 의미합니다. 타입 계층 구조화는 주로 상속과 인터페이스를 통해 이루어집니다. 상속을 사용하면 하위 클래스가 상위 클래스의 속성과 메소드를 재사용할 수 있으며, 인터페이스를 통해서는 구현을 강제하여 다형성을 실현할 수 있습니다.

다형성은 같은 인터페이스나 기본 클래스를 공유하는 다양한 객체들이 그 인터페이스나 클래스에 정의된 메소드를 다른 방식으로 실행할 수 있도록 하는 객체 지향 프로그래밍의 특징입니다. 이를 통해 다양한 타입의 객체를 동일한 방식으로 처리할 수 있게 됩니다.

예시

다음 예시에서는 타입 계층 구조화와 다형성을 보여주는 간단한 Java 코드를 살펴보겠습니다. 이 예시에서는 동물을 나타내는 기본 클래스 **Animal**과 이를 상속받는 두 하위 클래스 **Dog**와 **Cat**을 정의합니다. 또한, 이들 모두가 speak 메소드를 다르게 구현함으로써 다형성을 실현합니다.

// 기본 클래스
class Animal {
    void speak() {
        System.out.println("This animal speaks in a way unique to its species.");
    }
}

// Dog 클래스는 Animal 클래스를 상속받습니다.
class Dog extends Animal {
    @Override
    void speak() {
        System.out.println("Woof");
    }
}

// Cat 클래스는 Animal 클래스를 상속받습니다.
class Cat extends Animal {
    @Override
    void speak() {
        System.out.println("Meow");
    }
}

// 메인 클래스와 메인 메소드
public class Main {
    public static void main(String[] args) {
        Animal myDog = new Dog();
        Animal myCat = new Cat();

        // 동적 바인딩을 통해 실행 시점에 각 객체의 speak 메소드가 호출됩니다.
        myDog.speak(); // 출력: Woof
        myCat.speak(); // 출력: Meow

        // 배열을 사용하여 다형성을 더 쉽게 볼 수 있습니다.
        Animal[] animals = {new Dog(), new Cat()};
        for(Animal animal : animals) {
            animal.speak(); // 동적으로 올바른 메소드가 호출됩니다.
        }
    }
}

이 예시에서 Animal 클래스는 타입 계층의 최상위에 위치하며, **Dog**와 Cat 클래스는 이를 상속받아 speak 메소드를 각자의 방식으로 구현합니다. 이를 통해, **Dog**와 Cat 객체를 Animal 타입으로 처리할 수 있으며, 실행 시간에 Animal 참조를 통해 실제 객체의 speak 메소드가 호출됩니다. 이런 방식으로 타입 계층을 구조화하고 다형성을 활용하면, 코드의 재사용성과 유지보수성이 크게 향상됩니다.

</aside>

상속을 활용해 자식 클래스를 추가하려 한다면 스스로에게 질문을 해보길 권장한다.

  1. 상속을 사용하려는 목적이 코드 재사용인가 ?
  2. 클라이언트 관점에서 인스턴스들을 동일하게 행동하는 그룹으로 묶기 위해서인가 ? (그룹화)

초기에 상속은 타입 계층과 다형성을 구현할 수 있는 거의 유일한 방법이었다. 이에 상속에 대한 맹신 추종이 자라났다. 하지만 여전히 상속은 다형성을 구현할 수 있는 가장 일반적인 방법이다.

하지만 상속 이외에도 다형성을 구현할 수 있는 다양한 방법들을 제공하고 있다.

그럼, 어떤 다양한 방법들이 있는 것일까??

  1. 인터페이스(Interface) 인터페이스는 메서드의 시그니처만을 선언하는데, 구현 클래스는 인터페이스에 선언된 모든 메서드를 구현해야 합니다. 이 방법을 통해 다양한 클래스들이 같은 인터페이스를 구현함으로써 다형성을 실현할 수 있습니다. 인터페이스를 사용하면 클래스는 여러 인터페이스를 구현할 수 있으므로, 단일 상속의 제약에서 벗어날 수 있습니다.
  2. 추상 클래스(Abstract Class) 추상 클래스를 사용하면 몇몇 메서드는 구현하고, 나머지 메서드는 서브 클래스에서 구현하도록 남겨둘 수 있습니다. 이 방식은 추상 클래스를 상속받는 서브 클래스들이 다형성을 가지도록 할 수 있습니다.
  3. 컴포지션(Composition) 컴포지션은 "has-a" 관계를 통해 다형성을 실현합니다. 예를 들어, 특정 클래스가 다양한 행동을 가진 객체를 필드로 가질 수 있으며, 이 객체는 모두 같은 인터페이스를 구현합니다. 이를 통해 실행 시간에 다른 객체를 주입하여 다양한 행동을 실현할 수 있습니다.
  4. 전략 패턴(Strategy Pattern) 전략 패턴은 알고리즘의 일부분을 런타임에 교체할 수 있도록 해줍니다. 이 패턴을 사용하면 컨텍스트(사용하는 객체)는 전략 객체(알고리즘을 구현한 객체)를 바꿔 끼워 넣음으로써 다양한 작업을 수행할 수 있습니다. 모든 전략 객체는 동일한 인터페이스를 구현하므로 다형성을 실현할 수 있습니다.
  5. 델리게이션(Delegation) 델리게이션은 특정 작업을 다른 객체에 위임하는 방식을 말합니다. 이를 통해 다양한 객체들이 다형성을 실현할 수 있으며, 상속을 사용하지 않고도 코드 재사용성을 높일 수 있습니다.