[Java]다형성-2
다형성(2)
왜 다형성을 활용해야 할까?
위와 같은 각 클래스가 있고, 각 클래스에는 각 동물의 울음소리를 표현하는 단순한 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문을 사용하는 경우 역시 배열의 타입을 먼저 지정해주어야 하는데 각 동물의 타입이 모두 다르기 때문에 하나의 배열을 사용할 수 없다. 즉, 코드의 중복을 제거하려고 할 때 각 객체의 타입이 다르다는 점이 공통적으로 문제가 된다는 것이다.
바로 이 때, 우리는 다형성과 메서드 오버라이딩을 사용하여 타입이 다르다는 문제를 극복하고 코드의 중복을 제거할 수 있다.
다형성을 활용하기 위해 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가지의 문제점이 남아있기 때문이다.
-
Animal 클래스를 사용할 수 있는 문제
Animal animal = new Animal();
이렇듯 Animal 객체를 직접 생성하여 사용할 일이 있을까? Animal 클래스는 다형성을 위해 필요한 추상적인 클래스일 뿐 실제로 인스턴스를 생성해서 사용할 일은 없다. 하지만 Animal 역시 클래스이므로 인스턴스를 생성해서 사용하는데에 제약은 없으므로 누군가 new Animal() 처럼 Animal 클래스의 인스턴스를 생성할 수도 있는 것이다. 하지만 이 인스턴스는 작동은 하지만 제대로 된 기능을 수행하지 않는다. -
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)
상속은 이름 그대로 부모의 기능을 물려 받는 것이 목적이다. 하지만 인터페이스는 모든 메서드가 추상 메서드이다. 따라서 물려받을 수 있는 기능이 없고, 오히려 인터페이스에 정의한 모든 메서드를 자식이 오버라이딩 해서 기능을 구현해야 한다. 따서서 구현한다고 표현한다. 표현은 다르지만 사실상 자바 입장에선 동일하다. 구현 역시 일반 상속 구조와 동일하게 작동한다.
📍 인터페이스를 사용해야 하는 이유?
- 제약 : 인터페이스를 구현하는 곳에서는 인터페이스의 메서드를 반드시 구현해야하 제약이 발생한다. 추상 클래스의 경우 실행 가능한 기능이 있는 메서드가 추가 될 수 도 있고, 이런 경우 추가 된 기능들을 자식 클래스에서 구현하지 않을 수도 있다. 하지만 인터페이스는 이러한 예외없이 반드시 모든 메서드를 구현해야하므로 혹시 모를 오류의 가능성을 원천 차단할 수 있다.
- 다중 구현 : 클래스이 상속은 부모를 하나만 지정할 수 있다. 반면 인터페이스는 부모를 여럿 지정하는 다중 상속(다중 구현)이 가능하다.
인터페이스의 다중구현
왜 다중 상속은 허용하지 않지만, 다중 구현은 허용되는 걸까?
다중 상속이 허용되지 않는 이유는 왼쪽 그림처럼 부모 클래스에 동일한 이름의 메서드가 구현되어 있는 경우, 각 부모 클래스를 상속받은 자식 클래스에서 해당 메서드를 호출 할 때 둘 중 어떤 메서드를 호출해야하는가에 대한 문제, 즉 다이아몬드 문제를 발생시키기 때문이다.
하지만 인터페이스의 경우 각 인터페이스에 선언한 메서드는 모두 추상클래스이다. 어차피 구현체는 자식 클래스에 있기 때문에 자식 클래스의 메서드를 사용하게 되므로, 부모의 어떤 메서드를 호출할 지 고민할 필요가 없는 것이다. 그렇기 때문에 자바에서는 다중 상속은 허용하지 않지만 다중 구현은 허용한다.
Leave a comment