[Java]다형성-2

다형성(2)

왜 다형성을 활용해야 할까?

image-20241103171418070

위와 같은 각 클래스가 있고, 각 클래스에는 각 동물의 울음소리를 표현하는 단순한 sound()메서드를 구현하였다. 단순히 각 동물의 소리를 출력하는 프로그램을 만들었다고 했을 때 코드는 아래와 같을 것이다.

public class AnimalSoundMain {

    public static void main(String[] args) {
        Dog dog = new Dog();
        Cat cat = new Cat();
        Caw caw = new Caw();

        System.out.println("동물 소리 테스트 시작");
        dog.sound();
        System.out.println("동물 소리 테스트 종료");

        System.out.println("동물 소리 테스트 시작");
        cat.sound();
        System.out.println("동물 소리 테스트 종료");
      
        System.out.println("동물 소리 테스트 시작");
        caw.sound();
        System.out.println("동물 소리 테스트 종료");
    }
}

만약, 이 상태에서 동물이 추가 된다면 어떻게 해야할까?

Duck duck = new Duck();
System.out.println("동물 소리 테스트 시작");
duck.sound();
System.out.println("동물 소리 테스트 종료");

위와 같이 새로운 동물 객체를 생성하는 코드와, 해당 동물의 메서드를 사용하는 코드가 메인클래스에 중복으로 계속 추가 되어야 할 것이다. 이러한 중복을 사용하기 위해 배열과 for문 또는 메서드를 활용할 수 있을까? 메서드를 사용하는 경우 각 메서드에 받게 되는 매개변수의 타입이 모두 다르기 때문에 하나의 메서드로 중복을 제거할 수 없고, 배열과 for문을 사용하는 경우 역시 배열의 타입을 먼저 지정해주어야 하는데 각 동물의 타입이 모두 다르기 때문에 하나의 배열을 사용할 수 없다. 즉, 코드의 중복을 제거하려고 할 때 각 객체의 타입이 다르다는 점이 공통적으로 문제가 된다는 것이다.

바로 이 때, 우리는 다형성과 메서드 오버라이딩을 사용하여 타입이 다르다는 문제를 극복하고 코드의 중복을 제거할 수 있다.

image-20241103172400940

다형성을 활용하기 위해 Animal이라는 부모 클래스르 만들고 sound()메서드를 만든 뒤 상속을 사용하고, 메서드 오버라이딩을 사용하여 각 동물의 울음소리를 구현해보자. 이 경우 cat, dog, caw의 모든 타입을 받을 수 있는 Animal 타입을 매개변수로 넘겨 하나의 메서드를 만들어 코드의 중복을 제거할 수 있다.

‘오버라이딩 된 메서드’가 항상 우선권을 갖기 때문에, animal.sound()에서는 매개변수로 들어 온 하위 타입에서 오버라이딩 된 메서드가 항상 우선적으로 호출 되기 때문에 각 타입에 맞는 메서드가 출력된다. 즉, 다형성 + 메서드 오버라이딩의 특성으로 우리는 아래와 같이 코드의 중복을 제거할 수 있었다.

public class AnimalPolyMain {
    public static void main(String[] args) {
        Dog dog = new Dog();
        Cat cat = new Cat();
        Caw caw = new Caw();
        Duck duck = new Duck();

        soundAnimal(dog);
        soundAnimal(cat);
        soundAnimal(caw);
        soundAnimal(duck);
    }
    private static void soundAnimal(Animal animal) {
        System.out.println("동물 소리 테스트 시작");
        animal.sound();
        System.out.println("동물 소리 테스트 종료");
    }
}

코드의 중복은 제거하였지만, 위의 코드는 완벽한 코드는 아니다. 2가지의 문제점이 남아있기 때문이다.

  1. Animal 클래스를 사용할 수 있는 문제 Animal animal = new Animal();이렇듯 Animal 객체를 직접 생성하여 사용할 일이 있을까? Animal 클래스는 다형성을 위해 필요한 추상적인 클래스일 뿐 실제로 인스턴스를 생성해서 사용할 일은 없다. 하지만 Animal 역시 클래스이므로 인스턴스를 생성해서 사용하는데에 제약은 없으므로 누군가 new Animal() 처럼 Animal 클래스의 인스턴스를 생성할 수도 있는 것이다. 하지만 이 인스턴스는 작동은 하지만 제대로 된 기능을 수행하지 않는다.

  2. Animal 클래스를 상속받는 곳에서 sound() 메서드 오버라이딩을 하지 않을 가능성 Animal 클래스를 상속받는 새로운 동물 클래스를 만들었는데 해당 클래스에서 sound() 메서드를 오버라이딩 하지 않았을 수도 있다. 이 경우 해당 동물의 메서드가 없다면, 부모인 Animal.sound()를 호출하게 되어 문법적인 오류를 발생하지 않겠지만, 개발자가 기대한 기능을 수행하지는 못하게 되는 것이다.

따라서, 이러한 문제를 사전에 방지할 수 있도록 제약을 걸어야 할 필요가 있다.
추상 클래스와 추상 메서드를 사용하면 이런 문제를 한번에 해결할 수 있다!

🔻 추상 클래스

위 예시의 Animal 과 같이 부모 클래스로서 제공되지만 실제로 생성되서는 안되는 클래스를 추상 클래스라 한다.
말 그대로 추상적인 개념을 제공하는 클래스로 실체인 인스턴스가 존재하지 않고 상속을 목적으로 사용된다.

