[Java] 다형성 활용

szlee·2023년 12월 14일
0

Java

목록 보기
20/23

< 김영한의 실전 자바 - 기본편 > 강의를 보고 이해한 내용을 바탕으로 합니다.





다형성 활용

다형성을 사용하지 않았을 때

public class AminalSoundMain {
    public static void main(String[] args) {
        Dog dog = new Dog();
        Cat cat = new Cat();
        Cow cow = new Cow();

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

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

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

중복코드가 많다. 중복을 제거하기 위해서는 메서드를 사용하거나 배열과 for문을 사용하면 되는데 Dog, Cat, Cow는 서로 완전히 다른 클래스이다.

중복 제거 시도

메서드로 중복 제거 시도

 private static void soundCow(Cow cow){
        System.out.println("동물 소리 테스트 시작");
        cow.sound();
        System.out.println("동물 소리 테스트 종료");
    }

메서드를 사용하면 위와 같이 매개변수의 클래스를 Cow, Dog, Cat 중에 하나로 정해야한다.
따라서 이 메서드는 Cow 전용 메서드가 되고 Dog, Cat은 인수로 사용할 수 없다.
Dog, Cat, Cow의 타입(클래스)이 서로 다르기 때문에 soundCow메서드를 함께 사용하는 것은 불가능하다.

배열과 for문을 통한 중복 제거 시도

Cow[] cowArr = {dog, cat, cow}; //compile error
        for (Cow cow : cowArr) {
            cow.sound();
        }

배열과 for문을 사용해서 중복을 제거하려고 해도 배열의 타입을 Dog, Cat, Cow 중에 하나로 지정해야 한다. 같은 Cow들을 배열에 담아서 처리하는 것은 가능하지만 타입이 서로 다른 Dog, Cat, Cow를 하나의 배열에 담는 것은 불가능하다.

=> 지금 상황에서는 해결 방법이 없다.. 새로운 동물이 추가될 때마다 더 많은 중복 코드를 작성해야 한다.
지금까지의 모든 중복 제거 시도가 타입이 서로 다르기 때문에 불가능하다. 즉, 문제의 핵심은 바로 타입이 다르다는 점이다. Dog, Cat, Cow가 모두 같은 타입을 사용할 수 있는 방법이 있다면 메서드와 배열을 활용해서 코드의 중복을 제거할 수 있다는 것이다.

⭐️다형성의 핵심은 다형적 참조와 메서드 오버라이딩이다.⭐️ 이 둘을 활용하면 Dog, Cat, Cow가 모두 같은 타입을 사용하고 각자 자신의 메서드도 호출할 수 있다.



다형성을 활용하여 변경해보자.

다형성을 사용하기 위해서 여기서는 상속 관계를 사용한다. Animal이라는 부모 클래스를 만들고 sound()메서드를 정의한다. 이 메서드는 자식 클래스에서 오버라이딩할 목적으로 만들었다.
Dog, Cat, CowAnimal클래스를 상속받았다. 그리고 각각 부모의 sound() 메서드를 오버라이딩한다.

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 AnimalPoly {
    public static void main(String[] args) {
        Dog dog = new Dog();
        Cat cat = new Cat();
        Cow cow = new Cow();

        soundAnimal(dog);
        soundAnimal(cat);
        soundAnimal(cow);
    }

    private static void soundAnimal(Animal animal){
        System.out.println("동물 소리 테스트 시작");
        animal.sound();
        System.out.println("동물 소리 테스트 종료");
    }
}
  • soundAnimal(dog)을 호출하면 Animal animalDog 인스턴스가 전달된다. => 부모는 자식을 담을 수 있다.
  • 메서드 안에서 animal.sound()메서드를 호출한다.
  • animal변수의 타입은 Animal이므로 Dog인스턴스에 있는 Animal클래스 부분을 찾아서 sound()메서드를 실행한다. 그런데 하위 클래스인 Dog에서 sound()메서드를 오버라이딩 했다. 따라서 오버라이딩한 메서드가 우선권을 가진다.
  • Dog 클래스에 있는 sound()메서드가 호출된다.

이 코드의 핵심은 Animal animal 부분이다.

  • 다형적 참조 덕분에 animal변수는 자식인 Dog, Cat, Cow의 인스턴스를 참조할 수 있다.(부모는 자식을 담을 수 있다.)
  • 메서드 오버라이딩 덕분에 animal.sound()를 호출해도 Dog.sound(), Cat.sound(), Cow.sound()와 같이 각 인스턴스의 메서드를 호출할 수 있다.
    만약 자바에 메서드 오버라이딩이 없었다면 모두 Animalsound()가 호출되었을 것이다.

다형성 덕분에 이후에 새로운 동물을 추가해도 다음 코드를 그대로 재사용할 수 있다. 물론 다형성을 사용하기 위해 새로운 동물은 Animal을 상속 받아야 한다.

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


이번에는 배열과 for문을 사용해서 중복을 제거해보자.

public class AnimalPoly2 {
    public static void main(String[] args) {
        Dog dog = new Dog();
        Cat cat = new Cat();
        Cow cow = new Cow();

        Animal[] animals = {dog, cat, cow};

        //변하지 않는 부분
        for (Animal animal : animals) {
            System.out.println("동물 소리 테스트 시작");
            animal.sound();
            System.out.println("동물 소리 테스트 종료");

        }
    }
}

배열은 같은 타입의 데이터를 나열할 수 있다.
Dog, Cat, Cow는 모두 Animal의 자식이므로 Animal타입이다.

Animal[] animals = {dog, cat, cow};

따라서 Animal타입의 배열을 만들고 다형적 참조를 사용하면 된다.

이제 배열을 for문을 사용해서 반복하면 된다.

//변하지 않는 부분
        for (Animal animal : animals) {
            System.out.println("동물 소리 테스트 시작");
            animal.sound();
            System.out.println("동물 소리 테스트 종료");

        }

animal.sound()를 호출하지만 배열에는 Dog, Cat, Cow의 인스턴스가 들어있다. 메서드 오버라이딩에 의해 각 인스턴스의 오버라이딩 된 sound() 메서드가 호출된다.



조금 더 개선해보자!

public class AnimalPoly2 {
    public static void main(String[] args) {

        Animal[] animals = {new Dog(), new Cat(), new Cow()};

        //변하지 않는 부분
        for (Animal animal : animals) {
            animalSound(animal);
        }
    }

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

새로운 동물이 추가되어도 animalSound(..) 메서드는 코드 변경 없이 유지할 수 있다. 이렇게 할 수 있는 이유는 이 메서드는 Dog, Cat, Cow 같은 구체적인 클래스를 참조하는 것이 아니라 Animal이라는 추상적인 부모를 참조하기 때문이다.
따라서 Animal을 상속 받은 새로운 동물이 추가되어도 이 메서드의 코드는 변경 없이 유지할 수 있다.

위의 코드에서 변하는 부분은 main(), 변하지 않는 부분은 animalSound(..)이다. 새로운 기능이 추가되었을 때 변하는 부분을 최소화하는 것이 잘 작성된 코드이다. 이렇게 하기 위해서는 코드에서 변하는 부분과 변하지 않는 부분을 명확하게 구분하는 것이 좋다.

남은문제

지금까지의 코드에는 사실 두가지 문제가 있다.

  • Animal 클래스를 생성할 수 있는 문제
  • Animal 클래스를 상속받는 곳에서 sound()메서드를 오버라이딩을 하지 않을 가능성

1. Animal 클래스를 생성할 수 있는 문제
Animal 클래스는 동물이라는 추상적인 클래스이다. 이 클래스를 다음과 같이 직접 생성해서 사용할 일이 있을까?

Animal a = new Animal();

개, 고양이, 소가 실제 존재하는 것은 당연하지만 동물이라는 추상적인 개념이 실제로 존재하는 것은 이상하다. 이 클래스는 다형성을 위해서 필요한 것이지 직접 인스턴스를 생성해서 사용할 일은 없다.
하지만 Animal도 클래스이기 때문에 인스턴스를 생성하고 사용하는데 아무런 제약이 없다. 누군가 실수로 new Animal()을 사용해서 Animal의 인스턴스를 생성할 수 있다는 것이다. 이렇게 생성된 인스턴스는 작동은 하지만 제대로된 기능을 수행하지는 않는다.

2. Animal 클래스를 상속받는 곳에서 sound()메서드를 오버라이딩을 하지 않을 가능성
예를 들어 Animal을 상속 받은 Pig클래스를 만든다고 가정해보자. 우리가 기대하는 것은 Pig클래스가 sound() 메서드를 오버라이딩해서 "꿀꿀"소리가 나도록 하는 것이다. 그런데 개발자가 실수로 sound()메서드를 오버라이딩 하는 것을 빠뜨릴 수 있다. 이렇게 되면 부모의 기능을 상속 받는다. => 코드상 아무런 문제가 발생하지 않는다. 물론 프로그램을 실행하면 기대와 다르게 부모 클래스에 있는 Animal.sound()가 호출될 것이다.

좋은 프로그램은 제약이 있는 프로그램이다. 추상 클래스와 추상 메서드를 사용하면 이러한 문제를 한번에 해결할 수 있다.

profile
🌱

0개의 댓글