[Java] 다형성 Part.1

Java 기본 — 다형성1

김영한의 실전 자바 - 기본편 강의 내용을 정리한 글이다.

1. 다형성이란

객체지향 프로그래밍의 대표적인 특징으로는 캡슐화, 상속, 다형성이 있다. 그 중에서 다형성은 객체지향 프로그래밍의 꽃이라 불린다.

앞서 학습한 캡슐화나 상속은 직관적으로 이해하기 쉽다. 반면에 다형성은 제대로 이해하기도 어렵고, 잘 활용하기는 더 어렵다. 하지만 좋은 개발자가 되기 위해서는 다형성에 대한 이해가 필수다.

다형성(Polymorphism)은 이름 그대로 "다양한 형태", "여러 형태"를 뜻한다. 프로그래밍에서 다형성은 한 객체가 여러 타입의 객체로 취급될 수 있는 능력을 뜻한다. 보통 하나의 객체는 하나의 타입으로 고정되어 있다. 그런데 다형성을 사용하면 하나의 객체가 다른 타입으로 사용될 수 있다는 뜻이다.

다형성을 이해하기 위해서는 크게 2가지 핵심 이론을 알아야 한다.

  • 다형적 참조
  • 메서드 오버라이딩

2. 다형적 참조

다형적 참조를 이해하기 위해 다음과 같은 간단한 상속 관계를 코드로 만들어보자.

Parent + parentMethod() + childMethod() Child

부모와 자식이 있고, 각각 다른 메서드를 가진다.

package poly.basic; public class Parent { public void parentMethod() { System.out.println("Parent.parentMethod"); } }
package poly.basic; public class Child extends Parent { public void childMethod() { System.out.println("Child.childMethod"); } }
package poly.basic; // 다형적 참조: 부모는 자식을 품을 수 있다. 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.parentMethod(); // Child child1 = new Parent(); 자식은 부모를 담을 수 없다. // poly.childMethod(); // 자식의 기능은 호출할 수 없다. 컴파일 오류 발생 } }

실행 결과

Parent -> Parent Parent.parentMethod Child -> Child Parent.parentMethod Child.childMethod Parent -> Child Parent.parentMethod

메모리 구조로 이해하기

부모 타입의 변수가 부모 인스턴스 참조 (Parent → Parent)

x001 Parent parent parentMethod() 호출 x001 + parentMethod() Parent
  • Parent parent = new Parent()으로 부모 타입인 Parent를 생성했기 때문에 메모리 상에 Parent만 생성된다. 자식은 생성되지 않는다.
  • 생성된 참조값을 Parent 타입의 변수인 parent에 담아둔다.
  • parent.parentMethod()를 호출하면 인스턴스의 Parent 클래스에 있는 parentMethod()가 호출된다.

자식 타입의 변수가 자식 인스턴스 참조 (Child → Child)

x001 Child child childMethod() 호출 x001 + parentMethod() Parent + childMethod() Child
  • Child child = new Child()으로 자식 타입인 Child를 생성했기 때문에 메모리 상에 Child와 Parent가 모두 생성된다.
  • 생성된 참조값을 Child 타입의 변수인 child에 담아둔다.
  • child.childMethod()를 호출하면 인스턴스의 Child 클래스에 있는 childMethod()가 호출된다.

다형적 참조: 부모 타입의 변수가 자식 인스턴스 참조 (Parent → Child)

x001 Parent poly parentMethod() 호출 x001 + parentMethod() Parent + childMethod() Child
  • 부모 타입의 변수가 자식 인스턴스를 참조한다.
  • Parent poly = new Child()이므로 메모리 상에 Child와 Parent가 모두 생성된다.
  • 생성된 참조값을 Parent 타입의 변수인 poly에 담아둔다.

부모는 자식을 담을 수 있다.

  • 부모 타입은 자식 타입을 담을 수 있다. Parent poly = new Child() : 성공
  • 반대로 자식 타입은 부모 타입을 담을 수 없다. Child child1 = new Parent() : 컴파일 오류

다형적 참조와 인스턴스 실행

