개, 고양이, 소의 울음소리를 테스트하는 프로그램을 만들어봅시다.
먼저 다형성을 사용하지 경우 입니다.
public class Dog {
public void sound() {
System.out.println("멍멍");
}
}
public class Cat {
public void sound() {
System.out.println("냐옹");
}
}
public class Caw {
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("동물 소리 테스트 종료");
}
}
단순히 개, 고양이, 소의 울음소리를 출력하는 프로그램 입니다. 만약 여기서 새로운 동물이 추가되면 어떻게 될까요?
소가 추가된다고 하면 Caw 클래스를 만들고 Caw를 사용해야 하는 코드를 작성해야 할 것입니다.
Caw를 생성하는 부분은 당연히 필요하니 크게 상관이 없지만, Dog, Cat, 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("동물 소리 테스트 종료");
중복을 제거하기 위해서는 메서드를 사용하거나, 또는 배열과 for문을 사용하면 됩니다.
그런데 Dog, Cat, Caw는 서로 완전히 다른 클래스 입니다.
중복 제거 시도
메서드로 중복 제거 시도
메서드를 사용하면 다음과 같이 매개변수의 클래스를 Caw, Cat, Dog 중에 하나로 정해야 합니다.
private static void soundCaw(Caw caw) {
System.out.println("동물 소리 테스트 시작");
caw.sound();
System.out.println("동물 소리 테스트 종료");
}
따라서 이 메서드는 Caw 전용 메서드가 되고 Dog, Cat은 인수로 사용할 수 없습니다.
Dog, Cat, Caw의 타입(클래스)가 서로 다르기 때문에 soundCaw 메서드를 함께 사용하는 것을 불가능합니다.
배열과 for문을 통한 중복 제거 시도
Caw[] cawArr = {cat, dog, caw}; // 컴파일 오류 발생
System.out.println("동물 소리 테스트 시작");
for (Caw caw : cawArr) {
cawArr.sound();
}
System.out.println("동물 소리 테스트 종료");
배열과 for문을 사용해서 중복을 제거하려고 해도 배열의 타입을 Dog, Cat, Caw 중에 하나로 지정해야 합니다.
같은 Caw들을 배열에 담아서 처리하는 것은 가능하지만 타입이 서로 다른 Dog, Cat, Caw을 하나의 배열에 담는 것은
불가능합니다. 결과적으로 지금 상황에서는 해결 방법이 없습니다. 새로운 동물이 추가될 때 마다 더 많은 중복 코드를
작성해야 합니다.
지금까지 설명한 중복 제거 시도가 Dog, Cat, Caw의 타입이 서로 다르기 때문에 불가능합니다. 문제의 핵심은 바로 타입이
다르다는 점입니다. 반대로 이야기하면 Dog, Cat, Caw가 모두 같은 타입을 사용할 수 있는 방법이 있다면 메서드와 배열을 활용해서 코드의 중복을 제거할 수 있다는 것입니다.
다형성의 핵심은 다형적 참조와 메서드 오버라이딩 입니다.
이 둘을 활용한다면 Dog, Cat, Caw가 모두 같은 타입을 사용하고, 각자 자신의 메서드도 호출할 수 있습니다.
다형성을 사용하기 위해 여기서는 상속 관계를 사용합니다.
Animal(동물) 이라는 부모 클래스를 만들고 sound( ) 메서드를 정의합니다.
이 메서드는 자식 클래스에서 오버라이딩 할 목적으로 만들었습니다.
Dog, Cat, Caw는 Animal 클래스를 상속받았습니다.
그리고 각각 부모의 sound( ) 메서드를 오버라이딩 합니다.
public class Animal{
public void sound() {
System.out.println("동물 울음 소리");
}
}
public class Dog extends Animal{
@Override
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();
/*Animal dog = new Dog();
Animal cat = new Cat();
Animal caw = new Caw();*/
soundAnimal(dog);
soundAnimal(cat);
soundAnimal(caw);
}
private static void soundAnimal(Animal animal) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
}
실행 결과는 기존 코드와 같습니다.
- soundAnimal(dog) 을 호출한다.
- soundAnimal(Animal animal)에 Dog 인스턴스가 전달된다.
- 메서드 안에서 animal, sound( ) 메서드를 호출한다.
Animal animal = dog 로 이해하면 쉽습니다.
부모는 자식을 담을 수 있기 때문에 가능합니다.
- animal 변수의 타입은 Animal 이므로 Dog 인스턴스에 있는 Animal 클래스 부분을 찾아서 sound( ) 메서드를 실행한다. 그런데 하위 클래스인 Dog 에서 sound( ) 메서드를 오버라이딩 했다. 따라서 오버라이딩한 메서드가 우선권을 가진다.
- Dog 클래스에 있는 sound( ) 메서드가 호출되므로 "멍멍" 이 출력된다.
이 코드의 핵심은 Animal animal 부분입니다.
- 다형적 참조 덕분에 animal 변수는 자식인 Dog, Cat, Caw의 인스턴스를 참조할 수 있다.
- 메서드 오버라이딩 덕분에 animal.sound( )를 호출해도 Dog.sound( ), Cat.sound( ), Caw.sound( )와 같이 각 인스턴스의 메서드를 호출할 수 있다. 만약 자바의 오버라이딩이 없었다면 모두 Animal의 sound( )가 호출되었을 것이다.
다형성 덕분에 이후에 새로운 동물을 추가해도 다음 코드를 그대로 재사용할 수 있습니다.
물론 다형성을 사용하기 위해 새로운 동물은 Animal을 상속 받아야 합니다.
이번에는 배열과 for문을 사용해서 중복을 제거해보겠습니다.
public class AnimalSoundMain2 {
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
Caw caw = new Caw();
Animal[] animals = {dog, cat, caw};
// 변하지 않는 부분
for (Animal animal : animals) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
}
}
배열은 같은 타입의 데이터를 나열할 수 있습니다.
Dog, Cat, Caw 모두 Animal 의 자식의므로 Animal 타입입니다.
Animal 타입의 배열을 만들고 다형적 참조를 사용하면 됩니다.
Animal[] animals = new Animal[]{dog, cat, cow};
Animal[] animals = {dog, cat, caw};
다형적 참조 덕분에 Dog, Cat, Caw의 부모 타입인 Animal 타입으로 배열을 만들고, 각각의 배열에 포함했습니다.
이제 배열을 for문을 통해 반복하면 끝입니다.
for (Animal animal : animals) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
animal.sound( ) 를 호출하지만 배열에는 Dog, Cat, Caw 의 인스턴스가 들어있습니다.
메서드 오버라이딩에 의해 각 인스턴스의 오버라이딩 된 sound( ) 메서드가 호출됩니다.
이번에는 배열과 메서드 모두를 활용해서 기존 코드를 완성해보겠습니다.
public class AnimalSoundMain3 {
public static void main(String[] args) {
Animal[] animals = {new Dog(), new Cat(), new Caw()};
for (Animal animal : animals) {
soundAnimal(animal);
}
}
private static void soundAnimal(Animal animal) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
}
- Animal[ ] animals 를 통해서 배열을 사용한다.
- soundAnimal(Animal animal) 하나의 동물을 받아서 로직을 처리한다.
새로운 동물이 추가되어도 soundAnimal( ) 메서드는 코드 변경 없이 유지할 수 있습니다.
이렇게 할 수 있는 이유는 이 메서드는 Dog, Cat, Caw 같은 구체적인 클래스를 참조하는 것이 아니라 Animal 이라는
추상적인 부모를 참조하기 때문입니다. 따라서 Animal을 상속 받은 새로운 동물이 추가되어도 이 메서드의 코드는
변경 없이 유지할 수 있습니다.
새로운 기능이 추가되었을 때 변하는 부분을 최소화 하는 것이 잘 작성된 코드입니다.
이렇게 하기 위해서는 코드에서 변하는 부분과 변하지 않는 부분을 명확하게 구분하는 것이 좋습니다.
남은 문제
- Animal 클래스를 생성할 수 있는 문제
- Animal 클래스를 상속 받는 곳에서 sound( ) 메서드 오버라이딩을 하지 않을 가능성
Animal 클래스를 생성할 수 있는 문제
Animal 클래스는 동물이라는 클래스입니다. 이 클래스를 다음과 같이 직접 생성해서 사용할 일이 있을까요?
Animal animal = new Animal();
개, 고양이, 소가 실제 존재하는 것은 당연하지만, 동물이라는 추상적인 개념이 실제로 존재하는 것은 이상합니다.
사실 이 클래스는 다형성을 위해서 필요한 것이지, 직접 인스턴스를 생성해서 사용할 일은 없습니다.
하지만 Animal도 클래스이기 때문에 인스턴스를 생성하고 사용하는데 아무런 제약이 없습니다.
누군가 실수로 new Animal( )을 사용해서 Aniaml의 인스턴스를 생성할 수 있다는 뜻입니다.
이렇게 생성된 인스턴스는 작동은 하지만 제대로된 기능을 수행하지 않습니다.
Animal 클래스를 상속 받는 곳에서 sound( ) 메서드 오버라이딩을 하지 않을 가능성
예를 들어서 Animal을 상속 받은 Pig 클래스를 만든다고 가정해봅시다. 저희가 기대하는 것은 Pig 클래스가
sound( ) 메서드를 오버라이딩 해서 "꿀꿀" 이라는 소리가 나도록 하는 것 입니다. 그런데 개발자가 실수로
sound( ) 메서드를 오버라이딩 하는 것을 빠트릴 수 있습니다. 이렇게 되면 부모의 기능을 상속 받습니다.
따라서 코드상 아무런 문제가 발생하지 않고 부모 클래스에 있는 Animal.sound( ) 가 호출될 것 입니다.
이런 문제들을 추상 클래스와 추상 메서드를 사용해서 한번에 해결할 수 있습니다.
다음에는 추상 클래스와 추상 메서드에 대해 다뤄보겠습니다.
'Java' 카테고리의 다른 글
[Java] 다형성이 중요한 이유? (0) | 2024.03.26 |
---|---|
[Java] 인터페이스 (0) | 2024.03.21 |
[Java] 다형적 참조(업캐스팅 vs 다운캐스팅) (0) | 2024.03.18 |
[Java] 상속이 왜 필요할까? (feat.오버라이딩) (0) | 2024.03.14 |
[Java] static final (0) | 2024.03.13 |