다형성이란 업캐스팅 동적 바인딩 다형성 활용 instanceof·다운캐스팅 📝 문제풀이
◀ 이전 📋 목차 CH 13 ▶
🎭 CHAPTER 12 · 중급

하나의 이름, 여러 개의 얼굴

지난 장에서 배운 상속 위에 올라타는 개념이에요. 다형성(Polymorphism)은 "같은 명령어 한 줄이 상황에 따라 다르게 동작"하게 만드는 마법입니다. 처음엔 낯설지만, 비유로 하나씩 짚어가면 "아, 이래서 객체 지향이구나" 하는 순간이 와요.

🎯 이 장을 끝내면
🎭
다형성이란
"하나의 타입이 여러 모습을 가진다"
단어를 쪼개보면 뜻이 보여요. Poly(여러) + Morph(모습).

다형성(Polymorphism)은 말 그대로 "여러(poly) 모습(morph)"이라는 뜻이에요. 자바에서는 하나의 참조 변수(타입)가, 실제로는 여러 종류의 객체를 가리킬 수 있다는 성질을 말해요. 그리고 그 변수로 메서드를 부르면, 실제로 담긴 객체에 맞는 동작이 실행됩니다.

🔤
다형성(Polymorphism) — 여러(poly) 모습(morph)이라는 뜻이에요. 하나의 부모 타입 변수가 여러 종류의 자식 객체를 가리킬 수 있고, 같은 메서드를 불러도 실제로 담긴 객체에 맞는 동작이 실행되는 성질을 말해요.
🗣️ 비유 — "동물"이라는 리모컨 버튼 리모컨에 "소리 내" 버튼이 딱 하나 있다고 해봐요. 그런데 이 리모컨을 강아지에게 대고 누르면 "멍멍", 고양이에게 대고 누르면 "야옹"이 나와요.
버튼(명령)은 하나인데, 대상(실제 객체)이 무엇이냐에 따라 결과가 달라지죠. 이게 다형성이에요. 나(개발자)는 "소리 내"만 외치고, 진짜 어떤 소리를 낼지는 객체가 알아서 결정합니다. 💡 핵심 한 줄: 부르는 쪽은 똑같은데, 대상이 달라서 결과가 달라진다.
🧬
다형성은 상속이 전제예요. 강아지·고양이가 모두 "동물"이라는 공통 부모를 물려받았기 때문에, "동물" 타입 하나로 둘 다 가리킬 수 있는 거예요. 지난 장의 extends@Override가 그대로 재료로 쓰입니다.

왜 유용할까요? 만약 다형성이 없다면, 강아지용 코드·고양이용 코드·앵무새용 코드를 따로따로 써야 해요. 새 동물이 늘 때마다 코드를 뜯어고쳐야 하죠. 다형성을 쓰면 "동물"이라는 하나의 창구로 모두를 다룰 수 있어서, 코드가 유연하고 확장하기 쉬워집니다.

Polymorphism 상속 전제 업캐스팅 동적 바인딩 Override instanceof 다운캐스팅 ClassCastException
⬆️
업캐스팅
부모 타입 변수에 자식 객체 담기
다형성의 첫 관문. 그런데 놀랍도록 간단해요.

먼저 재료를 준비할게요. 부모 클래스 Animal과, 이를 상속받아 sound()를 오버라이딩한 자식 Dog·Cat이 있다고 해봐요.

class Animal {
    void sound() {
        System.out.println("어떤 소리를 냅니다");
    }
}

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

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

이제 핵심입니다. 부모 타입 변수에 자식 객체를 담을 수 있어요. 이걸 업캐스팅(upcasting)이라고 불러요 — 자식을 위(부모 방향)로 올려서 본다는 뜻이에요.

Animal a = new Dog();   // ⬆️ 업캐스팅 — Dog 객체를 Animal 타입 변수에 담음
Animal b = new Cat();   // ⬆️ Cat 객체도 Animal 타입으로
✅ 업캐스팅은 "자동" — 명시적 캐스트가 필요 없어요 Dog는 언제나 Animal이다. 강아지는 무조건 동물이니까요. 그래서 자바는 안전하다고 판단하고 자동으로 허용해요. 캐스트 연산자 (Animal)을 붙이지 않아도 됩니다.
Animal a = new Dog();는 그냥 되지만, 반대로 자식 타입에 부모를 담는 다운캐스팅은 자동이 아니에요(뒤 sec-4에서 다룸). 방향을 기억하세요: 위로(자식→부모)는 자동, 아래로(부모→자식)는 수동.
💡
담을 수 있는 규칙 한 줄. "자식 is-a 부모"가 성립하면(즉 상속 관계면) 부모 타입 변수에 자식 객체를 넣을 수 있어요. Object o = new Dog();도 되는데, 모든 클래스의 최상위 부모가 Object이기 때문이에요.
🖼️ 그림으로 보기 — 업캐스팅 — 부모 타입으로 자식 담기
Animal a 부모 타입 변수 (그릇의 라벨은 Animal) = new Dog() 실제 객체는 Dog (속 알맹이는 강아지) 변수 타입=Animal, 실제 객체=Dog → a.sound() 하면 Dog의 sound() 실행(동적 바인딩)
부모 타입 변수에 자식 객체를 담는 게 업캐스팅. 호출하면 실제 객체(자식)의 오버라이딩된 메서드가 실행돼요.
🎯
동적 바인딩
실제 객체의 메서드가 실행된다
여기가 다형성의 진짜 심장. 이 한 절만 이해하면 절반은 끝나요.

