Java 공부 28일차(다형성이란?)2편

임선구·2025년 2월 17일

몸 비틀며 Java

목록 보기
29/58

오늘의 잔디


오늘의 공부 헬스 갔다 와서


다형성 활용1

지금까지 학습한 다형성을 왜 사용하는지, 그 장점을 알아보기 위해 우선 다형성을 사용하지 않고 프로그램을 개발한 다음에 다형성을 사용하도록 코드를 변경해보자.
아주 단순하고 전통적인 동물 소리 문제로 접근해보자.

개, 고양이, 소의 울음 소리를 테스트하는 프로그램을 작성해보자. 먼저 다형성을 사용하지 않고 코드를 작성해보자.

예제1

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();
 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 caw = new Caw();
//Caw를 사용하는 코드
System.out.println("동물 소리 테스트 시작");
caw.sound();
System.out.println("동물 소리 테스트 종료");

Caw 를 생성하는 부분은 당연히 필요하니 크게 상관이 없지만, Dog , Cat , Caw 를 사용해서 출력하는 부분은 계속 중복이 증가한다.

중복 코드

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

이 부분의 중복을 제거할 수 있을까?

중복을 제거하기 위해서는 메서드를 사용하거나, 또는 배열과 for 문을 사용하면 된다.
그런데 Dog , Cat , Caw 는 서로 완전히 다른 클래스다.

중복 제거 시도

메서드로 중복 제거 시도

메서드를 사용하면 다음과 같이 매개변수의 클래스를 Caw , Dog , Cat 중에 하나로 정해야 한다.

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 가 모두 같은 타입을 사용하고, 각자 자신의 메서드도 호출할 수 있다.

다형성 활용2

이번에는 앞서 설명한 예제를 다형성을 사용하도록 변경해보자.

예제2

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

기존 코드를 유지하기 위해 새로운 패키지를 만들고 새로 코드를 작성하자.
주의! 패키지 이름에 주의하자 import 를 사용해서 다른 패키지에 있는 같은 이름의 클래스를 사용하면 안된다.

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) {
 System.out.println("동물 소리 테스트 시작");
 animal.sound();
 System.out.println("동물 소리 테스트 종료");
 }
}

실행 결과

동물 소리 테스트 시작
멍멍
동물 소리 테스트 종료
동물 소리 테스트 시작
냐옹
동물 소리 테스트 종료
동물 소리 테스트 시작
음매
동물 소리 테스트 종료

실행 결과는 기존 코드와 같다.

코드를 분석해보자.

  • soundAnimal(dog) 을 호출하면
  • soundAnimal(Animal animal)Dog 인스턴스가 전달된다.
    • Animal animal = dog 로 이해하면 된다. 부모는 자식을 담을 수 있다. AnimalDog 의 부모다.
  • 메서드 안에서 animal.sound() 메서드를 호출한다.
  • animal 변수의 타입은 Animal 이므로 Dog 인스턴스에 있는 Animal 클래스 부분을 찾아서 sound() 메서드 호출을 시도한다. 그런데 하위 클래스인 Dog 에서 sound() 메서드를 오버라이딩 했다. 따라서 오버라이딩한 메서드가 우선권을 가진다.
  • Dog 클래스에 있는 sound() 메서드가 호출되므로 "멍멍"이 출력된다.
  • 참고로 이때 Animalsound() 는 실행되지 않는다. (오버라이딩한 메서드가 우선권을 가지므로 Dogsound() 가 실행된다.)

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

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

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

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

다형성 활용3

이번에는 배열과 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();
 Animal[] animalArr = {dog, cat, caw};
 //변하지 않는 부분
 for (Animal animal : animalArr) {
 System.out.println("동물 소리 테스트 시작");
 animal.sound();
 System.out.println("동물 소리 테스트 종료");
 }
 }
}

실행 결과

동물 소리 테스트 시작
멍멍
동물 소리 테스트 종료
동물 소리 테스트 시작
냐옹
동물 소리 테스트 종료
동물 소리 테스트 시작
음매
동물 소리 테스트 종료

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

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

//둘은 같은 코드이다.
Animal[] animalArr = new Animal[]{dog, cat, caw};
Animal[] animalArr = {dog, cat, caw}

다형적 참조 덕분에 Dog , Cat , Caw 의 부모 타입인 Animal 타입으로 배열을 만들고, 각각을 배열에 포함했다.

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

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

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

조금 더 개선

이번에는 배열과 메서드 모두 활용해서 기존 코드를 완성해보자.

package poly.ex2;
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 를 통해서 배열을 사용한다.
  • soundAnimal(Animal animal)
    • 하나의 동물을 받아서 로직을 처리한다.

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

여기서 잘 보면 새로운 동물이 추가되었을 때 코드가 변하는 부분과 변하지 않는 부분이 있다.
main() 은 코드가 변하는 부분이다. 새로운 동물을 생성하고 필요한 메서드를 호출한다.
soundAnimal(..) 는 코드가 변하지 않는 부분이다.

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

남은 문제

지금까지 설명한 코드에는 사실 2가지 문제가 있다.

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

Animal 클래스를 생성할 수 있는 문제

Animal 클래스는 동물이라는 클래스이다. 이 클래스를 다음과 같이 직접 생성해서 사용할 일이 있을까?

Animal animal = new Animal();

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

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

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

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

profile
끝까지 가면 내가 다 이겨

0개의 댓글