우선, 글을 작성하기 전 이 글의 모든 내용은 김영한님의 JAVA 강의를 바탕으로 함을 알립니다.
다형성(Polymorphism)은 객체지향 프로그래밍(Object Oriented Programming)의 대표적인 특징 중 하나로 '다양한 형태'를 의미한다. 프로그래밍에서 다형성은 '한 객체가 여러 타입의 객체로 취급되는 것'을 의미한다.
다형성은 객체 지향 프로그래밍의 상속과 인터페이스를 기반으로 한다.
java에서 클래스를 생성하면 클래스의 속성(필드, 메서드)에 접근하기 위해 객체(인스턴스)를 생성해야한다. 객체가 생성되면 힙메모리 영역에 해당 객체의 클래스에 대한 내용이 저장되고, 이때 상속관계가 있다면 부모 클래스와 자식 클래스 모두 하나의 메모리 영역에 저장된다. 우리가 살펴보아할 경우는 '상속'관계가 존재하는 객체이다.
일반적으로, 객체를 생성하면 해당 객체가 생성된 힙메모리영역의 참조값을 해당 클래스 형태의 변수에 저장한다.
Parent parent = new Parent();
Child child = new Child();
예를들어, new Parent();를 통해 객체를 통해 생성된 메모리의 참조값을 x001이라고 가정하면 다음과 같다고 볼 수 있다.
Parent parent = x001;
하지만 java는 다형적 참조에의해 상속관계에 놓인 Parent와 Child 사이에서 부모클래스인 Parent는 Parent와 자식 클래스 Child 모두를 참조할 수 있다. 이 개념이 '다형적 참조'이다.
Parent poly = new Child();
즉, 다형적 참조의 핵심은 "부모는 자식을 품을 수 있다"라는 것이다. 이는 '캐스팅'의 개념과 연결된다.
위에서 언급했듯이, 상속관계가 존재하는 클래스에 대해 객체를 생성하면 메모리 영역에 부모클래스와 자식클래스에 대한 속성이 메모리 영역에 저장된다. 그렇다면 정확히 어떤 클래스를 우선 참조할까? 정답은 ,, 타입에 의존한다는 것이다.
Child child = new Child(); // Child 타입 변수 --> Child 클래스를 우선 참조
Parent poly = new Child(); // 다형적 참조 --> Parent 클래스를 우선 참조
다형적 참조의 경우 부모 타입의 변수에 참조값이 저장되었기에 우선적으로 메모리의 부모 클래스에 대한 속성을 참조하게 되고 직접적으로는 부모 클래스에 대한 속성만 사용이 가능하다. 즉, 다형적 참조에서 객체를 통한 직접적인 자식 클래스에 대한 속성을 사용하는 것은 컴파일 에러를 야기한다.

이럴때 자식 클래스의 속성을 사용하고 싶다면,, 어떻게 해야할까? '다운 캐스팅'
다운 캐스팅은 말 그대로 부모 -> 자식으로 캐스팅 하는 것이고, 업 캐스팅은 자식 -> 부모로 캐스팅하는 것이다.
일시적 다운 캐스팅은 자식 클래스의 메소드를 호출할 때만 일시적으로 다운 캐스팅 되는 경우를 의미한다.
// 다운 캐스팅
Parent poly = new Child();
Child child = (Child) poly;
// 일시적 다운 캐스팅
Parent poly = new Child();
((Child)poly).childMethod()
// 업 캐스팅
Child child = new Child();
Parent parent1 = (Parent) child;
Parent parent2 = child; // parent2 처럼 업캐스팅은 생략을 권장한다.
부모는 자식을 품을 수 있다. 단, 서로 부모-자식 관계가 성립이 되어야 가능하다. 이게 무슨 말일까?
상속관계의 클래스는 객체 생성시 메모리에 부모, 자식 속성이 모두 저장된다고 했다. 이렇게 상속관계에 의해 메모리에 부모와 자식이 모두 존재할 때 비로소 다운 캐스팅이 가능하다.

다운캐스팅이 불가능한 경우는 다음과 같다.
Parent parent = new Parent();
Child child = (Child) parent;
상속관계가 없는 경우에는 메모리상에 부모와 자식이 공존하지 않기에 다운 캐스팅이 불가능하다.


다형성의 두 번째 핵심이론은 '메서드 오버라이딩'이다. 메서드 오버라이딩이 뭘까?
Override, "덮어쓰기"라는 의미이다. 직역하면 메서드 오버라이딩은 메서드를 덮어쓰는 것을 의미한다. 즉, 메서드 오버라이딩이란 상속관계가 존재하는 부모클래스의 특정 메서드를 자식 클래스에서 재정의하여 사용하는 것을 의미한다.
public class Parent {
public String value = "Parent";
public void method() {
System.out.println("Parent.method");
}
}
public class Child extends Parent {
public String value = "child";
@Override
public void method() {
System.out.println("Child.method");
}
}
위의 예제를 보면 Child에서 Parent의 method()를 재정의한 것을 볼 수 있다. 이를 그림으로 도식화하면 다음과 같다.

그러면 여기서 한 가지 의문점이 발생한다. 그냥 굳이 오버라이딩을 하지않고 자식 클래스에 동일한 이름의 메서드를 만들고 해당 메서드의 고유 기능을 정의하면 되는 것 아닐까??
이 부분에서 메서드 오버라이디의 두 가지 특징을 살펴본다.
메서드 오버라이딩의 특징과 다형적 참조를 연결지어보면 다음과 같다.
Parent poly = new Child();
poly.method();
위의 코드를 도식화하면 다음과 같다.

즉, 메서드 오버라이딩에 의해 부모 클래스의 고유 기능은 유지하며 여러 자식 클래스에 특화된 동작이 가능해진 것이다. 이는 객체지향 프로그래밍에서 코드의 유지보수를 용이하게 하는 방법 중 하나이다.