다형성
객체 지향 프로그래밍의 대표적인 특징으로는 캡슐화, 상속, 다형성이 있다. 그중에서 다형성은 객체지향 프로그래밍의 꽂이라 불린다. why? :코드의 유연성과 재사용성을 크게 높여주며, 복잡한 시스템을 더 간단하게 다룰 수 있게 해 주기 때문! 앞서 학습한 캡슐화나 상속은 직관적으로 이해하기 쉽다. 반면에 다형성은 제대로 이해하기도 어렵고, 잘 활용하기는 더 어렵다. 하지만 좋은 개발자가 되기 위해서는 다형성에 대한 이해가 필수라고 한다! 다형성에 대해 공부하고 정리해보자!
다형성(Polymorphism)은 이름 그대로 "다양한 형태", "여러 형태"를 를 뜻한다. 프로그래밍에서 다형성은 한 객체가 여러 타입의 객체로 취급될 수 있는 능력을 뜻한다. 보통 하나의 객체는 하나의 타 입으로 고정되어 있다. 그런데 다형성을 사용하면 하나의 객체가 다른 타입으로 사용될 수 있다는 뜻이다. 지금은 이 내 용을 이해하기보다는 참고만 하자! (오버라이드가 다형성 구현 중 하나인가?)
- 다형적 참조
- 메서드 오버라이딩
다형적 참조
부모 타입의 변수가 자식 인스턴스 참조
public class polyMain {
public static void main(String[] args) {
//부모 변수가 부모 인스턴스 참조
System.out.println("Parent -> Parent");
Parent parent = new Parent();
parent.parentMethod();
//자식 변수가 자식 인스턴스 참조
System.out.println("Child -> Child");
Child child = new Child();
child.parentMethod();
child.childMethod();
//부모 변수가 자식 인스턴스 참조(다형적 참조)
System.out.println("Parent -> Child");
Parent poly = new Child(); //부모타입의 변수인 poly에 담는다.
poly.parentMethod();
// Child child1 = new Parent(); //자식은 부모를 담을 수 없다.
}
}
자바에서 부모 타입은 자기 자신은 물론이고, 자신을 기준으로 모든 자식 타입을 참조할 수 있다. 이것이 바로 다양한 형태를 참조할 수 있다고 해서 다형적 참조라고 한다.
나는 대화식으로 쉽게 표현하는 것이 이해가 잘되어서 한 번 쉽게 풀어보았다.
- Parent poly = new Child();
- "나는 부모님의 능력을 우선 장착한 이후 태어났어, 하지만 내 개성도 있다!
- 이 경우, poly는 parent의 메서드만 사용할 수 있습니다. child의 특별한 메서드는 사용 X (한계)
- class Child extends parent
- "나는 부모님의 모든 능력을 물려받았어, 거기에 나만의 특별한 능력도 추가했지!"
- Child poly = new Child();
- "나는 나 자신 그대로의 모습이야. 부모님의 능력도 있고, 내 특별한 능력도 모두 사용할 수 있어!" 이거지?
다만, 다형적 참조의 한계가 있다.

