[김영한의 실전 자바 기본편] - 다형성1

jkky98·2024년 5월 2일
0

Java

목록 보기
6/51

다형성의 핵심 2가지

이번 챕터는 "이 개념이 왜 필요한가?"에 대한 예측이 어려운 챕터였다.

다형성의 필요성을 이해하기 위해 우선 핵심 2가지에 대한 작동 방식을 이해해야한다. 이번 포스팅은 다형성의 필요성을 이해하기 위한 중요한 개념적 톱니바퀴들의 작동방식을 이해하는 것이다. 이해해야할 핵심 소개념은 아래와 같다.

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

다형적 참조

// 부모 클래스 Parent, 자식 클래스 Child
Parent poly = new Child();
poly.childMethod(); // 호출 불가
poly.parentMethod(); // 호출 가능

위 예시는 다형적 참조의 대표적인 형태이다.

보통 우리는 인스턴스 생성시 변수의 타입과 생성자의 클래스를 동일하게 지정해왔다.

상속관계가 성립한다면, 변수의 타입은 부모 클래스로 지정하고 인스턴스는 자식 클래스로 생성하는 것이 가능해진다.

poly의 타입이 Parent이기 때문에 실제 인스턴스가 Child로 생성되었더라도 poly는 Parent클래스에 정의된 메서드, 필드에만 접근할 수 있다.

여기서 알 수 있는 것은 부모 타입은 자식 타입을 담을 수 있다는 것이다.

Parent poly는 Parent타입인데 Child타입을 담으려고 한다. 만약 Child클래스에 자식으로 GrandChild가 존재한다면 GrandChild타입도 담을 수 있다.

"김씨 가문(type)의 한 사람으로서 김유민(class)"이라고 표현할 수도 있고, "김유민" 그 자체로 표현할 수도 있다.
즉, 김유민은 '김씨 가문'이라는 넓은 범주(부모 타입)에서도 바라볼 수 있지만, 동시에 '김유민'이라는 구체적인 존재(자식 클래스)로도 바라볼 수 있다.

캐스팅

위 코드에서 childMethod()는 호출할 수 없었다.

그 이유는 poly가 Parent 타입으로 선언되어 있었기 때문이다. 부모 타입으로 선언된 객체는 부모 클래스에 정의된 메서드만 호출할 수 있다.

그러나 캐스팅을 통해 Parent 타입을 Child 타입으로 변환하면 자식 클래스에 정의된 메서드도 호출할 수 있다.

현재 poly는 Parent 타입으로 선언되어 있으나, 실제로는 Child 객체를 참조하고 있다. 따라서 poly를 Child 타입으로 캐스팅하면 Child 클래스에 정의된 메서드인 childMethod()를 호출할 수 있다. 이 과정을 통해 부모 타입의 참조를 자식 타입으로 변환하여 자식 클래스의 기능을 사용할 수 있게 되는 것이다.

캐스팅 후에는 컴파일러가 자식 클래스의 메서드와 필드 접근을 허용하게 되므로, poly.childMethod() 호출이 가능해진다.

다운 캐스팅

Parent 타입의 poly를 Child 타입으로 변환하는 것은 객체의 타입을 자식 클래스 타입으로 바꾼다는 의미다. 이러한 타입 변환을 다운캐스팅이라고 한다. 다운캐스팅은 부모 타입에서 자식 타입으로 변환하는 작업을 말하며, 이는 마치 우리가 숫자를 변환할 때 (int) 3.14와 같은 형변환을 했던 것과 유사하다.

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

위 코드에서는 poly를 Child 타입으로 캐스팅하여 새로운 변수 child에 저장하고, 이를 통해 childMethod()를 호출했다. 다운캐스팅을 통해 자식 클래스의 메서드를 호출할 수 있다.

일시적 다운 캐스팅
변수를 생성하지 않고, 일시적으로 다운캐스팅하여 바로 메서드를 호출하는 방법도 있다. 예를 들어:

((Child) poly).childMethod();

위 코드는 (Child) poly를 괄호로 한 번 더 감싸서 일시적으로 poly를 Child 타입으로 변환한 뒤, childMethod()를 호출한 것이다. 이 방식은 별도의 변수를 만들지 않고 즉석에서 자식 클래스의 메서드를 사용할 수 있다.

업 캐스팅

업 캐스팅은 다운 캐스팅의 반대방향이다. 만약 Child child = new Child();라는 인스턴스를 생성했고 Child 클래스의 부모로 Parent가 존재한다면

Parent parent = (Parent) child 와 같이 parent가 타입이 Parent로 변화된 child인스턴스를 받을 수 있다는 것이다.

업 캐스팅시 (Parent)는 생략이 가능하며, 실제로 생략해서 사용하는 경우가 대부분이다.

업 캐스팅은 생략할 수 있고 다운 캐스팅은 생략할 수 없다.

