'김영한의 실전 자바 - 기본편' 강의를 들으면서 복습할만한 내용을 정리하였다.

11. 다형성 1

11.1 다형성

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

다형성(Polymorphism)은 이름 그대로 다양한 형태, 여러 형태 를 뜻한다.

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


11.2 다형적 참조

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

다형적 참조 : 부모 타입의 변수가 자식 인스턴스 참조

System.out.println("Parent -> Child");
Parent poly = new Child();
poly.parentMethod();

  • 부모 타입의 변수가 자식 인스턴스를 참조한다.
  • Parent poly = new Child()
  • Child 인스턴스를 만들었다. 이 경우 자식 타입인 Child를 생성했기 때문에 메모리 상에 ChildParent 가 모두 생성된다.
  • 생성된 참조값을 Parent 타입의 변수인 poly 에 담아둔다.

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

  • 부모 타입은 자식 타입을 담을 수 있다.
  • Parent poly 는 부모 타입이다. new Child() 를 통해 생성된 결과는 Child 타입이다. 자바에서 부모 타입은 자식 타입을 담을 수 있다.
  • 반대로 자식 타입은 부모 타입을 담을 수 없다.
    • Child child = new Parent() : 컴파일 오류 발생

다형적 참조

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

다형적 참조의 한계

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


11.3 다형성과 캐스팅

Parent poly = new Child() 와 같이 부모 타입의 변수를 사용하게 되면 poly.childMethod() 와 같이 자식 타입에 있는 기능은 호출할 수 없다.

다운캐스팅

호출하는 타입을 자식인 Child 타입으로 변경하면 인스턴스의 Child 에 있는 childMethod() 를 호출할 수 있다. 하지만 다음과 같은 문제에 봉착한다.

부모는 자식을 담을 수 있지만 자식은 부모를 담을 수 없다.

Child child = (Child) poly

(타입) 처럼 괄호와 그 사이에 타입을 지정하면 참조 대상을 특정 타입으로 변경할 수 있다. 이렇게 특정 타입으로 변경하는 것을 캐스팅이라 한다.

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

캐스팅

  • 업캐스팅(upcasting) : 부모 타입으로 변경
  • 다운캐스팅(downcasting) : 자식 타입으로 변경

캐스팅 용어

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

일시적 다운 캐스팅

Child child = (Child) poly
child.childMethod();

다운캐스팅 결과를 변수에 담아두는 과정이 번거롭다. 이런 과정 없이 일시적으로 다운캐스팅을 해서 인스턴스에 있는 하위 클래스의 기능을 바로 호출할 수 있다.

// 일시적 다운캐스팅 - 해당 메서드를 호출하는 순간만 다운캐스팅
((Child) poly).childMethod();

polyParent 타입이다. 그런데 위의 코드를 실행하면 Parent 타입을 임시로 Child 로 변경한다. 그리고 메서드를 호출할 때 Child 타입에서 찾아서 실행한다.

업캐스팅

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

Child child = new Child();
Parent parent1 = (Parent) child; //업캐스팅은 생략 가능, 생략 권장
Parent parent2 = child; //업캐스팅 생략

업캐스팅은 생략할 수 있다. 다운캐스팅은 생략할 수 없다. 참고로 업캐스팅은 매우 자주 사용하기 때문에 생략을 권장한다.


11.4 다운캐스팅과 주의점

다운캐스팅은 잘못하면 심각한 런타임 오류가 발생할 수 있다.

Parent parent2 = new Parent();
Child child2 = (Child) parent2; // 런타임 오류 - ClassCastException
child2.childMethod(); // 실행 불가

다운캐스팅이 가능한 경우

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

다운캐스팅이 가능한 경우와 불가능한 경우의 차이점은 new Parent()에 있다.

Parent parent2 = new Parent()

먼저 new Parent() 로 부모 타입으로 객체를 생성한다. 따라서 메모리 상에 자식 타입은 전혀 존재하지 않는다. 생성 결과는 parent2에 담아둔다. 이 경우 같은 타입이므로 여기서는 문제가 발생하지 않는다.

Child child2 = (Child) parent2

다음으로 parent2Child 타입으로 다운캐스팅한다. 그런데 parent2Parent로 생성이 되었다. 따라서 메모리 상에 Child 자체가 존재하지 않는다. Child 자체를 사용할 수 없는 것이다.

