본문 바로가기
KOSTA/복습

[JAVA] 다형성

by 호올랑 2025. 2. 21.

 

다형성을 배웠습니다. 하지만 개념이 잘 와닿지가 않습니다. 이번 글에서는 다형성이 왜 필요하고 어떻게 활용되는지 정리해보려고 합니다.

1️⃣ 다형성 개념

  • 다형성이란?
    • 부모 타입 하나로 여러 자식 객체를 다룰 수 있는 것
    • 코드의 유연성과 확장성을 높일 수 있음
  • 다형성이 필요한 이유
    •  코드 중복을 줄이고 재사용성을 높일 수 있음
    • 유지보수가 쉬워지고 새로운 기능 추가가 간편해짐
    • 매개변수와 리턴 타입의 유연성이 증가해 확장성이 좋아짐
    •  
  • 업캐스팅 vs 다운캐스팅
    • 업캐스팅: Parent p = new Child(); → 부모 타입으로 자식 객체를 참조
    • 다운캐스팅: Child c = (Child) p; → 다시 자식 타입으로 변환

 

2️⃣ 다형성의 핵심: 오버라이딩

  • 오버라이딩이 없다면 다형성이 의미가 없는 이유
    • 자식 클래스에서 오버라이딩하지 않으면, 부모 클래스의 메서드가 그대로 실행됨
    • 업캐스팅을 했더라도 오버라이딩된 메서드만 자식 클래스의 것을 실행함

 

사실 개념만 보아서는 다형성의 필요성과 활용도를 잘 모르겠습니다. 업캐스팅과 다운캐스팅을 사용하여 활용한다고 하는데 업캐스팅과 다운캐스팅의 개념은 이해하지만 이것들과 다형성을 연결하는 것이 어려웠습니다. 하지만 코드를 보다 보니 조금은 알 것 같기도..?

 

먼저, 오버라이딩을 한 경우의 다형성 활용 코드를 한번 봅시다.

class Animal {
    void makeSound() {
        System.out.println("동물이 소리를 냅니다.");
    }
}

class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("멍멍!");
    }
}

class Cat extends Animal {
    @Override
    void makeSound() {
        System.out.println("야옹!");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal myAnimal = new Dog();
        myAnimal.makeSound();
    }
}

 

출력결과:

 

그렇다면 왜 이렇게 나올까요?

Animal myAnimal = new Dog(); → 업캐스팅하여 부모 타입 Animal로 Dog 객체를 참조해 옵니다. 그러면 오버라이딩된 Dog의 makeSound()가 실행되는 것입니다.

 

부모 타입 변수를 사용해도 실제 객체의 메서드가 실행되는 것이 다형성의 핵심 원리입니다.

 

그렇다면 다음의 예제는 어떤 결과가 나올까요

class Animal {
    void makeSound() {
        System.out.println("동물이 소리를 냅니다.");
    }
}

class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("멍멍!");
    }
}

class Person {
    void speak(Animal animal) {
        animal.makeSound();
    }
}

public class Main {
    public static void main(String[] args) {
        Person person = new Person();
        person.speak(new Dog());  
    }
}

 

헷갈려 오기 시작합니다.

 

출력결과:

 

또다시 이렇게 나옵니다.  왜 그럴까요?

speak() 메서드는 Animal 타입의 매개변수를 받습니다. 그러면 person.speak(new Dog()); 를 실행했을 때
Animal animal = new Dog(); 형태가 되는 것입니다. 그래서 animal.makeSound();를 실행하면 Dog 클래서에서 오버라이딩 된 makeSound()가 실행되는 것이죠.

 

정리해 보면 

업캐스팅(Animal → Dog) 상태여도, 오버라이딩된 메서드가 실행된다
부모 타입 변수를 통해 자식 클래스의 메서드를 다룰 수 있다 (다형성 활용)

 

오버라이딩 된 메서드가 존재할 때에서는 그다지 혼란스럽지 않았습니다. 하지만 오버라이딩 된 함수가 없다면 어떻게 될까요? 

 

지금부터는 오버라이딩 된 함수가 없을 때의 예제입니다.

class Animal {
    void makeSound() {
        System.out.println("동물이 소리를 냅니다.");
    }
}

class Dog extends Animal {
    
}

public class Main {
    public static void main(String[] args) {
        Animal myAnimal = new Dog();  
        myAnimal.makeSound(); 
    }
}

 

makeSound()에서 메서드를 오버라이딩 하지 않은 상태입니다. 이 코드를 실행하면

Animal 클래스의 makeSound()를 호출합니다. Dog에 아무런 메서드도 없으니 뭔가 그럴듯합니다. 하지만

다음의 코드는 저를 혼란스럽게 만들었죠

 

class Animal {
    void makeSound() {
        System.out.println("동물이 소리를 냅니다.");
    }
}