abstract class AbstractAnimal{...}

앞에 abstract키워드를 붙여주어 표현한다. 기존 클래스와 완전히 같지만 new AbstractAnimal() 처럼 객체를 생성할 수 없다는 점이 다르다.

추상 메서드

부모 클래스를 상속 받는 자식 클래스가 반드시 오버라이딩 해야 하는 메서드를 부모 클래스에 정의할 수 있다. 이것을 추상 메서드라 한다. 추상 메서드는 이름 그대로 추상적인 개념을 제공하는 메서드이다. 따라서 실체가 존재하지 않고, 메서드 바디가 없다.

public abstract void sound();

추상 클래스와 마찬가지로 abstract키워드를 붙여서 표현한다.

  • 추상 메서드는 바디가 없으므로 추상 메서드를 가진 클래스는 작동하지 않는 메서드를 가진 불완전한 클래스라고 할 수 있다. 따라서 추상 메서드가 하나라도 포함 된 클래스는 반드시 추상 클래스로 선언해야 한다.
  • 추상 메서드는 상속받는 자식 클래스가 반드시 오버라이딩하여 사용해야한다.

추상클래스와 추상메서드를 사용하면 위에서 언급한 2가지의 문제점을 완전하게 해결 할 수 있다.

(+) 순수 추상 클래스

모든 메서드가 추상 메서드인 추상 클래스를 일컫는다. 즉, 코드를 실행할 바디 부분이 전혀 없이 단지 다형성의 부모 타입으로써의 역할만을 하기 위한 껍데기 클래스로 다음과 같은 특징을 가진다.

  • 인스턴스를 생성할 수 없다.
  • 상속은 받은 자식은 반드시 모든 추상 메서드를 오버라이딩해야한다.
  • 주로 다형성을 위해 사용한다.

🔻 인터페이스

앞서 설명한 순수 추상 클래스를 더 편리하게 사용할 수 있는 기능을 제공하 개념이다. 인터페이스는 class가 아니라 interface키워드를 사용한다.

public interface InterfaceAnimal {
  public abstract void sound();
  public abstract void move();
}

순수 추상 클래스와 동일한 특성을 갖지만, 약간의 편의 기능이 추가 된다.

  • 인터페이스의 메서드는 모두 public abstract지만 해당 키워드는 생략이 가능하며, 생략이 권장된다.
  • 인터페이스에 멤버 변수를 선언하는 경우, 해당 멤버 변수는 항상 public static final이다. 하지만 이 키워드 또한 생략할 수 있다.
  • 인터페이스는 다중 구현(다중 상속)을 지원한다.

순수 추상 클래스와 궤가 같지만, 부모 클래의 기능을 자식이 물려받는 걸 상속이라고 부르는 것과 달리 부모 인터페이스의 기능을 자식이 물려받을 때는 구현이라는 키워드를 사용하고 문법적으로도 implements를 사용한다. 만약 InterFaceAnimal 인터페이스를 구현하는 자식 클래스는 아래와 같이 표현한다.

public class Dog implements InterFaceAnimal{...}

📍 상속(extends) vs 구현(implemets)

상속은 이름 그대로 부모의 기능을 물려 받는 것이 목적이다. 하지만 인터페이스는 모든 메서드가 추상 메서드이다. 따라서 물려받을 수 있는 기능이 없고, 오히려 인터페이스에 정의한 모든 메서드를 자식이 오버라이딩 해서 기능을 구현해야 한다. 따서서 구현한다고 표현한다. 표현은 다르지만 사실상 자바 입장에선 동일하다. 구현 역시 일반 상속 구조와 동일하게 작동한다.

📍 인터페이스를 사용해야 하는 이유?

  1. 제약 : 인터페이스를 구현하는 곳에서는 인터페이스의 메서드를 반드시 구현해야하 제약이 발생한다. 추상 클래스의 경우 실행 가능한 기능이 있는 메서드가 추가 될 수 도 있고, 이런 경우 추가 된 기능들을 자식 클래스에서 구현하지 않을 수도 있다. 하지만 인터페이스는 이러한 예외없이 반드시 모든 메서드를 구현해야하므로 혹시 모를 오류의 가능성을 원천 차단할 수 있다.
  2. 다중 구현 : 클래스이 상속은 부모를 하나만 지정할 수 있다. 반면 인터페이스는 부모를 여럿 지정하는 다중 상속(다중 구현)이 가능하다.

인터페이스의 다중구현

왜 다중 상속은 허용하지 않지만, 다중 구현은 허용되는 걸까? image-20241103181849032image-20241103182001423 다중 상속이 허용되지 않는 이유는 왼쪽 그림처럼 부모 클래스에 동일한 이름의 메서드가 구현되어 있는 경우, 각 부모 클래스를 상속받은 자식 클래스에서 해당 메서드를 호출 할 때 둘 중 어떤 메서드를 호출해야하는가에 대한 문제, 즉 다이아몬드 문제를 발생시키기 때문이다.
하지만 인터페이스의 경우 각 인터페이스에 선언한 메서드는 모두 추상클래스이다. 어차피 구현체는 자식 클래스에 있기 때문에 자식 클래스의 메서드를 사용하게 되므로, 부모의 어떤 메서드를 호출할 지 고민할 필요가 없는 것이다. 그렇기 때문에 자바에서는 다중 상속은 허용하지 않지만 다중 구현은 허용한다.

Leave a comment