우선, 글을 작성하기 전 이 글의 모든 내용은 김영한님의 JAVA 강의를 바탕으로 함을 알립니다.
앞선 포스팅 다형성1에서는 다형성의 의미와 핵심기능을 살펴보았다. 이번 포스팅에서는 다형성의 목적과 활용에 대해서 포스팅한다.
다형성의 핵심기능 두 가지 다시 짚고 넘어가보자.
다형성의 필요성을 설명하기 위해 다형성을 사용하지 않은 코드와 다형성을 사용한 코드를 비교해보자.
public class Dog {
public void sound(){
System.out.println("멍멍!");
}
}
public class Cat {
public void sound(){
System.out.println("냐옹~");
}
}
public class Cow {
public void sound(){
System.out.println("음메~~~");
}
}
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("동물 소리 테스트 종료");
}
}
위의 코드는 개,고양이,소의 울음소리를 출력문으로 구현하기 위해 Dog,Cat,Cow 클래스를 구현한 것이다. main메서드를 보면 클래스에 대한 객체를 생성하고 해당 클래스에 대한 sound 메서드를 순차적으로 호출하여 각 동물에 대한 울음소리를 구현한다.
근데 더 많은 종류의 동물들이 추가되면 어떻게 될까...?
즉, 새로운 클래스가 추가될 때마다 중복 코드가 계속해서 늘어난다. 중복 코드를 없애기 위해 두 가지 방법을 사용해보자.
private static void soundCaw(Cow cow) {
System.out.println("동물 소리 테스트 시작");
caw.sound();
System.out.println("동물 소리 테스트 종료");
}
Caw[] cawArr = {cat, dog, caw}; //컴파일 오류 발생!
System.out.println("동물 소리 테스트 시작");
for (Caw caw : cawArr) {
cawArr.sound();
}
첫 번째, 메서드를 생성하는 것은 해당 메서드만 호출하면 출력문과 sound()메서드를 호출하는 부분의 중복을 제거 할 수 있다. 하지만 매개변수로 특정 클래스 타입을 받아야 하기에 해당 메서드도 클래스의 개수 만큼 호출해야한다는 단점이 존재한다.
두 번째, for문과 배열을 사용한 부분은 배열을 생성하는 부분에서 배열의 타입에 의해 컴파일 에러가 발생한다.
두 방법 모두 선언 '타입'에 의해 단점이 존재하고 에러가 발생함을 알 수 있다. 이를 해결하기위해 '공통'의 타입이 필요하다. Animal 부모클래스를 만들고 Dog, Cat, Cow 클래스를 상속을 통해 자식 클래스화 해보면 다음과 같다.

public class Animal {
public void sound() {
System.out.println("동물 울음 소리");
}
}
public class Dog extends Animal {
@Override
public void sound(){
System.out.println("멍멍!");
}
}
public class Cat extends Animal {
@Override
public void sound(){
System.out.println("냐옹~");
}
}
public class Cow extends Animal {
@Override
public void sound(){
System.out.println("음메~~~");
}
}
public class AnimalPolyMain3 {
public static void main(String[] args) {
Animal[] animalArr = {new Dog(), new Cat(), new Caw()};
for (Animal animal : animalArr) {
soundAnimal(animal);
}
}
//동물이 추가 되어도 변하지 않는 코드
private static void soundAnimal(Animal animal) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
위 코드의 핵심은 다음과 같다
Animal[] animalArr = {new Dog(), new Cat(), new Caw()};
for (Animal animal : animalArr) {
soundAnimal(animal);
}
위 처럼 다형성을 고려한 코드는 새로운 '동물'을 추가하여도 soundAnimal() 메서드는 변경이 필요없다. 이 부분이 가능한 이유는 동물 마다 (Dog, Cat, Cow) 참조하는 것이 아니라 Animal이라는 부모 클래스만 참조하기 때문에 가능한 것이다.
위 코드는 다형성을 고려한 유지보수가 용이한 코드이다. 하지만 위 코드에는 두 가지의 문제점이 존재한다.
1. 부모 클래스 Animal 클래스의 인스턴스를 생성할 문제
기존의 코드의 문제점은 각 동물 객체를 추가할 때마다 메인 로직을 변경해야 한다는 점이고 이를 해결하기 위해 상속을 도입하여 다형적 참조를 진행하였다. 하지만, Animal 부모 클래스는 오직 다형적 참조을 위해 생성 된 것이지 직접 인스턴스를 생성하여 사용 할 이유도 없고 혼란을 야기한다.
Animal animal = new Animal();
2. Animal 클래스를 상속 받는 곳에서 sound() 메서드 오버라이딩을 하지 않을 가능성
Pig라는 새로운 자식 클래스를 추가했다고 가정하자. 근데 개발자가 실수로 오버라이딩 해야하는 메서드를 오버라이딩 하지 않았다면 어떻게 될까? 만약 부모 클래스의 특정 메서드를 오버라이딩하지 않는다면 자식 클래스가 아닌 부모 클래스의 메서드 기능이 호출과 동시에 실행 되어 목적에 맞지 않는 기능을 구현하게 된다. 하지만 진짜 문제점이 이 부분이다. 오류가 발생하지 않는다는 것이다. 당장 Pig 클래스만 보면 큰 문제가 없다고 생각할수도 있으나 코드가 더 복잡해지고 길어진다면 유지보수 측면에서 틀린부분을 고치는 것은 어려울 것이다. 제약을 걸어 줄 수 없을까?
다음 포스팅에서는 다형성의 단점 부분을 보완하는 개념인 추상 클래스와 인터페이스에 대해 포스팅 해보겠다. 끝!