class Dog extends Animal {
    String doggyname;
}

public class Main {
    public static void main(String[] args) {
        Animal myAnimal = new Dog();  
        myAnimal.makeSound(); 
        myAnimal.doggyname="바둑이";
    }
}

 

실행해 볼 필요도 없이

오류가 납니다. myAnimal의 타입은 Animal로 부모입니다 하지만 Animal 클래스에는 변수 doggyname 변수가 정의되어있지 않죠, 자식클래스 Dog에는 doggyname  변수가 있지만 부모타입으로 선언된 변수 myAnimal은 Dog의 멤버 변수에 접근할 수 없습니다. 여기서부터 인지 부조화가 오는 것이죠. 물론 해결 방법은 있습니다.

 

다운 캐스팅을 하거나 부모클래스에 변수 doggyname을 추가하는 것입니다.

Animal myAnimal = new Dog();
Dog d = (Dog) myAnimal;  // 다운캐스팅
d.doggyname = "치키차카초코";  // 정상 작동

 

여기서 의문이 생깁니다. Dog d를 새로 생성하는 거라면 굳이 처음부터 Animal myAnimal을 사용하는 걸까요..? 그냥 처음부터 Dog d를 사용하면 안 되는 건지... 왜 업캐스팅을 했다가 다시 다운캐스팅을 하는 건지 궁금해졌습니다. 

하지만 이유가 있으니 만들어 뒀겠죠. 찾아봤습니다. 이 글을 작성하게 된 계기입니다. 

 

3️⃣ 다운캐스팅이 필요한 경우

1. 여러 자식 클래스를 한 배열에 저장할 때

class Parent {
    void show() {
        System.out.println("부모 클래스");
    }
}

class Child1 extends Parent {
    void hello() {
        System.out.println("Child1 클래스");
    }
}

class Child2 extends Parent {
    void hello() {
        System.out.println("Child2 클래스");
    }
}

public class Main {
    public static void main(String[] args) {
        Parent[] arr = { new Child1(), new Child2() };  // Parent 타입 배열

        for (Parent p : arr) {
            if (p instanceof Child1) {
                ((Child1) p).hello();  // 다운캐스팅 후 자식 클래스 메서드 사용
            } else if (p instanceof Child2) {
                ((Child2) p).hello();
            }
        }
    }
}

 

한 번에 많은 양의 데이터를 담을 일이 존재할 때, Parent[] 배열을 쓰면 Child1과 Child2를 하나의 배열에 저장할 수 있습니다. 하지만 hello()는 Parent에 없는 메서드입니다. 이럴 때 hello()에 접근하기 위해 다운캐스팅을 해서 호출합니다.

 

2. 메서드의 매개변수로 다형성을 활용할 때

class Parent { }

class Child extends Parent {
    void play() {
        System.out.println("Child가 놉니다");
    }
}

public class Main {
    public static void callPlay(Parent p) {
        if (p instanceof Child) {
            ((Child) p).play();  // 다운캐스팅 후 사용
        }
    }

    public static void main(String[] args) {
        Parent p = new Child();  // 업캐스팅
        callPlay(p);  // "Child가 놉니다" 출력
    }
}

 

이럴 땐 왜 존재할까요? 클래스의 상속 특성상 언젠가 Child2, Child3 같은 클래스가 추가될 수도 있을 것입니다. 서비스를 만들다 보면 한 두 개가 아니라 무수히 많은 클래스가 생길 수도 있을 것입니다. 그럴 때 다형성을 이용한다면, 부모 타입 하나만 받아도 여러 자식클래스를 다룰 수 있는 장점이 있습니다.

 

📌 

그냥 Child c= new Child() 하면 안 될까 하는 의문이 들 수 있습니다. 웬만한 경우에서는 그것이 더 나을 수도 있겠습니다. 하지만, 다형성을 활용하면 여러 자식 클래스를 하나의 부모 타입으로 관리할 수 있어서 유지보수가 훨씬 쉬워집니다. 새로운 클래스를 추가해도 코드 수정 없이 처리할 수 있는 유연함이 생깁니다.

 

4️⃣ 정리 

✅ 다형성은 부모 타입 하나로 여러 자식 객체를 다룰 수 있게 해 준다.

✅ 오버라이딩을 사용할 때 활용도가 높아진다.

✅ 부모 타입 변수(업캐스팅된 상태)에서는 부모 클래스에 존재하는 메서드만 호출할 수 있다.

✅ 자식 클래스에만 있는 메서드를 호출하려면 다운캐스팅이 필요하다.

✅ 다운캐스팅은 웬만하면 지양하지만, 필요할 때(배열, 매개변수) 활용할 수 있다.

✅ 다형성을 적극 활용하면 유지보수가 쉬운 유연한 코드 작성이 가능하다