자바에서는 이렇게 사용할 수 없는 타입으로 다운캐스팅하는 경우에 ClassCastException 이라는 예외를 발생시킨다. 예외가 발생하면 다음 동작이 실행되지 않고, 프로그램이 종료된다. 따라서 child2.childMethod() 코드 자체가 실행되지 않는다.

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

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

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

new B() 로 인스턴스를 생성하면 인스턴스 내부에 자신과 부모인 A, B 가 생성된다. 따라서 B 의 부모 타입인 A, B 모두 B 인스턴스를 참조할 수 있다. 상위로 올라가는 업캐스팅은 인스턴스 내부에 부모가 모두 생성되기 때문에 문제가 발생하지 않는다. 하지만 객체를 생성할 때 하위 자식은 생성되지 않기 때문에 하위로 내려가는 다운캐스팅은 인스턴스 내부에 없는 부분을 선택하는 문제가 발생할 수 있다.

C c = (C) new B() : 하위 타입으로 강제 다운캐스팅, 하지만 B 인스턴스에 C 와 관련되 부분이 없으므로 잘못된 캐스팅, ClassCastException 런타임 오류 발생

컴파일 오류 vs 런타임 오류

컴파일 오류는 변수명 오타, 잘못된 클래스 이름 사용 등 자바 프로그램을 실행하기 전에 발생하는 오류이다. 이런 오류는 IDE에서 즉시 확인할 수 있기 때문에 안전하고 좋은 오류이다.

반면에 런타임 오류는 이름 그대로 프로그램이 실행되고 있는 시점에 발생하는 오류이다. 런타임 오류는 매우 안좋은 오류이다. 왜냐하면 보통 고객이 해당 프로그램을 실행하는 도중에 발생하기 때문이다.


11.5 instanceof

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

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

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

private static void call(Parent parent) {
	parent.parentMethod();
 	if (parent instanceof Child) {
 		System.out.println("Child 인스턴스 맞음");
 		Child child = (Child) parent;
        child.childMethod();
	}
}

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

instanceof 키워드의 값은 왼쪽에 오는 값이 오른쪽에 있는 값에 대입이 되는지 보면 된다. 즉, 자식 instanceof 부모 이면 true를 반환한다. 또한, A instanceof Atrue를 반환한다. parent instanceof Childfalse다.

자바 16 - Pattern Matching for instanceof

자바 16부터는 instanceof 를 사용하면서 동시에 변수를 선언할 수 있다.

private static void call(Parent parent) {
	parent.parentMethod();
 	//Child 인스턴스인 경우 childMethod() 실행
	if (parent instanceof Child child) {
 		System.out.println("Child 인스턴스 맞음");
        child.childMethod();
	}
}

parent instanceof Childtrue 인 경우 바로 child 변수에 대입을 해줘서 인스턴스가 맞는 경우 직접 다운캐스팅 하는 코드를 생략할 수 있다.

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

오버라이딩 된 메서드가 항상 우선권을 가진다.

위의 상속 구조를 가지고 ChildParentmethod() 를 오버라이딩 한 상황이다. 멤버 변수인 value는 오버라이딩 되지 않는다. 메서드는 오버라이딩 된다.

Parent poly = new Child();
System.out.println("Parent -> Child");
System.out.println("value = " + poly.value); //변수는 오버라이딩X
poly.method(); //메서드 오버라이딩!
  • 실행 결과
Parent -> Child
value = parent
Child.method

poly 변수는 Parent 타입이다. 따라서 Poly.value, poly.method()를 호출하면 인스턴스의 Parent 타입에서 기능을 먼저 찾아서 실행한다.

  • poly.value : Parent 타입에 있는 value 값을 읽는다.
  • poly.method() : Parent 타입에 있는 method()를 실행하려고 한다. 그런데 하위 타입인 Child.method() 가 오버라이딩 되어 있다. 오버라이딩 된 메서드는 항상 우선권을 가진다. 따라서 Parent.method() 가 아니라 Child.method()가 실행된다.

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

정리

다형적 참조 : 하나의 변수 타입으로 다양한 자식 인스턴스를 참조할 수 있는 기능

메서드 오버라이딩 : 기존 기능을 하위 타입에서 새로운 기능으로 재정의


profile
가오리의 개발 이야기

0개의 댓글