호출자인 poly는 Parent 타입이므로 Parent 클래스부터 시작해서 필요한 기능을 찾는다. 그런데, 상속 관계에서는 부모 방향으로 찾아 올라갈 수는 있지만, 자식 방향으로 찾아 내려갈 수는 없다. Parent는 부모타입이고 상위에 부모가 없다. 따라서 childMethod()를 찾을 수 없으므로 컴파일 오류가 발생한다.
그럼 childMethod()를 호출하고 싶다면? 바로 →’ 캐스팅’이 필요하다.
다형적 참조의 핵심은 부모는 자식을 품을 수 있다는 것.
But, 왜 필요하지? 그냥 상속과 같이 클래스를 받아서 하는 거랑 뭐가 다르지?라는 의문이 든다. 하지만 지금은 좀 더 이론적으로 접근하여 이해하도록 집중해 봐야겠다.
다형성과 캐스팅
public class CastingMain1 {
public static void main(String[] args) {
//부모 변수가 자식 인스턴스 참조 (다형적 참조)
Parent poly = new Child();
//단 자식의 기능은 호출 할 수 없다.
// poly.childMethod //컴파일 오류
//다운 캐스팅 (부모 타입 => 자식 타입)
Child child = (Child) poly;
((Child) poly).childMethod();
}
}
호출하는 타입의 자식인 child 타입으로 변경하면, childMethod()를 호출할 수 있다. 하지만, 부모는 자식을 담을 수 있지만 자식은 부모를 담을 수 없는 문제에 봉착한다.
실행순서:
Child child = (Child) poly //다운캐스팅을 통해 부모타입을 자식 타입으로 변환한 다음에 대입 시도
Child child = (Child) x001 //참조값을 읽은 다음 자식 타입으로 지정
Child child = x001 //최종 결과
캐스팅을 한다고 해서 Parent poly의 타입이 변하는 것이 아니다. 해당 참조값을 꺼내고 꺼낸 참조값이 Child 타입이 되는 것이다. 따라서 poly의 타입은 Parent로 유지.
캐스팅
- 업캐스팅(upcasting): 부모 타입으로 변경
- 다운캐스팅(downcasting): 자식 타입으로 변경
캐스팅의 종류
일시적 다운 캐스팅
//일시적 다운캐스팅 -해당 메서드를 호출하는 순간만 다운캐스팅
((Child) poly).childMethod(); //연산자 우선순위로 괄호 씌우기.
업캐스팅
Child child = new Child();
Parent parent1 = (Parent) child; //업캐스팅은 생략 가능, 생략 권장
Parent parent2 = child;
업캐스팅은 생략해도 되고, 다운캐스팅은 개발자가 직접 명시적으로 캐스팅을 해야 한다.
업캐스팅이 안전하고 다운캐스팅이 위험한 이유
업캐스팅의 경우 이런 문제가 절대로 발생하지 않는다. 왜냐하면 객체를 생성하면 해당 타입의 상위 부모 타입은 모두 함께 생성된다! 따라서 위로만 타입을 변경하는 업캐스팅은 메모리 상에 인스턴스가 모두 존재하기 때문에 항상 안전하다. 따라서 캐스팅을 생략할 수 있다.
반면에 다운캐스팅의 경우 인스턴스에 존재하지 않는 하위 타입으로 캐스팅하는 문제가 발생할 수 있다. 왜냐하면 객체를 생성하면 부모 타입은 모두 함께 생성되지만 자식 타입은 생성되지 않는다. 따라서 개발자가 이런 문제를 인지하고 사용해야 한다는 의미로 명시적으로 캐스팅을 해주어야 한다.
- 업캐스팅: 자식 클래스의 객체를 부모 클래스의 타입으로 취급하는 것입니다. 이는 안전하다. 왜냐하면 자식 클래스는 항상 부모 클래스의 모든 특성을 가지고 있기 때문입니다.
- 다운캐스팅: 부모 클래스의 객체를 자식 클래스의 타입으로 취급하는 것입니다. 이는 위험할 수 있다. 왜냐하면 부모 클래스의 객체가 실제로 자식 클래스의 특별한 속성이나 메서드를 가지고 있지 않을 수 있기 때문입니다.
확실하게 그림이 그려지지 않아, 역시 또다시 실생활 예시로 우선 이해해 봐야겠다.
실생활 예시: 가족과 물건
클래스 구조
- 가족 (최상위 클래스)
- 부모님 (가족을 상속)
- 자녀 (부모님을 상속)
물건 분류
- 가족 공용 물건: TV, 냉장고, 소파
- 부모님 물건: 차, 커피머신
- 자녀 물건: 게임기, 스마트폰
업캐스팅 (안전한 경우)
- 자녀가 가족 공용 물건 사용
- 자녀가 부모님 물건 사용 (허락 하에)
🔑 핵심: 자녀는 가족의 일원이고 부모님의 자녀이기 때문에 이러한 물건들을 사용할 권한이 있어 안전.
다운캐스팅 (위험할 수 있는 경우)
- 가족 공용 물건을 자녀 전용 물건으로 취급
- 부모님 물건을 자녀 전용 물건으로 취급
⚠️주의: 이는 위험할 수 있다. 모든 가족 공용 물건이나 부모님 물건이 자녀 전용은 아니기 때문이다.
결론
- 업캐스팅은 일반적으로 안전하고 자연스러운 과정이다.
- 다운캐스팅은 주의가 필요하며, 특정 상황에서만 적절하게 사용해야 한다.
그림으로 설명 -업캐스팅

즉 이거에 맞춰서 위에 예시를 적용해 보면,
- Class A = 가족 클래스
- Class B = 부모님 클래스 (가족을 상속)
- Class C = 자녀 클래스 (부모님을 상속, 즉 가족도 상속)
new C()를 실행하면 (즉, 새로운 자녀 인스턴스를 생성하면):
- 메모리에 x001이라는 객체가 생성됩니다.
- 이 객체는 A(가족), B(부모님), C(자녀) 클래스의 모든 메서드를 포함:
즉, 자녀 클래스의 인스턴스를 생성할 때:
- 가족의 기능 (TV 시청 등)
- 부모님의 기능 (차 운전 등)
- 자녀 자신의 기능 (게임기 사용 등)
생각 정리 해보자
업캐스팅은 자식 클래스를 부모클래스로 캐스팅하는 거야 타입을 바꾸는 거지 이건 상관이 없어 왜냐면 자식은 애초에 부모 거를 상속받고 있기 때문에 거기 안에 있는 모든 메서드들을 쓸 수 있고 부모 객체의 특성을 자식 객체 안에 포함할 수 있어 안전해 상관없지.
다만, 다운 캐스팅을 해보자 만약 부모클래스를 자식 클래스로 내려버려. 근데 부모 클래스에는 자식 클래스에 있는 게 없단 말이지? 왜냐! 자식 클래스의 기능은 부모에게 공유되지 않아 이거 맞나?

