지금까지 학습한 다형성을 왜 사용하는지, 그 장점을 알아보기 위해 우선 다형성을 사용하지 않고 프로그램을 개발한 다음에 다형성을 사용하도록 코드를 변경해보자.
아주 단순하고 전통적인 동물 소리 문제로 접근해보자.
개, 고양이, 소의 울음 소리를 테스트하는 프로그램을 작성해보자. 먼저 다형성을 사용하지 않고 코드를 작성해보자.
package poly.ex1;
public class Dog {
public void sound() {
System.out.println("멍멍");
}
}
package poly.ex1;
public class Cat {
public void sound() {
System.out.println("야옹");
}
}
package poly.ex1;
public class Caw {
public void sound() {
System.out.println("음메");
}
}
package poly.ex1;
public class AnimalSoundMain {
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
Caw caw = new Caw();
dog.sound();
cat.sound();
caw.sound();
}
}
멍멍
야옹
음메
단순히 개, 고양이, 소 동물들의 울음 소리를 출력하는 프로그램이다. 만약 여기서 새로운 동물이 추가되면 어떻게 될까?
만약 기존 코드에 소가 없었다고 가정해보자, 소가 추가된다고 가정하면 Caw 클래스를 만들고 다음 코드도 추가해야한다.
Caw 를 생성하는 부분은 당연히 필요하니 크게 상관이 없지만 Dog, Cat, Caw 를 사용해서 출력하는 부분은 계속 중복이 증가한다.
dog.sound();
cat.sound();
caw.sound();
이 부분의 중복을 제거할 수 있을까?
중복을 제거하기 위해서는 메서드를 사용하거나, 또는 배열과 for 문을 사용하면 된다고 생각할 것입니다.
그런데 Dog, Cat, Caw 는 서로 완전히 다른 클래스다.
중복 제거 시도가 Dog, Cat, Caw 의 타입이 서로 다르기 때문에 불가능하다. 문제의 핵심은 바로 타입이 다르다는 점이다.
반대로 이야기하면 Dog, Cat, Caw 가 모두 같은 타입을 사용할 수 있는 방법이 있다면 메서드와 배열을 활용해서 코드의 중복을 제거할 수 있다는 것이다.
다형성의 핵심은 다형적 참조와 메서드 오버라이딩이다. 이 둘을 활용하면 Dog, Cat, Caw 가 모두 같은 타입을 사용하고, 각자 자신의 메서드도 호출할 수 있다.
다형성을 사용하기 위해 여기서는 상속 관계를 사용한다. Animal(동물) 이라는 부모 클래스를 만들고 sound() 메서드를 정의한다. 이 메서드는 자식 클래스에서 오버라이딩 할 목적으로 만들었다.
Dog, Cat, Caw 는 Animal 클래스를 상속 받았다. 그리고 각각 부모의 sound() 메서드를 오버라이딩 한다.
package poly.ex2;
public class Animal {
public void sound() {
System.out.println("동물소리");
}
}
package poly.ex2;
public class Dog extends Animal {
@Override
public void sound() {
System.out.println("멍멍");
}
}
package poly.ex2;
public class Cat extends Animal{
@Override
public void sound() {
System.out.println("야옹");
}
}
package poly.ex2;
public class Caw extends Animal {
@Override
public void sound() {
System.out.println("음메");
}
}
package poly.ex2;
public class AnimalPolyMain1 {
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
Caw caw = new Caw();
soundAnimal(dog);
soundAnimal(cat);
soundAnimal(caw);
}
private static void soundAnimal(Animal animal) {
animal.sound();
}
}
멍멍
야옹
음메
이 코드의 핵심은 Animal aniaml 부분이다.
다형적 참조 덕분에 animal 변수는 자식인 Dog, Cat, Caw의 인스턴스를 참조할 수 있다. (부모는 자식을 담을 수 있다.)
메서드 오버라이딩 덕분에 animal.sound() 를 호출해도 Dog.sound(), Cat.sound(), Caw.sound() 와 같이 각 인스턴스의 메서드를 호출할 수 있다.
만약 자바에 메서드 오버라이딩이 없었다면 모두 Animal 의 sound() 가 호출되었을 것이다.
다형성 덕분에 이후에 새로운 동물을 추가해도 다음 코드를 그대로 재사용 할 수 있다. 물론 다형성을 사용하기 위해 새로운 동물은 Animal 을 상속 받아야 한다.
package poly.ex2;
public class Duck extends Animal {
@Override
public void sound() {
System.out.println("꽥꽥");
}
}
package poly.ex2;
public class AnimalPolyMain1 {
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) {
animal.sound();
}
}
멍멍
야옹
음메
꽥꽥
이번에는 배열과 for문을 사용해서 중복을 제거해보자.
package poly.ex2;
public class AnimalPolyMain2 {
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
Caw caw = new Caw();
Duck duck = new Duck();
Animal[] animals = {dog, cat, caw, duck};
for (Animal animal : animals) {
animal.sound();
}
}
}
멍멍
야옹
음메
꽥꽥
배열은 같은 타입의 데이터를 나열할 수 있다.
Dog, Cat, Caw, Duck 은 모두 Animal 의 자식이므로 Animal 타입니다.
지금까지 설명한 코드에는 사실 2가지 문제가 있다.
Animal 클래스를 생성할 수 있는 문제
Animal 클래스는 동물이라는 클래스이다. 이 클래스를 다음과 같이 직접 생성해서 사용할 일이 있을까?
Animal animal = new Animal();
개, 고양이, 소가 실제 존재하는 것은 당연하지만, 동물이라는 추상적인 개념이 실제로 존재하는 것은 이상하다. 사실 이 클래스는 다형성을 위해서 필요한 것이지 직접 인스턴스를 생성해서 사용할 일은 없다.
하지만 Animal 도 클래스이기 때문에 인스턴스를 생성하고 사용하는데 아무런 제약이 없다. 누군가 실수로 new Animal() 을 사용해서 Animal 의 인스턴스를 생성할 수 있다는 것이다. 이렇게 생성된 인스턴스는 작동은 하지만 제대로된 기능을 수행하지는 않는다.
Animal 클래스를 상속 받는 곳에서 sound() 메서드 오버라이딩을 하지 않을 가능성
예를 들어서 Aniaml을 상속 받는 Pig 클래스를 만든다고 가정해보자. 우리가 기대하는 것은 Pig 클래스가 sound() 메서드를 오버라이딩 해서 "꿀꿀" 이라는 소리가 나도록 하는 것이다. 그런데 개발자가 실수로 sound() 메서드를 오버라이딩 하는 것을 빠트릴 수 있다. 이렇게 되면 부모의 기능을 상속 받는다. 따라서 코드상 아무런 문제가 발생하지 않는다. 물론 프로그램을 실행하면 기대와 다르게 "꿀꿀"이 아니라 부모 클래스에 있는 Animal.sound() 가 호출될 것이다.
좋은 프로그램은 제약이 있는 프로그램이다. 추상 클래스와 추상 메서드를 사용하면 이런 문제를 한번에 해결할 수 있다.