본문으로 건너뛰기
안동민 개발노트 아이콘

안동민 개발노트

본문 시작
8장 : 상속과 다형성

오버라이딩과 동적 디스패치

상위 타입 변수에 하위 객체를 담으면 호출 가능한 메서드는 변수의 정적 타입으로 검사됩니다.

실제로 실행할 재정의 메서드는 런타임 객체 타입에 따라 선택됩니다.

이것이 같은 호출 코드가 객체마다 다른 동작을 내는 다형성의 핵심입니다.

필드와 static 메서드는 인스턴스 메서드처럼 동적 디스패치되지 않습니다.

이 절에서는 컴파일 시점 선택과 실행 시점 선택을 분리합니다.


@Override가 재정의 의도를 검사한다

메서드 이름이나 매개변수를 잘못 쓰면 새 메서드가 생길 수 있습니다.

@Override를 붙이면 상위 선언과 정확히 맞는지 컴파일러가 확인합니다.


호출 가능성은 참조의 정적 타입이 정한다

Animal 변수에서는 Animal이 선언한 메서드만 바로 호출할 수 있습니다.

실제 객체가 Dog여도 Dog 전용 메서드는 패턴 검사나 타입 설계가 필요합니다.

DynamicDispatch.java는 변수 선언은 Message로 유지하면서 실제 객체로 Warning을 넣습니다. formatcolor의 호출 가능 여부와 실행 구현이 서로 다른 단계에서 결정되는 이유를 비교합니다.

DynamicDispatch.java
public class DynamicDispatch {
    static class Message {
        String format() { return "message"; }
    }
    static final class Warning extends Message {
        @Override String format() { return "warning"; }
        String color() { return "amber"; }
    }

    static void print(Message message) {
        System.out.println(message.format());
    }

    public static void main(String[] args) {
        Message message = new Warning();
        print(message);
        if (message instanceof Warning warning) {
            System.out.println(warning.color());
        }
        // message.color(); // 컴파일 오류: 정적 타입 Message에 선언되지 않음
    }
}

상위 타입 변수로는 상위 타입에 선언된 메서드만 호출할 수 있습니다. 실제 객체가 하위 메서드를 더 가지고 있어도 컴파일러는 참조의 정적 타입이 보장하지 않는 호출을 허용하지 않습니다.

warning
amber

format은 실제 Warning 구현이 실행되지만 color 호출 가능성은 Message라는 정적 타입으로 검사됩니다.


실행 구현은 실제 객체 타입이 고른다

재정의된 인스턴스 메서드는 런타임의 가장 구체적인 구현이 실행됩니다.

호출자가 하위 타입별 if를 늘리지 않고 새 구현을 추가할 수 있습니다.

호출 자체가 가능하다고 결정된 뒤에는 실제 객체의 오버라이딩 구현이 실행됩니다. 같은 상위 타입 배열에 서로 다른 하위 객체를 넣어 한 메서드를 호출하면 객체마다 다른 출력이 나오는 것이 동적 디스패치입니다.

비교 1
class Parent {
    static String staticName() { return "parent"; }
    String instanceName() { return "parent"; }
}
class Child extends Parent {
    static String staticName() { return "child"; }
    @Override String instanceName() { return "child"; }
}
비교 2
Parent value = new Child();
System.out.println(value.staticName());   // parent: 정적 타입
System.out.println(value.instanceName()); // child: 실제 객체

다운캐스트는 타입 정보가 사라진 경계를 드러낸다

정말 하위 전용 동작이 필요하면 instanceof 패턴으로 검사한 뒤 사용합니다.

반복적인 캐스트는 상위 계약에 필요한 동작이 빠졌다는 신호일 수 있습니다.

다운캐스트 전에 instanceof 패턴으로 실제 타입과 필요한 값을 함께 확인해 보세요. 캐스팅이 곳곳에 반복된다면 공통 동작이 상위 타입에 빠졌거나 컬렉션이 지나치게 넓은 타입을 담고 있다는 설계 신호일 수 있습니다.


다형성은 상위 타입으로 호출을 고정하고 실제 객체가 구현을 선택하게 해 조건 분기를 객체 경계로 옮깁니다.