poly.parentMethod()를 호출하면 먼저 참조값을 사용해서 인스턴스를 찾는다. 그리고 다음으로 인스턴스 안에서 실행할 타입도 찾아야 한다. poly는 Parent 타입이다. 따라서 Parent 클래스부터 시작해서 필요한 기능을 찾는다. 인스턴스의 Parent 클래스에 parentMethod()가 있으므로 해당 메서드가 호출된다.

다형적 참조의 한계

Parent poly = new Child()처럼 자식을 참조한 상황에서 poly가 자식 타입인 Child에 있는 childMethod()를 호출하면 어떻게 될까?

x001 Parent poly childMethod() 호출 x001 + parentMethod() Parent + childMethod() Child 부모 -> 자식 호출X / 컴파일 오류

poly.childMethod()를 실행하면 먼저 참조값을 통해 인스턴스를 찾는다. 호출자인 poly는 Parent 타입이므로 Parent 클래스부터 시작해서 필요한 기능을 찾는다. 그런데 상속 관계는 부모 방향으로 찾아 올라갈 수는 있지만 자식 방향으로 찾아 내려갈 수는 없다. Parent는 부모 타입이고 상위에 부모가 없다. 따라서 childMethod()를 찾을 수 없으므로 컴파일 오류가 발생한다.

다형적 참조의 핵심은 부모는 자식을 품을 수 있다는 것이다. 이런 경우 childMethod()를 호출하고 싶으면 캐스팅이 필요하다.

자바에서 부모 타입은 자신은 물론이고, 자신을 기준으로 모든 자식 타입을 참조할 수 있다. 이것이 바로 다양한 형태를 참조할 수 있다고 해서 다형적 참조라 한다.

  • Parent poly = new Parent()
  • Parent poly = new Child()
  • Parent poly = new Grandson() : Child 하위에 손자가 있다면 가능

3. 다형성과 캐스팅

Parent poly = new Child()와 같이 부모 타입의 변수를 사용하게 되면 poly.childMethod()와 같이 자식 타입에 있는 기능은 호출할 수 없다. 이때는 다운캐스팅이라는 기능을 사용해서 부모 타입을 잠깐 자식 타입으로 변경하면 된다.