이제 진짜 신기한 부분이에요. Animal a = new Dog(); 한 뒤 a.sound()를 부르면 뭐가 나올까요? 변수 타입은 Animal인데 말이죠.

Animal a = new Dog();
a.sound();   // 출력: 멍멍  ← Animal의 "어떤 소리를 냅니다"가 아님!

Animal b = new Cat();
b.sound();   // 출력: 야옹

참조 타입(Animal)이 아니라, 실제 객체(Dog)의 오버라이딩된 메서드가 실행돼요. 변수는 Animal이라고 적혀 있지만, 안에 진짜 들어있는 건 Dog 객체니까, Dog가 재정의한 sound()가 불립니다.

📝
a.sound()
Animal 타입 변수로 호출
🔍
실행 시점 판단
JVM이 "a에 진짜 뭐 들었지?" 확인
🐶
실제 객체 = Dog
Dog의 sound()로 결정
🔊
"멍멍" 출력
오버라이딩된 버전 실행
🧠 왜 이렇게 동작할까 — "동적 바인딩(가상 메서드 호출)" "바인딩(binding)"은 "메서드 호출을 어떤 실제 코드에 연결할지 정하는 일"이에요.
정적 바인딩이라면 컴파일할 때 변수 타입(Animal) 기준으로 미리 정해버려요.
하지만 자바의 인스턴스 메서드는 동적 바인딩(dynamic binding)이에요 — 실행되는 순간에 "이 변수에 진짜 들어있는 객체가 뭐지?"를 보고, 그 객체(Dog)의 메서드로 연결합니다. 그래서 실제 객체 기준으로 실행돼요. 💡 다른 말로 가상 메서드 호출이라고도 해요. 이 성질이 없으면 다형성도 없어요. 딱 이거예요: "변수 타입이 아니라, 담긴 객체가 메서드를 결정한다."
⚠️
주의 — 변수 타입은 "부를 수 있는 메서드 범위"를 정해요. Animal a = new Dog();일 때 a로는 Animal에 선언된 메서드만 부를 수 있어요. Dog에만 있는 고유 메서드(예: a.bark())는 컴파일 에러예요. "어떤 메서드를 부를 수 있나"는 변수 타입이 정하고, "어떤 버전이 실행되나"는 실제 객체가 정합니다. 이 둘을 구분하세요!
🎪
다형성 활용
한 창구로 여러 객체를 다루기
여기서 다형성의 "실용성"이 확 와닿아요.

부모 타입 하나로 자식들을 다 담을 수 있으니, 부모 타입 배열에 서로 다른 자식들을 섞어 넣을 수 있어요. 그리고 반복문 한 줄로 돌리면, 각자 자기 방식대로 동작합니다.

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

for (Animal a : arr) {
    a.sound();   // 같은 a.sound() 한 줄이지만…
}
// 출력:
// 멍멍
// 야옹
// 멍멍

코드에는 a.sound() 딱 한 줄뿐인데, 반복문이 도는 동안 담긴 객체가 바뀌면서 매번 다른 소리가 나와요. if (a instanceof Dog) … else if (a instanceof Cat) … 같은 분기가 전혀 필요 없어요. 동적 바인딩이 알아서 처리하니까요.

📦 매개변수로도 — "동물이면 뭐든 받아요" 메서드 매개변수를 부모 타입으로 선언하면, 그 자식은 무엇이든 넘길 수 있어요.
이렇게 하면 makeSound 메서드는 동물의 종류를 몰라도 동작해요. 나중에 Parrot(앵무새) 클래스를 새로 추가해도, 이 메서드는 한 글자도 안 고치고 그대로 씁니다. 이게 다형성이 주는 "확장성"이에요.
static void makeSound(Animal a) {   // Animal 자리에 Dog·Cat 무엇이든 OK
    a.sound();
}