휴 이해하는데 오래 걸렸다..
컴파일 오류 vs 런타임 오류
- 컴파일 오류 vs 런타임 오류 컴파일 오류는 변수명 오타, 잘못된 클래스 이름 사용등 자바 프로그램을 실행하기 전에 발생하는 오류이다. 이런 오류는 IDE에서 즉시 확인할 수 있기 때문에 안전하고 좋은 오류이다. 반면에 런타임 오류는 이름 그대로 프로그램이 실행되고 있는 시점에 발생하는 오류이다. 런타임 오류는 매우 안 좋은 오류이다. 왜냐하면 보통 고객이 해당 프로그램을 실행하는 도중에 발생하기 때문이다.
instance of
public class CastingMain5 {
public static void main(String[] args) {
Parent parent1 = new Parent();
call(parent1);
Parent parent2 = new Child();
call(parent2);
}
private static void call(Parent parent){
if(parent instanceof Child){
System.out.println("Child 인스턴스 맞음");
Child child = (Child) parent; //다운캐스팅 가능
child.childMethod();
}else{
System.out.println("Child 인스턴스 아님");
}
}
다형성에서 참조형 변수는 이름 그대로 다양한 자식을 대상으로 참조할 수 있다. 그런데 참조하는 대상이 다양하기 때문에 어떤 인스턴스를 참조하고 있는지 확인하려면 어떻게 해야 할까?
위의 코드처럼 parent1, parent2 변수가 참조하는 인스턴스 타입을 확인하고 싶다면 instance of 키워드를 사용하자!
다형성과 메서드 오버라이딩
앞서 위에 기재된 메서드 오버라이딩은 반쪽짜리이다. 메서드 오브라딩의 진짜 힘은 다형성과 함께 사용할 때 나타난다!
public class OverridingMain {
public static void main(String[] args) {
//자식 변수가 자식 인스턴스 참조
Child child = new Child();
System.out.println("Child => Child");
System.out.println("value = " + child.value);
child.method();
//부모 변수가 부모 인스턴스 참조
Parent parent = new Parent();
System.out.println("Parent => Parent");
System.out.println("value = " + parent.value);
parent.method();
//부모 변수가 자식 인스턴스 참조 (다형적 참조)
Parent poly = new Child();
System.out.println("Parent => Child");
System.out.println("value = " + poly.value); //변수는 오버라이딩 x
poly.method(); //메서드 오버라이딩 !
}
}
poly.value: Parent 타입에 있는 value 값을 읽는다. poly.method() : Parent 타입에 있는 method()를 실행하려고 한다. 그런데 하위 타입인 Child.method()가 오버라이딩 되어 있다. 오버라이딩 된 메서드는 항상 우선권을 가진다. 따라서 Parent.method()가 아니라 Child.method()가 실행된다.
🔑 오버라이딩 된 메서드는 항상 우선권을 가진다.
- 오버라이딩은 부모 타입에서 정의한 기능을 자식 타입에서 재정의하는 것이다. 만약 자식에서도 오버라이딩 하고 손자에서도 같은 메서드를 오버라이딩을 하면 손자의 오버라이딩 메서드가 우선권을 가진다. 더 하위 자식의 오버라이딩 된 메서드가 우선권을 가지는 것이다.
- 말 그대로 오버라이딩은 위에서 말했듯이, 엎어 쓰기이기 때문에 제일 ‘최신화된 메서드를 보여주기 때문일까?’라는 생각이 든다.
핵심 :
- 다형적 참조 : 하나의 변수 타입으로 다양한 자식 인스턴스를 참조할 수 있는 기능
- 메서드 오버라이딩 : 기존 기능을 하위 타입에서 새로운 기능으로 재정의
지금까지 다형성의 이론에 관해 학습하였지만, 계속하여 이게 어떻게 쓰이지?라는 의문을 가지고 이론을 학습하였다.
이제 다양한 예제들에 직접 적용하며 다형성이 왜 필요한지, 다형성의 장점은 무엇인지, 몸소 코드를 작성하며 체화하도록 해야겠다.continue...
🔜🔜🔜🔜
'Language > ☕ Java' 카테고리의 다른 글
[Java] 다형성 역할과 구현 예제 (0) | 2024.08.15 |
---|---|
[Java] 다형성 2 (0) | 2024.08.14 |
[Java] 상속 (0) | 2024.08.12 |
[Java] 반복문(for) (2) | 2024.08.08 |
[Java] 메모리 구조 & static (2) | 2024.08.07 |