다운 캐스팅에서 명시적인 형변환을 요구하는 이유는 프로그래머에게 주의를 환기시키고 잠재적인 런타임 오류를 방지하기 위함이다. 다운 캐스팅은 상위 클래스의 객체를 하위 클래스 타입으로 변환하려는 시도로, 잘못된 형변환이 발생하면 ClassCastException이 발생할 수 있다. 반면 업 캐스팅은 하위 클래스를 상위 클래스 타입으로 변환하는 것으로, 객체의 본질을 바꾸지 않고 안전하게 수행되기 때문에 명시적인 형변환 없이도 이루어질 수 있다. 이러한 차이는 두 경우의 안전성 차이에서 기인하며, 다운 캐스팅의 명시적 표기는 코드 작성 시 프로그래머가 더 신중하게 검토하도록 유도하기 위함이다.

Child child = new Child();

위의 코드는 상속 개념에 따라 Child와 Parent 인스턴스가 메모리상에 동시에 생성된다. 이를 통해 어떤 클래스를 생성하더라도 상속 관계에 있는 모든 부모 클래스의 인스턴스가 메모리상에 함께 생성된다는 것을 알 수 있다. 따라서 업캐스팅은 안전하게 수행되며 별다른 문제가 발생하지 않는다.

그러나 다운캐스팅의 경우, 메모리에 존재하는 인스턴스는 Child와 Parent뿐이므로, 실제로 존재하지 않는 하위 클래스의 인스턴스로 변환하려고 하면 런타임 오류가 발생할 수 있다. 이러한 이유로 다운캐스팅은 더욱 신중하게 다루어야 한다.

Parent poly = new Child();

위 코드는 다형성을 활용한 참조로, 다운캐스팅이 가능하다.

이는 poly를 생성할 때 메모리에 Parent와 Child 두 클래스의 인스턴스가 모두 포함되기 때문이다. 그러나 다운캐스팅은 잘못된 형변환 시 런타임 에러가 발생할 위험이 있기 때문에, Java는 명시적으로 캐스팅을 지정하도록 설계되었다. 만약 poly가 참조하는 인스턴스가 어떤 클래스인지 확인하고 싶다면, instanceof 연산자를 활용할 수 있다.

instanceof

poly instanceof Parent // true
poly instanceof Child // true
child instanceof Parent // false
child instanceof Child // true

insatanceof키워드는 위처럼 사용되며, boolean을 리턴한다. 해석은 왼쪽 부분이 오른쪽 부분의 메모리 구조에 포함되느냐?로 해석하면 좋을 듯 하다. poly는 Parent이므로 당연히 참이며 poly의 타입인 Parent는 Child클래스로 생성시 Parent도 메모리에 생기기에 참이다.

반면 child의 타입은 Child이므로 메모리 안에서 Parent클래스로 인해 만들어지지 않기에 false이다.

런타임 오류 vs 컴파일 오류

컴파일 오류는 문법적인 오류등 JVM실행전 발생하는 오류로 IDE에서 발생하자마자 실행하기도 전에 빨간줄을 그어주는 등 즉시 확인가능한 오류이다. 그러나 런타임 오류는 프로그램이 실행되고 있는 상황에서 발생하는 오류기에 상대적으로 더 치명적이다.

메서드 오버라이딩

메서드 오버라이딩은 이전 상속에 관한 포스팅에서 소개했다.

// Parent
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");
    }
// main
        // 다형적 참조
        Parent poly = new Child();
        System.out.println("Parent -> Child");
        System.out.println("value = " + poly.value); //변수는 오버라이딩X
        poly.method(); //메서드 오버라이딩!

다음과 같이 구성했을 때 poly의 타입은 Parent이다. poly의 .method()를 호출할 때 오버라이딩에 의해 강제적으로 자식 클래스의 메서드가 호출된다.(필드 오버라이딩은 존재X) 만약 자식 클래스가 더 깊이 있어, 그곳에도 동일하게 오버라이딩 되어있다면 가장 아랫쪽의 자식 메서드가 실행된다.

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

지금까지의 강의에서 코드는 필요성과 목적이 명확했지만, 이번에는 필요성을 이해하는 것이 조금 어렵게 느껴진다. 다만, 이를 형변환 개념과 연결해 생각해볼 수 있었다. 기초 수업에서 배웠던 내용 중, int 타입의 값 2가 double로 자동 형변환될 때 2.0으로 변환되지만, 반대로 double을 int로 바꿀 때는 명시적으로 (int)를 사용해야 한다는 점이 떠올랐다. 이는 참조형은 아니지만, 다운캐스팅과 업캐스팅의 개념과 유사하게 이해할 수 있다. double이 더 많은 값을 표현할 수 있기 때문에 int에서 double로의 변환은 비교적 자유롭지만, double에서 int로의 변환은 소숫점 이하를 버리는 손실이 발생하기 때문에 명시적 형변환이 필요하다는 점에서 그렇다.

profile
자바집사의 거북이 수련법

0개의 댓글