makeSound(new Dog());   // 멍멍
makeSound(new Cat());   // 야옹
// 훗날 Parrot 을 추가해도 makeSound 는 그대로 재사용!
🌱
확장에 열려 있다. "새 자식 클래스를 추가해도 기존 코드를 안 고쳐도 되는" 이 성질이 객체 지향 설계의 핵심 이점이에요. 다형성이 바로 그걸 가능하게 만들어 줍니다.
🔽
instanceof · 다운캐스팅
실제 타입을 확인하고 되돌리기
가끔은 "이 동물이 진짜 강아지인지" 확인하고 원래 모습으로 돌려야 해요.

앞에서 봤듯, Animal a = new Dog();일 때 a로는 Animal 메서드만 부를 수 있어요. 그런데 Dog에만 있는 고유 기능(예: bark())을 써야 한다면? 다시 Dog 타입으로 되돌려야 해요. 이걸 다운캐스팅(downcasting)이라고 합니다 — 부모 타입을 아래(자식 방향)로 내려서 보는 것.

🧭 방향으로 기억하기 — 위로는 자동, 아래로는 수동
⬆️
업캐스팅 · 자식 → 부모
"Dog는 언제나 Animal" 이라 항상 안전 → 자동. 캐스트 없이 Animal a = new Dog();
🔽
다운캐스팅 · 부모 → 자식
"이 Animal이 진짜 Dog?" 는 보장 안 됨 → 수동. 명시적 캐스트 Dog d = (Dog) a;

다운캐스팅은 업캐스팅과 달리 자동이 아니에요. "이 동물이 진짜 강아지가 맞아?"가 보장되지 않으니, 개발자가 명시적으로 캐스트 연산자를 써서 "내가 책임진다"고 알려줘야 합니다.

Animal a = new Dog();

// a.bark();          // ❌ 컴파일 에러 — Animal 타입엔 bark()가 없음
Dog d = (Dog) a;      // 🔽 다운캐스팅 — 명시적 캐스트 필요
d.bark();             // ✅ 이제 Dog 고유 메서드 호출 가능
🧪 안전장치 — instanceof 로 먼저 확인 문제는, 실제로 Dog가 아닌데 억지로 (Dog)로 캐스트하면 실행 중에 터져요. 그래서 다운캐스팅 전에 instanceof로 실제 타입을 확인하는 게 안전해요.
객체 instanceof 타입은 그 객체가 해당 타입(또는 자식)이면 true, 아니면 false를 줍니다. 💡 규칙: 확인(instanceof) → 통과하면 다운캐스팅. 이 순서를 지키면 캐스팅 사고를 막을 수 있어요.
Animal a = new Cat();

System.out.println(a instanceof Cat);     // true  — 실제 객체가 Cat
System.out.println(a instanceof Dog);     // false — Cat은 Dog가 아님
System.out.println(a instanceof Animal);  // true  — Cat은 Animal이기도 함

if (a instanceof Dog) {       // false → 블록 안 들어감 (안전!)
    Dog d = (Dog) a;
    d.bark();
} else {
    System.out.println("이 동물은 강아지가 아니에요");
}
💥
잘못된 다운캐스팅 = ClassCastException. 실제로 Cat 객체인데 Dog d = (Dog) a;처럼 억지로 다른 자식 타입으로 캐스트하면, 컴파일은 통과하지만 실행 중에 ClassCastException이 발생해요. 그래서 instanceof 확인이 중요합니다. 시험에서 "잘못된 다운캐스팅 예외"를 물으면 답은 ClassCastException이에요.
✨ 번외 — instanceof 패턴(Java 16+): 확인과 캐스트를 한 번에
위 코드는 instanceof로 확인한 뒤 또 캐스트하는 게 조금 번거로워요. Java 16부터는 패턴 매칭 instanceof로 이걸 한 방에 합칠 수 있어요.
Animal a = new Dog();

// 확인이 true면, 곧바로 d(Dog 타입)에 캐스팅된 값이 담김
if (a instanceof Dog d) {
    d.bark();   // 별도 캐스트 없이 바로 사용!
}
instanceof Dog d에서 뒤의 d가 바로 캐스팅까지 끝난 변수예요. 조건이 참일 때만 유효하고, 코드가 짧고 안전해집니다. 최신 자바를 쓴다면 이 형태를 즐겨 쓰게 될 거예요. (지금 단계에선 "이런 것도 있구나" 정도면 충분해요.)
🧠 이 장 핵심 요약
📝
문제풀이 · 점검
배운 걸 가볍게 점검해봐요
시험이 아니라 "내가 이해했나" 확인용이에요. 틀려도 바로 해설이 나와요.
🧪
문제를 풀면 즉시 정답과 해설이 나오고, 위쪽 바에 점수가 쌓여요. 네 가지 유형(객관식 · O/X · 빈칸 · 코드결과)을 섞어 두었어요. 편하게 눌러보세요.
CH 13 🧩 추상 클래스와 인터페이스 — "틀"만 정해두고 자식이 채우게 하기