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

안동민 개발노트

본문 시작
7장 : 상속·오버라이딩·다형성

다형성과 캐스팅

부모 참조를 자식 변수에 바로 대입할 수 없는 이유와 명시적 다운캐스팅이 참조 관점만 바꾼다는 사실을 확인합니다.

부모 타입 참조는 여러 자식 객체를 담을 수 있어 유연하지만 자식 전용 멤버는 보이지 않습니다.

실제 객체가 특정 자식임을 알고 그 행동이 꼭 필요할 때 다운캐스팅으로 참조의 정적 타입을 좁힐 수 있습니다.

캐스팅은 새 객체를 만들거나 기존 객체의 본질을 바꾸지 않습니다.


다운캐스팅 컴파일 오류

lab/ParentToChildAssignment.java
public final class ParentToChildAssignment {
    public static void main(String[] args) {
        Post post = new CodePost();
        CodePost codePost = post;
        codePost.commit();
    }

    private static class Post {}

    private static final class CodePost extends Post {
        void commit() {
            System.out.println("commit");
        }
    }
}
컴파일 실패 관찰
error: incompatible types: Post cannot be converted to CodePost

post 변수의 타입만 보면 Post 자체, TextPost, CodePost 등 어느 하위 객체든 담길 수 있습니다.

컴파일러가 항상 CodePost라고 보장할 수 없으므로 자동 대입을 막습니다.

실제 초기화 식을 사람이 볼 수 있어도 변수는 이후 다른 객체를 받을 수 있다는 타입 계약을 따릅니다.

명시적 캐스트로 개발자가 조건을 책임질 수 있습니다.

CodePost codePost = (CodePost) post;

실행 시 실제 객체가 CodePost 호환인지 검사하고, 맞으면 같은 참조값을 자식 타입 변수에 담습니다.


올바른 다운캐스팅

src/ValidDowncast.java
public final class ValidDowncast {
    public static void main(String[] args) {
        CodePost original = new CodePost("feature/login");
        Post parentView = original;

        parentView.publish();
        CodePost childView = (CodePost) parentView;
        childView.commit();

        System.out.println("same=" + (original == childView));
    }

    private static class Post {
        void publish() {
            System.out.println("publish");
        }
    }

    private static final class CodePost extends Post {
        private final String branch;

        CodePost(String branch) {
            this.branch = branch;
        }

        void commit() {
            System.out.println("commit=" + branch);
        }
    }
}
publish
commit=feature/login
same=true

original, parentView, childView는 모두 같은 객체를 가리킵니다.

업캐스팅 때 CodePost 부분이 제거되지 않았고 다운캐스팅 때 새로 생기지도 않았습니다.

변수의 정적 타입이 제공하는 멤버 목록만 달라집니다.


일회성 캐스트 표현식

자식 변수를 별도로 만들지 않고 한 호출에만 다운캐스팅할 수 있습니다.

src/TemporaryDowncast.java
public final class TemporaryDowncast {
    public static void main(String[] args) {
        Post post = new CodePost();

        ((CodePost) post).commit();
        post.publish();
    }

    private static class Post {
        void publish() {
            System.out.println("publish");
        }
    }

    private static final class CodePost extends Post {
        void commit() {
            System.out.println("commit");
        }
    }
}
commit
publish

괄호가 필요한 이유는 멤버 접근보다 캐스트 범위를 먼저 묶기 위해서입니다.

캐스트 결과 표현식만 CodePost 타입이고 원래 post 변수의 선언 타입은 계속 Post입니다.

다음 줄에서 post.commit()은 여전히 컴파일되지 않습니다.

일시적 캐스팅은 한 번만 사용할 때 간단하지만 같은 자식 기능을 반복 호출한다면 자식 타입 지역 변수로 이름을 붙이는 편이 읽기 쉽습니다.


캐스팅과 설계 신호

다형적 배열을 순회하며 매번 다음처럼 타입별 캐스팅을 한다면 공통 계약이 부족할 수 있습니다.

if (post instanceof CodePost) {
    ((CodePost) post).commit();
} else if (post instanceof TextPost) {
    ((TextPost) post).previewBody();
}

각 게시글이 수행해야 할 공통 목적이 게시판 발행이라면 부모의 publish()를 오버라이딩하는 편이 낫습니다.

반면 commit이 코딩 게시글에만 의미 있고 특정 관리 화면에서만 필요하다면 안전한 타입 검사와 캐스팅이 정당할 수 있습니다.

캐스팅은 타입 설계의 빈틈을 메우는 기본 도구가 아니라, 실제 구체 타입의 기능이 필요한 제한된 경계에서 사용합니다.

클라이언트가 구체 타입을 많이 알수록 새 하위 타입 추가 시 분기를 수정해야 합니다.


하위 타입 정보 접근

src/PostDetails.java
public final class PostDetails {
    public static void main(String[] args) {
        Post[] posts = {
                new CodePost("로그인 구현", "feature/login"),
                new TextPost("가입 인사", 120)
        };

        for (Post post : posts) {
            System.out.println(post.title());
            if (post instanceof CodePost) {
                CodePost codePost = (CodePost) post;
                System.out.println("branch=" + codePost.branch());
            }
        }
    }

    private static class Post {
        private final String title;

        Post(String title) {
            this.title = title;
        }

        String title() {
            return title;
        }
    }

    private static final class CodePost extends Post {
        private final String branch;

        CodePost(String title, String branch) {
            super(title);
            this.branch = branch;
        }

        String branch() {
            return branch;
        }
    }

    private static final class TextPost extends Post {
        private final int bodyLength;

        TextPost(String title, int bodyLength) {
            super(title);
            this.bodyLength = bodyLength;
        }
    }
}
로그인 구현
branch=feature/login
가입 인사

타입 검사 없이 둘째 TextPost를 CodePost로 캐스팅하면 실행 중 ClassCastException이 발생합니다.

다음 문서에서 업캐스팅이 안전한 이유와 다운캐스팅의 실패 조건을 더 정확히 추적합니다.


연습 문제

부모 타입 변수에 VideoPost 객체를 담고, 올바르게 다운캐스팅해 addCaption(String)을 호출하세요.

원래 부모 변수의 타입은 바뀌지 않는다는 것을 주석과 출력으로 확인합니다.

해설 보기
src/VideoPostCast.java
public final class VideoPostCast {
    public static void main(String[] args) {
        Post post = new VideoPost();
        VideoPost video = (VideoPost) post;

        video.addCaption("ko");
        post.publish();
    }

    private static class Post {
        void publish() {
            System.out.println("publish");
        }
    }

    private static final class VideoPost extends Post {
        void addCaption(String language) {
            System.out.println("caption=" + language);
        }
    }
}
caption=ko
publish

video와 post는 같은 객체를 가리키지만 각 변수에서 컴파일 가능한 멤버 범위가 다릅니다.