package poly.basic; public class CastingMain1 { public static void main(String[] args) { // 부모 변수가 자식 인스턴스 참조(다형적 참조) Parent poly = new Child(); // 단 자식의 기능은 호출할 수 없다. 컴파일 오류 발생 // poly.childMethod(); // 다운캐스팅(부모 타입 -> 자식 타입) Child child = (Child) poly; child.childMethod(); } }

실행 결과

Child.childMethod

다운캐스팅 동작 원리

x001 Parent poly 1. 다운캐스팅 x001 Child child 2. childMethod() 호출 x001 + parentMethod() Parent + childMethod() Child

캐스팅의 실행 순서는 다음과 같다.

Child child = (Child) poly // 다운캐스팅을 통해 부모타입을 자식 타입으로 변환한 다음에 대입 시도 Child child = (Child) x001 // 참조값을 읽은 다음 자식 타입으로 지정 Child child = x001 // 최종 결과

참고로 캐스팅을 한다고 해서 Parent poly의 타입이 변하는 것은 아니다. 해당 참조값을 꺼내고 꺼낸 참조값이 Child 타입이 되는 것이다. 따라서 poly의 타입은 Parent로 기존과 같이 유지된다.

캐스팅 용어 정리

용어방향예시생략 가능
업캐스팅(upcasting)자식 → 부모 타입Parent p = child가능 (권장)
다운캐스팅(downcasting)부모 → 자식 타입Child c = (Child) poly불가능 (명시 필수)

"캐스팅"은 영어 단어 "cast"에서 유래되었다. "cast"는 금속이나 다른 물질을 녹여서 특정 형태나 모양으로 만드는 과정을 의미한다.

4. 캐스팅의 종류

자식 타입의 기능을 사용하려면 다운캐스팅 결과를 변수에 담아두고 이후에 기능을 사용하면 된다. 하지만 다운캐스팅 결과를 변수에 담아두는 과정이 번거롭다. 이런 과정 없이 일시적으로 다운캐스팅을 해서 인스턴스에 있는 하위 클래스의 기능을 바로 호출할 수 있다.

일시적 다운캐스팅

package poly.basic; public class CastingMain2 { public static void main(String[] args) { // 부모 변수가 자식 인스턴스 참조(다형적 참조) Parent poly = new Child(); // 단 자식의 기능은 호출할 수 없다. 컴파일 오류 발생 // poly.childMethod(); // 일시적 다운캐스팅 - 해당 메서드를 호출하는 순간만 다운캐스팅 ((Child) poly).childMethod(); } }

실행 결과

Child.childMethod
x001 Parent poly 기존 참조 일시적 다운캐스팅 ((Child) poly).childMethod() x001 + parentMethod() Parent + childMethod() Child

((Child) poly).childMethod()poly의 Parent 타입을 임시로 Child로 변경한다. 그리고 메서드를 호출할 때 Child 타입에서 찾아서 실행한다. 정확히는 poly가 Child 타입으로 바뀌는 것이 아니다. 해당 참조값을 꺼내고 꺼낸 참조값이 Child 타입이 되는 것이다. 따라서 poly의 타입은 Parent로 그대로 유지된다.

업캐스팅

다운캐스팅과 반대로 현재 타입을 부모 타입으로 변경하는 것을 업캐스팅이라 한다.

package poly.basic; // upcasting vs downcasting public class CastingMain3 { public static void main(String[] args) { Child child = new Child(); Parent parent1 = (Parent) child; // 업캐스팅은 생략 가능, 생략 권장 Parent parent2 = child; // 업캐스팅 생략 parent1.parentMethod(); parent2.parentMethod(); } }

실행 결과

Parent.parentMethod Parent.parentMethod

그런데 부모 타입으로 변환하는 경우에는 캐스팅 코드인 (타입)을 생략할 수 있다.

Parent parent2 = child; // 업캐스팅 생략 Parent parent2 = new Child() // 이것도 동일

업캐스팅은 생략할 수 있다. 다운캐스팅은 생략할 수 없다. 참고로 업캐스팅은 매우 자주 사용하기 때문에 생략을 권장한다. 자바에서 부모는 자식을 담을 수 있다. 하지만 그 반대는 안된다. 꼭 필요하다면 다운캐스팅을 해야 한다.

5. 다운캐스팅과 주의점

다운캐스팅은 잘못하면 심각한 런타임 오류가 발생할 수 있다. 다음 코드를 통해 다운캐스팅에서 발생할 수 있는 문제를 확인해보자.

package poly.basic; // 다운캐스팅을 자동으로 하지 않는 이유 public class CastingMain4 { public static void main(String[] args) { Parent parent1 = new Child(); Child child1 = (Child) parent1; child1.childMethod(); // 문제 없음 Parent parent2 = new Parent(); Child child2 = (Child) parent2; // 런타임 오류 - ClassCastException child2.childMethod(); // 실행 불가 } }

실행 결과

Child.childMethod Exception in thread "main" java.lang.ClassCastException: class poly.basic.Parent cannot be cast to class poly.basic.Child (poly.basic.Parent and poly.basic.Child are in unnamed module of loader 'app') at poly.basic.CastingMain4.main(CastingMain4.java:11)

실행 결과를 보면 child1.childMethod()는 잘 호출되었지만, child2.childMethod()는 실행되지 못하고, 그 전에 오류가 발생했다.

다운캐스팅이 가능한 경우

x001 Parent parent1 다운캐스팅 x001 Child child1 childMethod() 호출 x001 + parentMethod() Parent + childMethod() Child new Child()

parent1의 경우 다운캐스팅을 해도 문제가 되지 않는다. 이미 new Child()로 생성했기 때문에 인스턴스 내부에 Child가 존재한다.

다운캐스팅이 불가능한 경우

x001 Parent parent2 다운캐스팅 x001 Child child2 x001 + parentMethod() Parent Child 인스턴스 없음 런타임 오류 new Parent()

parent2를 다운캐스팅하면 ClassCastException이라는 심각한 런타임 오류가 발생한다. new Parent()로 부모 타입으로 객체를 생성했기 때문에 메모리 상에 자식 타입은 전혀 존재하지 않는다. parent2를 Child 타입으로 다운캐스팅하면, 메모리 상에 Child 자체가 존재하지 않아 사용할 수 없다. 자바에서는 이렇게 사용할 수 없는 타입으로 다운캐스팅하는 경우에 ClassCastException이라는 예외를 발생시킨다.

업캐스팅이 안전하고 다운캐스팅이 위험한 이유

업캐스팅의 경우 이런 문제가 절대로 발생하지 않는다. 왜냐하면 객체를 생성하면 해당 타입의 상위 부모 타입은 모두 함께 생성된다. 따라서 위로만 타입을 변경하는 업캐스팅은 메모리 상에 인스턴스가 모두 존재하기 때문에 항상 안전하다. 따라서 캐스팅을 생략할 수 있다.

반면에 다운캐스팅의 경우 인스턴스에 존재하지 않는 하위 타입으로 캐스팅하는 문제가 발생할 수 있다. 왜냐하면 객체를 생성하면 부모 타입은 모두 함께 생성되지만 자식 타입은 생성되지 않는다. 따라서 개발자가 이런 문제를 인지하고 사용해야 한다는 의미로 명시적으로 캐스팅을 해주어야 한다.

컴파일 오류 vs 런타임 오류

구분발생 시점특징
컴파일 오류실행 전 (빌드 시)IDE에서 즉시 확인 가능. 안전하고 좋은 오류다.
런타임 오류실행 중고객이 사용 도중에 발생. 매우 안좋은 오류다.

6. instanceof

다형성에서 참조형 변수는 이름 그대로 다양한 자식을 대상으로 참조할 수 있다. 그런데 참조하는 대상이 다양하기 때문에 어떤 인스턴스를 참조하고 있는지 확인하려면 어떻게 해야할까?

Parent parent1 = new Parent() Parent parent2 = new Child()

여기서 Parent는 자신과 같은 Parent의 인스턴스도 참조할 수 있고, 자식 타입인 Child의 인스턴스도 참조할 수 있다. 이때 parent1, parent2 변수가 참조하는 인스턴스의 타입을 확인하고 싶다면 instanceof 키워드를 사용하면 된다.

package poly.basic; public class CastingMain5 { public static void main(String[] args) { Parent parent1 = new Parent(); System.out.println("parent1 호출"); call(parent1); Parent parent2 = new Child(); System.out.println("parent2 호출"); call(parent2); } private static void call(Parent parent) { parent.parentMethod(); if (parent instanceof Child) { System.out.println("Child 인스턴스 맞음"); Child child = (Child) parent; child.childMethod(); } } }

실행 결과

parent1 호출 Parent.parentMethod parent2 호출 Parent.parentMethod Child 인스턴스 맞음 Child.childMethod

call(Parent parent) 메서드를 보자. 이 메서드는 매개변수로 넘어온 parent가 참조하는 타입에 따라서 다른 명령을 수행한다. 참고로 지금처럼 다운캐스팅을 수행하기 전에는 먼저 instanceof를 사용해서 원하는 타입으로 변경이 가능한지 확인한 다음에 다운캐스팅을 수행하는 것이 안전하다.

instanceof 판단 기준

instanceof 키워드는 오른쪽 대상의 자식 타입을 왼쪽에서 참조하는 경우에도 true를 반환한다. 쉽게 이야기해서 오른쪽에 있는 타입에 왼쪽에 있는 인스턴스의 타입이 들어갈 수 있는지 대입해보면 된다. 대입이 가능하면 true, 불가능하면 false가 된다.

// new Parent() instanceof Parent Parent p = new Parent() // 같은 타입 → true // new Child() instanceof Parent Parent p = new Child() // 부모는 자식을 담을 수 있다 → true // new Parent() instanceof Child Child c = new Parent() // 자식은 부모를 담을 수 없다 → false // new Child() instanceof Child Child c = new Child() // 같은 타입 → true

자바 16 - Pattern Matching for instanceof

자바 16부터는 instanceof를 사용하면서 동시에 변수를 선언할 수 있다. 덕분에 인스턴스가 맞는 경우 직접 다운캐스팅 하는 코드를 생략할 수 있다.

package poly.basic; public class CastingMain6 { public static void main(String[] args) { Parent parent1 = new Parent(); System.out.println("parent1 호출"); call(parent1); Parent parent2 = new Child(); System.out.println("parent2 호출"); call(parent2); } private static void call(Parent parent) { parent.parentMethod(); // Child 인스턴스인 경우 childMethod() 실행 if (parent instanceof Child child) { System.out.println("Child 인스턴스 맞음"); child.childMethod(); } } }

실행 결과

parent1 호출 Parent.parentMethod parent2 호출 Parent.parentMethod Child 인스턴스 맞음 Child.childMethod

7. 다형성과 메서드 오버라이딩

다형성을 이루는 또 하나의 중요한 핵심 이론은 바로 메서드 오버라이딩이다. 메서드 오버라이딩에서 꼭 기억해야 할 점은 오버라이딩 된 메서드가 항상 우선권을 가진다는 점이다. 그래서 이름도 기존 기능을 덮어 새로운 기능을 재정의 한다는 뜻의 오버라이딩이다.

앞서 메서드 오버라이딩을 학습했지만 지금까지 학습한 메서드 오버라이딩은 반쪽짜리다. 메서드 오버라이딩의 진짜 힘은 다형성과 함께 사용할 때 나타난다.

Parent + value = "parent" + method() Child + value = "child" + method(): 오버라이딩
  • Parent, Child 모두 value라는 같은 멤버 변수를 가지고 있다. 멤버 변수는 오버라이딩 되지 않는다.
  • Parent, Child 모두 method()라는 같은 메서드를 가지고 있다. Child에서 메서드를 오버라이딩 했다. 메서드는 오버라이딩 된다.
package poly.overriding; public class Parent { public String value = "parent"; public void method() { System.out.println("Parent.method"); } }
package poly.overriding; public class Child extends Parent { public String value = "child"; @Override public void method() { System.out.println("Child.method"); } }
package poly.overriding; 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(); // 메서드 오버라이딩! } }

실행 결과

Child -> Child value = child Child.method Parent -> Parent value = parent Parent.method Parent -> Child value = parent Child.method

핵심 포인트: Parent → Child

x001 Parent poly value, method() 호출 오버라이딩 호출 x001 + value = "parent" + method() Parent + value = "child" + method() Child new Child()
  • poly 변수는 Parent 타입이다. 따라서 poly.value, poly.method()를 호출하면 인스턴스의 Parent 타입에서 기능을 찾아서 실행한다.
  • poly.value : Parent 타입에 있는 value 값을 읽는다. 결과: "parent"
  • poly.method() : Parent 타입에 있는 method()를 실행하려고 한다. 그런데 하위 타입인 Child.method()가 오버라이딩 되어 있다. 오버라이딩 된 메서드는 항상 우선권을 가진다. 따라서 Parent.method()가 아니라 Child.method()가 실행된다.

오버라이딩 된 메서드는 항상 우선권을 가진다. 만약 자식에서도 오버라이딩 하고 손자에서도 같은 메서드를 오버라이딩 하면 손자의 오버라이딩 메서드가 우선권을 가진다. 더 하위 자식의 오버라이딩 된 메서드가 우선권을 가지는 것이다.

8. 정리

지금까지 다형성을 이루는 핵심 이론인 다형적 참조와 메서드 오버라이딩에 대해 학습했다.

개념설명
다형적 참조하나의 변수 타입으로 다양한 자식 인스턴스를 참조할 수 있는 기능. 부모는 자식을 담을 수 있다.
메서드 오버라이딩기존 기능을 하위 타입에서 새로운 기능으로 재정의. 오버라이딩 된 메서드는 항상 우선권을 가진다.
업캐스팅자식 → 부모 타입으로 변환. 자동으로 처리되며 생략 권장.
다운캐스팅부모 → 자식 타입으로 변환. 명시적으로 캐스팅 필요. 잘못하면 ClassCastException 발생.
instanceof참조 변수가 어떤 인스턴스를 가리키는지 확인. 자바 16부터 Pattern Matching 지원.

이 둘을 이해하고 나면 진정한 다형성의 위력을 맛볼 수 있다. 다음 시간에는 지금까지 학습한 이론들이 다형성에 어떻게 활용되는지 다형성의 힘을 예제를 통해 알아보자.

'Study > Java' 카테고리의 다른 글

[Java] 다형성 Part.2  (0) 2026.04.09
[Java] 상속  (0) 2026.04.09
[Java] final  (0) 2026.04.07
[Java] 자바의 메모리 구조  (0) 2026.04.06
[Java] 접근 제한자  (0) 2026.04.06