본문 바로가기

부모 타입 변수로 자식 객체 다루기

부모 타입 변수가 자식 객체를 가리킬 수 있는 이유와, 그때 변수의 타입이 호출할 수 있는 메서드를 정하고 실제 객체가 실행할 구현을 정한다는 규칙을 배웁니다.

앞 글의 마지막 예제에서 Post 배열에 TextPost와 CodePost를 함께 담고, 반복문 안에서 Post 타입 변수 하나로 두 객체의 publish()를 호출했습니다. 변수에는 Post라고 적혀 있는데 객체는 TextPost나 CodePost입니다. 변수의 타입과 객체의 클래스가 같지 않아도 되는 것입니다.

이렇게 한 객체를 자기 클래스뿐 아니라 부모 타입으로도 다룰 수 있는 성질을 다형성(polymorphism)이라고 합니다. '여러 형태'라는 뜻인데, CodePost 객체는 CodePost이면서 동시에 Post이기도 하다는 의미입니다. 부모 타입 변수가 자식 객체를 가리키는 것을 다형적 참조라고 부르며, 이 글에서 살펴볼 주제입니다. 다형적 참조를 쓰면 게시글 종류마다 코드를 따로 쓰지 않고 Post만 다루는 코드 하나로 모든 게시글을 처리할 수 있습니다. 대신 대가도 있습니다. 변수가 Post 타입이면 객체가 실제로 CodePost여도 CodePost에만 있는 메서드는 부를 수 없습니다. 이 글에서는 무엇이 되고 무엇이 안 되는지, 그리고 왜 그런지를 차례로 확인합니다.

부모 타입 변수에 자식 객체 담기

먼저 같은 Post 타입 변수 세 개에 각각 Post, TextPost, CodePost 객체를 담아 보겠습니다. 이 예제의 두 자식은 publish를 오버라이딩하지 않고 부모의 것을 그대로 물려받습니다.

src/PolymorphicReferences.java
public final class PolymorphicReferences {
    public static void main(String[] args) {
        Post base = new Post("공지");
        Post textPost = new TextPost("가입 인사");
        Post codePost = new CodePost("로그인 구현");

        base.publish();
        textPost.publish();
        codePost.publish();
    }

    private static class Post {
        private final String title;

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

        void publish() {
            System.out.println("board=" + title);
        }
    }

    private static final class TextPost extends Post {
        TextPost(String title) {
            super(title);
        }
    }

    private static final class CodePost extends Post {
        CodePost(String title) {
            super(title);
        }
    }
}
board=공지
board=가입 인사
board=로그인 구현

세 변수의 타입은 모두 Post이지만 가리키는 객체의 클래스는 Post, TextPost, CodePost로 서로 다릅니다. 두 자식이 publish를 오버라이딩하지 않았으므로 세 호출 모두 Post의 publish 코드를 실행합니다. 출력이 다른 것은 같은 코드가 객체마다 다른 title을 읽었기 때문입니다.

세 Post 참조의 서로 다른 대상과 같은 부모 구현

PolymorphicReferences에서 base, textPost, codePost의 정적 타입은 모두 Post지만 실제 객체 타입은 Post, TextPost, CodePost다. 이 프로그램의 두 자식은 publish를 오버라이딩하지 않아 세 호출 모두 Post.publish를 실행한다.

다른 객체를 가리켜도 같은 부모 publish 구현을 사용할 수 있습니다

PolymorphicReferences에서는 TextPost와 CodePost가 publish()를 오버라이딩하지 않습니다.

정적 타입이 모두 Post인 세 참조와 실제 publish 출력
변수의 정적 타입실제 객체 타입실행 구현과 출력
Post basePostPost.publish()

board=공지

Post textPostTextPostPost.publish()

board=가입 인사

Post codePostCodePostPost.publish()

board=로그인 구현

다른 객체를 가리켜도 같은 부모 publish 구현을 사용할 수 있습니다 Post정적타입base/textPost/codePost는별개Post/TextPost/CodePost 3객체참조;오버라이딩없어동일Post.publish코드사용,각title로board공지/가입인사/로그인구현.실선참조/점선공통구현사용. Post base 참조 변수 Post 객체 title: 공지 board=공지 Post textPost 참조 변수 TextPost 객체 title: 가입 인사 board=가입 인사 Post codePost 참조 변수 CodePost 객체 title: 로그인 구현 board=로그인 구현 Post.publish 공통 코드 오버라이딩 없음 실선은 객체 참조 · 점선은 같은 부모 구현 사용 board= 출력은 각 객체의 title을 읽은 결과
다른 객체를 가리켜도 같은 부모 publish 구현을 사용할 수 있습니다 Post정적타입base/textPost/codePost는별개Post/TextPost/CodePost 3객체참조;오버라이딩없어동일Post.publish코드사용,각title로board공지/가입인사/로그인구현.실선참조/점선공통구현사용. 별개 대상 1 Post base 참조 변수 Post 객체 title: 공지 board=공지 별개 대상 2 Post textPost 참조 변수 TextPost 객체 title: 가입 인사 board=가입 인사 별개 대상 3 Post codePost 참조 변수 CodePost 객체 title: 로그인 구현 board=로그인 구현 Post.publish 코드 세 객체가 부모 구현 사용 객체마다 title을 읽음 오버라이딩 없음 실선: 참조 → 다른 객체 점선: 같은 구현 사용
  1. Post base

    실제 Post 객체에서 Post.publish()를 실행하여 board=공지를 출력합니다.

  2. Post textPost

    실제 TextPost 객체도 Post.publish()를 실행하여 board=가입 인사를 출력합니다.

  3. Post codePost

    실제 CodePost 객체도 Post.publish()를 실행하여 board=로그인 구현을 출력합니다.

세 호출은 Post에 선언된 publish() 계약으로 컴파일됩니다. 실제 객체가 자식 타입이어도 오버라이딩이 없으면 부모 구현을 사용합니다. 앞의 PolymorphicReferenceLimit은 별도 프로그램이며, 그 예제의 post.commit()은 Post에 없는 멤버라 컴파일되지 않습니다.

Post textPost = new TextPost("가입 인사");가 허용되는 이유는 앞 글에서 본 객체 구조에 있습니다. TextPost 객체 안에는 Post가 선언한 필드가 모두 들어 있고, Post의 메서드도 모두 물려받았습니다. 그래서 Post로 할 수 있는 일은 TextPost 객체에도 언제나 할 수 있고, 이 대입이 잘못될 여지가 없습니다. 부모 타입 변수에는 자식뿐 아니라 손자처럼 더 아래에 있는 모든 하위 클래스의 객체를 담을 수 있습니다.

반대 방향은 허용되지 않습니다. CodePost codePost = new Post("공지");라고 쓰면 incompatible types: Post cannot be converted to CodePost라는 컴파일 오류가 납니다. Post 객체에는 CodePost가 덧붙인 필드도, 메서드도 없기 때문입니다. CodePost 변수를 믿고 자식에만 있는 메서드를 호출하면 실행할 코드가 없게 됩니다.

업캐스팅은 객체를 바꾸지 않는다

자식 타입의 값을 부모 타입으로 다루는 것을 업캐스팅(upcasting)이라고 합니다. 상속 관계를 그릴 때 부모를 위에 두므로 위쪽으로 올라간다는 뜻의 이름이 붙었습니다. 업캐스팅은 언제나 안전하므로 (Post) 같은 형변환 표시 없이 대입만으로 일어납니다.

CodePost child = new CodePost("로그인 구현");
Post parentView = child;

둘째 줄이 실행되어도 새 객체가 생기거나 CodePost 객체가 Post 객체로 바뀌지 않습니다. 기본형과 참조형 복사에서 본 것처럼 대입은 변수에 든 값을 복사하는데, 참조형 변수에 든 값은 객체를 가리키는 참조값입니다. 그래서 child와 parentView에는 같은 참조값이 들어 있고, child == parentView는 true입니다. 달라지는 것은 그 객체를 어떤 타입으로 다루느냐, 즉 그 변수로 호출할 수 있는 메서드의 범위뿐입니다.

변수 타입에 없는 메서드는 호출할 수 없다

그 범위가 실제로 어떻게 제한되는지 보겠습니다. 다음 코드는 CodePost 객체를 Post 변수에 담은 뒤, CodePost에만 있는 commit()을 호출합니다.

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

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

    private static final class CodePost extends Post {
        void commit() {
            System.out.println("commit");
        }
    }
}
PolymorphicReferenceLimit.java:4: error: cannot find symbol
        post.commit();
            ^
  symbol:   method commit()
  location: variable post of type Post
1 error

post가 가리키는 객체는 분명히 CodePost이고, 그 객체에는 commit 메서드가 있습니다. 그런데도 컴파일러는 commit()이라는 메서드를 찾을 수 없다고 합니다. 오류 메시지의 마지막 줄 location: variable post of type Post가 이유를 알려 줍니다. 컴파일러는 post를 Post 타입 변수로 보고 Post 클래스에서 commit을 찾았는데, Post와 그 부모인 Object 어디에도 commit이 없었던 것입니다.

컴파일러가 객체를 보지 않고 변수 타입만 보는 이유는 컴파일하는 시점에는 객체가 아직 없기 때문입니다. 객체는 프로그램을 실행해야 생깁니다. 이 예제는 바로 윗줄에서 new CodePost()를 대입하니 사람이 보기에는 뻔하지만, 변수에는 나중에 얼마든지 다른 객체를 대입할 수 있습니다. Post를 매개변수로 받는 메서드라면 호출하는 쪽에서 TextPost를 넘길 수도 있고, 사용자 입력에 따라 어떤 게시글을 만들지 정할 수도 있습니다. 그래서 컴파일러는 Post 변수에 어떤 하위 클래스의 객체가 담기더라도 반드시 성공하는 호출, 즉 Post에 선언된 메서드만 허락합니다. 자식에만 있는 메서드를 꼭 불러야 할 때 쓰는 방법은 다음 글의 다운캐스팅입니다.

컴파일러는 변수 타입을, 실행은 객체를 본다

지금까지 본 두 가지 사실을 하나로 정리할 수 있습니다. 다음 선언에는 타입이 두 개 등장합니다.

Post post = new CodePost("로그인 구현");
^^^^            ^^^^^^^^
변수의 타입     실제 객체의 클래스

왼쪽 Post는 변수를 선언할 때 정해져 바뀌지 않는 변수의 타입입니다. 소스 코드만 보고 알 수 있으므로 정적 타입(static type) 또는 컴파일 시점 타입이라고도 합니다. 오른쪽 CodePost는 실행 중에 만들어진 객체의 실제 클래스입니다. 같은 변수라도 무엇을 대입했느냐에 따라 달라지므로 런타임 타입이라고도 합니다. 메서드 호출 하나에는 이 두 타입이 단계를 나누어 관여합니다.

  1. 컴파일할 때: 컴파일러가 변수의 타입에서 그 메서드를 찾습니다. 변수 타입이나 그 부모에 메서드가 없거나 접근할 수 없으면 컴파일 오류입니다. 앞의 post.commit()이 여기서 막혔습니다.
  2. 실행할 때: JVM이 변수가 가리키는 실제 객체를 확인하고, 그 객체의 클래스에서부터 부모 쪽으로 올라가며 실행할 구현을 찾습니다. 자식이 메서드를 오버라이딩했다면 자식의 구현이, 그렇지 않다면 물려받은 부모의 구현이 실행됩니다.

PolymorphicReferences에서 세 호출이 모두 Post의 publish를 실행한 것은 2단계에서 찾아 올라간 결과가 모두 Post였기 때문입니다. 자식이 오버라이딩하면 같은 post.publish() 호출이 객체에 따라 다른 코드를 실행하게 됩니다. 이렇게 실행할 구현을 실행 시점에 실제 객체를 보고 고르는 것을 동적 디스패치(dynamic dispatch)라고 하며, 다형성이 쓸모 있는 이유가 바로 이 동작에 있습니다.

이 규칙은 오버라이딩할 수 있는 인스턴스 메서드에 적용됩니다. 필드와 static 메서드는 오버라이딩되지 않으므로, 실행 중인 객체가 아니라 변수의 타입을 기준으로 어느 것을 쓸지 정해집니다. 부모와 자식에 같은 이름의 필드가 있다면 Post 변수로는 부모의 필드가 읽힙니다. 이 차이는 instanceof와 오버라이딩에서 예제로 확인합니다. 지금은 필드를 private으로 숨기고 메서드로만 다루면 이런 혼란이 생길 일이 없다는 것만 기억해 두면 충분합니다.

부모 타입 배열 하나로 여러 게시글 처리하기

두 단계 규칙을 배열 예제로 확인해 보겠습니다. 이번에는 두 자식이 게시글 설명을 만드는 describe()를 오버라이딩합니다. 자식의 구현은 super.describe()로 부모가 만든 설명을 받아 앞에 종류 표시를 붙입니다. 조회 수를 돌려주는 viewCount()는 오버라이딩하지 않습니다.

src/PolymorphicPostArray.java
public final class PolymorphicPostArray {
    public static void main(String[] args) {
        Post[] posts = {
                new TextPost("가입 인사", 40),
                new CodePost("로그인 구현", 50),
                new Post("공지", 20)
        };

        int total = 0;
        for (Post post : posts) {
            System.out.println(post.describe());
            total += post.viewCount();
        }
        System.out.println("total=" + total);
    }

    private static class Post {
        private final String title;
        private final int viewCount;

        Post(String title, int viewCount) {
            this.title = title;
            this.viewCount = viewCount;
        }

        String describe() {
            return "board=" + title;
        }

        int viewCount() {
            return viewCount;
        }
    }

    private static final class TextPost extends Post {
        TextPost(String title, int viewCount) {
            super(title, viewCount);
        }

        @Override
        String describe() {
            return "text=" + super.describe();
        }
    }

    private static final class CodePost extends Post {
        CodePost(String title, int viewCount) {
            super(title, viewCount);
        }

        @Override
        String describe() {
            return "code=" + super.describe();
        }
    }
}
text=board=가입 인사
code=board=로그인 구현
board=공지
total=110

반복문 안의 post 변수는 타입이 Post이므로, 컴파일러는 Post에 선언된 describe()와 viewCount() 호출을 허락합니다. 실행할 때는 원소마다 실제 객체를 보고 구현을 고릅니다. 첫 원소를 처리하는 과정을 단계별로 따라가 보겠습니다.

단계실행하는 코드일어나는 일
1for (Post post : posts)post가 0번 원소인 TextPost 객체를 가리킨다
2post.describe()실제 객체가 TextPost이고 describe를 오버라이딩했으므로 TextPost의 구현으로 들어간다
3super.describe()Post의 구현이 "board=가입 인사"를 돌려준다
4return "text=" + ...TextPost의 구현이 "text=board=가입 인사"를 돌려준다
5System.out.println(...)반복문이 돌려받은 문자열을 출력한다
6total += post.viewCount()TextPost에는 오버라이딩한 구현이 없으므로 Post의 구현이 40을 돌려주고, total이 40이 된다

여기서 메서드를 고르는 일, 문자열을 만들어 돌려주는 일, 출력하는 일은 서로 다른 단계라는 점에 주목하세요. describe()는 출력하지 않고 문자열을 돌려줄 뿐이며, 출력은 호출한 반복문이 합니다. 두 번째 원소도 같은 과정을 거쳐 CodePost의 구현이 code=board=로그인 구현을 만들고, total은 90이 됩니다. 세 번째 원소는 Post 객체이므로 구현을 고를 때 Post의 describe가 바로 선택되어 board=공지가 출력되고, 마지막에 20을 더해 total=110이 됩니다.

부모 배열에서 달라지는 describe와 공유하는 viewCount

배열 원소 0·1·2의 describe 구현 선택과 부모 viewCount 읽기 뒤 total 40→90→110이 됩니다. 메서드 선택·반환·호출자 출력/누산의 시점을 나눌 수 있습니다.

부모 배열에서 달라지는 describe와 공유하는 viewCount

PolymorphicPostArray의 세 원소는 TextPost, CodePost, Post 객체이다. describe는 자식의 오버라이딩과 super.describe를 따르고, viewCount는 모두 부모 구현으로40,50,20을 읽어 total110을 만든다.

describe는 자식별로 달라지고 viewCount는 부모 구현을 씁니다

PolymorphicPostArray는 Post post로 순회하며 describe()와 viewCount()를 호출합니다.

Post 배열의 실제 타입별 describe 출력과 조회 수
배열 원소 / 실제 타입describe()의 구현과 출력viewCount()
posts[0]

TextPost

TextPost.describe()가 super.describe()에 "text="를 붙입니다.

text=board=가입 인사

Post.viewCount()

40

posts[1]

CodePost

CodePost.describe()가 super.describe()에 "code="를 붙입니다.

code=board=로그인 구현

Post.viewCount()

50

posts[2]

Post

Post.describe()

board=공지

Post.viewCount()

20

describe는 자식별로 달라지고 viewCount는 부모 구현을 씁니다 원소0Text/1Code/2Post 순서;describe는자식super결과text=board가입인사/code=board로그인구현/board공지,viewCount모두부모40/50/20,누적40→90→110.표현줄나눔은실제출력줄아님. 0 · TextPost describe() 선택 자식 + super text=board= 가입 인사 Post.viewCount() 반환 40 모두 부모 구현 누적 total 40 1 · CodePost describe() 선택 자식 + super code=board= 로그인 구현 Post.viewCount() 반환 50 모두 부모 구현 누적 total 90 2 · Post describe() 선택 부모 구현 board= 공지 Post.viewCount() 반환 20 모두 부모 구현 누적 total 110 원소 0 → 1 → 2 순서 40 + 50 + 20 = 110
describe는 자식별로 달라지고 viewCount는 부모 구현을 씁니다 원소0Text/1Code/2Post 순서;describe는자식super결과text=board가입인사/code=board로그인구현/board공지,viewCount모두부모40/50/20,누적40→90→110.표현줄나눔은실제출력줄아님. 0 · TextPost describe() 선택 자식 + super text=board= 가입 인사 Post.viewCount() 반환 40 모두 부모 구현 누적 total 40 1 · CodePost describe() 선택 자식 + super code=board= 로그인 구현 Post.viewCount() 반환 50 모두 부모 구현 누적 total 90 2 · Post describe() 선택 부모 구현 board= 공지 Post.viewCount() 반환 20 모두 부모 구현 누적 total 110 원소 0 → 1 → 2 순서 40 + 50 + 20 = 110
  1. posts[0]: TextPost

    TextPost.describe()가 super.describe()에 "text="를 붙여 text=board=가입 인사를 반환합니다. Post.viewCount()는 40을 반환합니다.

  2. posts[1]: CodePost

    CodePost.describe()가 super.describe()에 "code="를 붙여 code=board=로그인 구현을 반환합니다. Post.viewCount()는 50을 반환합니다.

  3. posts[2]: Post

    Post.describe()가 board=공지를 반환하고 Post.viewCount()가 20을 반환합니다.

반복문은 각 describe() 결과를 위 순서로 출력합니다. 조회 수 40 + 50 + 20을 더한 마지막 출력은 total=110입니다.

두 메서드 모두 Post에 선언되어 부모 타입으로 호출할 수 있습니다. 이 원문에서 describe()만 오버라이딩되며, viewCount()는 세 객체가 부모 구현을 사용합니다.

이 예의 조건

  • 배열 원소 0→1→2 순회이며 줄 나눔은 표현 배치입니다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2원소 0 Text/1 Code/2 Post 순서이다. describe는 자식이 super의 결과에 덧붙인 text=board 가입 인사/code=board 로그인 구현/board 공지이고, viewCount는 모두 부모 구현의 40/50/20이며 누적은 40→90→110이다. 표현을 위한 줄 나눔은 실제 출력 줄이 아니다.
원 도표의 설명과 주석
구분내용
설명원소 0 Text/1 Code/2 Post 순서이다. describe는 자식이 super의 결과에 덧붙인 text=board 가입 인사/code=board 로그인 구현/board 공지이고, viewCount는 모두 부모 구현의 40/50/20이며 누적은 40→90→110이다. 표현을 위한 줄 나눔은 실제 출력 줄이 아니다.
전체 읽기 정보부모 배열에서 달라지는 describe와 공유하는 viewCount 부모 배열에서 달라지는 describe와 공유하는 viewCount PolymorphicPostArray의 세 원소는 TextPost, CodePost, Post 객체이다. describe는 자식의 오버라이딩과 super.describe를 따르고, viewCount는 모두 부모 구현으로 40,50,20을 읽어 total 110을 만든다. describe는 자식별로 달라지고 viewCount는 부모 구현을 씁니다 PolymorphicPostArray 는 Post post 로 순회하며 describe() 와 viewCount() 를 호출합니다. Post 배열의 실제 타입별 describe 출력과 조회 수 배열 원소 / 실제 타입 describe()의 구현과 출력 viewCount() posts[0] TextPost TextPost.describe() 가 super.describe() 에 "text=" 를 붙입니다. text=board=가입 인사 Post.viewCount() 40 posts[1] CodePost CodePost.describe() 가 super.describe() 에 "code=" 를 붙입니다. code=board=로그인 구현 Post.viewCount() 50 posts[2] Post Post.describe() board=공지 Post.viewCount() 20 posts[0] : TextPost TextPost.describe() 가 super.describe() 에 "text=" 를 붙여 text=board=가입 인사 를 반환합니다. Post.viewCount() 는 40 을 반환합니다. posts[1] : CodePost CodePost.describe() 가 super.describe() 에 "code=" 를 붙여 code=board=로그인 구현 을 반환합니다. Post.viewCount() 는 50 을 반환합니다. posts[2] : Post Post.describe() 가 board=공지 를 반환하고 Post.viewCount() 가 20 을 반환합니다. 반복문은 각 describe() 결과를 위 순서로 출력합니다. 조회 수 40 + 50 + 20 을 더한 마지막 출력은 total=110 입니다. 두 메서드 모두 Post 에 선언되어 부모 타입으로 호출할 수 있습니다. 이 원문에서 describe() 만 오버라이딩되며, viewCount() 는 세 객체가 부모 구현을 사용합니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
Text 원소

0번 TextPost의 describe와 부모 viewCount를 읽습니다.

  • describe: text=board=가입 인사
  • count/total: 40/40
  • 자식 설명과 부모 공통 조회가 같은 원소에서 사용됩니다.
Code 원소

1번 CodePost의 describe와 부모 조회 값을 읽습니다.

  • describe: code=board=로그인 구현
  • count/total: 50/90
  • 자식별 설명이 달라도 viewCount 구현은 공유합니다.
Post 원소

2번 Post의 부모 describe와 20을 읽습니다.

  • describe: board=공지
  • 최종 total: 110
  • 순회 누적은 40+50+20입니다.

부모 배열에서 달라지는 describe와 공유하는 viewCount

배열 원소 0·1·2의 describe 구현 선택과 부모 viewCount 읽기 뒤 total 40→90→110이 됩니다. 메서드 선택·반환·호출자 출력/누산의 시점을 나눌 수 있습니다.

부모 배열에서 달라지는 describe와 공유하는 viewCount

PolymorphicPostArray의 세 원소는 TextPost, CodePost, Post 객체이다. describe는 자식의 오버라이딩과 super.describe를 따르고, viewCount는 모두 부모 구현으로40,50,20을 읽어 total110을 만든다.

describe는 자식별로 달라지고 viewCount는 부모 구현을 씁니다

PolymorphicPostArray는 Post post로 순회하며 describe()와 viewCount()를 호출합니다.

Post 배열의 실제 타입별 describe 출력과 조회 수
배열 원소 / 실제 타입describe()의 구현과 출력viewCount()
posts[0]

TextPost

TextPost.describe()가 super.describe()에 "text="를 붙입니다.

text=board=가입 인사

Post.viewCount()

40

posts[1]

CodePost

CodePost.describe()가 super.describe()에 "code="를 붙입니다.

code=board=로그인 구현

Post.viewCount()

50

posts[2]

Post

Post.describe()

board=공지

Post.viewCount()

20

describe는 자식별로 달라지고 viewCount는 부모 구현을 씁니다 원소0Text/1Code/2Post 순서;describe는자식super결과text=board가입인사/code=board로그인구현/board공지,viewCount모두부모40/50/20,누적40→90→110.표현줄나눔은실제출력줄아님. 0 · TextPost describe() 선택 자식 + super text=board= 가입 인사 Post.viewCount() 반환 40 모두 부모 구현 누적 total 40 1 · CodePost describe() 선택 자식 + super code=board= 로그인 구현 Post.viewCount() 반환 50 모두 부모 구현 누적 total 90 2 · Post describe() 선택 부모 구현 board= 공지 Post.viewCount() 반환 20 모두 부모 구현 누적 total 110 원소 0 → 1 → 2 순서 40 + 50 + 20 = 110
describe는 자식별로 달라지고 viewCount는 부모 구현을 씁니다 원소0Text/1Code/2Post 순서;describe는자식super결과text=board가입인사/code=board로그인구현/board공지,viewCount모두부모40/50/20,누적40→90→110.표현줄나눔은실제출력줄아님. 0 · TextPost describe() 선택 자식 + super text=board= 가입 인사 Post.viewCount() 반환 40 모두 부모 구현 누적 total 40 1 · CodePost describe() 선택 자식 + super code=board= 로그인 구현 Post.viewCount() 반환 50 모두 부모 구현 누적 total 90 2 · Post describe() 선택 부모 구현 board= 공지 Post.viewCount() 반환 20 모두 부모 구현 누적 total 110 원소 0 → 1 → 2 순서 40 + 50 + 20 = 110
  1. posts[0]: TextPost

    TextPost.describe()가 super.describe()에 "text="를 붙여 text=board=가입 인사를 반환합니다. Post.viewCount()는 40을 반환합니다.

  2. posts[1]: CodePost

    CodePost.describe()가 super.describe()에 "code="를 붙여 code=board=로그인 구현을 반환합니다. Post.viewCount()는 50을 반환합니다.

  3. posts[2]: Post

    Post.describe()가 board=공지를 반환하고 Post.viewCount()가 20을 반환합니다.

반복문은 각 describe() 결과를 위 순서로 출력합니다. 조회 수 40 + 50 + 20을 더한 마지막 출력은 total=110입니다.

두 메서드 모두 Post에 선언되어 부모 타입으로 호출할 수 있습니다. 이 원문에서 describe()만 오버라이딩되며, viewCount()는 세 객체가 부모 구현을 사용합니다.

이 예의 조건

  • 배열 원소 0→1→2 순회이며 줄 나눔은 표현 배치입니다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2원소 0 Text/1 Code/2 Post 순서이다. describe는 자식이 super의 결과에 덧붙인 text=board 가입 인사/code=board 로그인 구현/board 공지이고, viewCount는 모두 부모 구현의 40/50/20이며 누적은 40→90→110이다. 표현을 위한 줄 나눔은 실제 출력 줄이 아니다.
원 도표의 설명과 주석
구분내용
설명원소 0 Text/1 Code/2 Post 순서이다. describe는 자식이 super의 결과에 덧붙인 text=board 가입 인사/code=board 로그인 구현/board 공지이고, viewCount는 모두 부모 구현의 40/50/20이며 누적은 40→90→110이다. 표현을 위한 줄 나눔은 실제 출력 줄이 아니다.
전체 읽기 정보부모 배열에서 달라지는 describe와 공유하는 viewCount 부모 배열에서 달라지는 describe와 공유하는 viewCount PolymorphicPostArray의 세 원소는 TextPost, CodePost, Post 객체이다. describe는 자식의 오버라이딩과 super.describe를 따르고, viewCount는 모두 부모 구현으로 40,50,20을 읽어 total 110을 만든다. describe는 자식별로 달라지고 viewCount는 부모 구현을 씁니다 PolymorphicPostArray 는 Post post 로 순회하며 describe() 와 viewCount() 를 호출합니다. Post 배열의 실제 타입별 describe 출력과 조회 수 배열 원소 / 실제 타입 describe()의 구현과 출력 viewCount() posts[0] TextPost TextPost.describe() 가 super.describe() 에 "text=" 를 붙입니다. text=board=가입 인사 Post.viewCount() 40 posts[1] CodePost CodePost.describe() 가 super.describe() 에 "code=" 를 붙입니다. code=board=로그인 구현 Post.viewCount() 50 posts[2] Post Post.describe() board=공지 Post.viewCount() 20 posts[0] : TextPost TextPost.describe() 가 super.describe() 에 "text=" 를 붙여 text=board=가입 인사 를 반환합니다. Post.viewCount() 는 40 을 반환합니다. posts[1] : CodePost CodePost.describe() 가 super.describe() 에 "code=" 를 붙여 code=board=로그인 구현 을 반환합니다. Post.viewCount() 는 50 을 반환합니다. posts[2] : Post Post.describe() 가 board=공지 를 반환하고 Post.viewCount() 가 20 을 반환합니다. 반복문은 각 describe() 결과를 위 순서로 출력합니다. 조회 수 40 + 50 + 20 을 더한 마지막 출력은 total=110 입니다. 두 메서드 모두 Post 에 선언되어 부모 타입으로 호출할 수 있습니다. 이 원문에서 describe() 만 오버라이딩되며, viewCount() 는 세 객체가 부모 구현을 사용합니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
Text 원소

0번 TextPost의 describe와 부모 viewCount를 읽습니다.

  • describe: text=board=가입 인사
  • count/total: 40/40
  • 자식 설명과 부모 공통 조회가 같은 원소에서 사용됩니다.
Code 원소

1번 CodePost의 describe와 부모 조회 값을 읽습니다.

  • describe: code=board=로그인 구현
  • count/total: 50/90
  • 자식별 설명이 달라도 viewCount 구현은 공유합니다.
Post 원소

2번 Post의 부모 describe와 20을 읽습니다.

  • describe: board=공지
  • 최종 total: 110
  • 순회 누적은 40+50+20입니다.

반복문은 TextPost나 CodePost라는 이름을 한 번도 쓰지 않았습니다. 게시글 종류마다 if로 나누지 않아도 describe는 객체마다 다르게 동작하고, viewCount는 부모의 구현 하나를 모두가 함께 씁니다. 나중에 VideoPost를 추가하더라도 Post를 상속하고 describe를 오버라이딩하기만 하면 이 반복문은 한 글자도 고치지 않고 영상 게시글을 처리합니다.

생각해 보기: 같은 객체를 배열의 두 칸에 넣으면?

위 예제에서 TextPost 객체 하나를 변수 first에 담고, 배열의 첫째 칸과 둘째 칸에 모두 first를 넣어 보세요. 셋째 칸의 조회 수 20인 Post는 그대로 두고 반복문도 바꾸지 않습니다. 배열의 칸은 세 개지만 게시글 객체는 두 개입니다. 무엇이 출력되고, 마지막 total은 얼마일까요? 같은 객체를 한 번만 더해 줄까요?

풀이: 첫째 칸과 둘째 칸은 같은 TextPost 객체의 참조를 담고 있으므로 text=board=가입 인사가 두 번 출력되고, 셋째 칸에서 board=공지가 출력됩니다. 반복문은 칸마다 조회 수를 더하므로 total은 40 + 40 + 20 = 100입니다. 배열의 칸에는 객체가 아니라 참조값이 들어 있고, 반복문은 두 칸이 같은 객체를 가리키는지 확인하지 않습니다. 게시글이 중복으로 들어가지 않게 하려면 그런 규칙을 직접 코드로 만들어야 합니다.

매개변수 타입을 부모로 받으면

다형적 참조는 배열뿐 아니라 메서드 매개변수에서도 똑같이 동작합니다. 다음 process 메서드는 매개변수 타입이 Post이므로 Post와 그 모든 자식 객체를 받을 수 있습니다.

src/PostProcessor.java
public final class PostProcessor {
    public static void main(String[] args) {
        process(new TextPost());
        process(new CodePost());
    }

    private static void process(Post post) {
        System.out.println("before");
        post.publish();
        System.out.println("after");
    }

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

    private static final class TextPost extends Post {
        @Override
        void publish() {
            System.out.println("publish text post");
        }
    }

    private static final class CodePost extends Post {
        @Override
        void publish() {
            System.out.println("publish code post");
        }
    }
}
before
publish text post
after
before
publish code post
after

process(new TextPost())를 호출하면 인자로 넘긴 참조값이 매개변수 post에 복사되는데, 이것도 TextPost를 Post 변수에 담는 업캐스팅입니다. process 안의 before와 after 출력은 두 번 모두 같고, 가운데 post.publish()만 넘겨받은 객체에 따라 다른 줄을 출력했습니다.

process는 TextPost나 CodePost라는 클래스가 있다는 사실조차 모릅니다. 아는 것은 Post에 publish()가 있다는 것뿐입니다. 그래서 새 게시글 종류를 추가해도, 그 클래스가 Post를 상속하고 publish를 올바르게 오버라이딩하기만 하면 process는 고칠 필요가 없습니다. 이렇게 사용하는 쪽이 구체적인 클래스 대신 부모 타입에만 기대게 만드는 것이 다형성을 이용한 설계의 출발점이며, 8장의 인터페이스로 역할과 구현 분리하기가 이 생각을 이어 갑니다.

다형적 참조로 할 수 있는 일과 없는 일

지금까지 본 내용을 정리하면, 다형적 참조로 할 수 있는 일은 다음과 같습니다.

  • 부모 타입 배열 하나에 여러 하위 클래스의 객체를 함께 담습니다.
  • 부모 타입 매개변수 하나로 여러 하위 클래스의 객체를 받습니다.
  • 부모 타입에 선언된 메서드만 알고도 모든 하위 클래스의 객체를 다루는 코드를 씁니다.
  • 오버라이딩과 함께 쓰면 같은 호출이 실제 객체에 맞는 구현을 실행합니다.

반면 다형적 참조가 해 주지 않는 일도 있습니다.

  • 부모 타입 변수로 자식에만 있는 메서드를 호출할 수는 없습니다.
  • 상속 관계가 없는 클래스 사이에서는 변환할 수 없습니다. String 객체를 Post 변수에 담을 수는 없습니다.
  • 다운캐스팅으로 자식 타입에 다시 접근할 수는 있지만, 실제 객체가 그 자식이 아니면 실행 중에 실패합니다. 다형적 참조가 이 실패를 막아 주지는 않습니다.
  • 부모에 없는 동작을 자식들이 같은 이름으로 각자 만들었다고 해서 부모 타입으로 함께 호출할 수 있게 되지는 않습니다. 함께 호출하려면 그 메서드가 부모에 선언되어 있어야 합니다.

자식에만 있는 메서드가 자꾸 필요해진다면, 캐스팅을 늘리기 전에 그 동작이 부모에 선언되어야 할 공통 동작인지 먼저 따져 보세요. 일부 자식에게만 해당하는 역할이라면 추상 클래스와 인터페이스에서 배울 인터페이스로 따로 나누는 방법도 있습니다.

정리

  • 부모 타입 변수에는 자기 클래스와 모든 하위 클래스의 객체를 담을 수 있습니다. 자식 객체에는 부모의 필드와 메서드가 모두 들어 있기 때문입니다. 반대로 자식 타입 변수에 부모 객체를 담을 수는 없습니다.
  • 자식을 부모 타입으로 다루는 업캐스팅은 형변환 표시 없이 자동으로 일어나며, 객체를 복사하거나 바꾸지 않고 같은 참조값을 담습니다.
  • 메서드 호출은 두 단계로 결정됩니다. 컴파일러는 변수의 타입에 그 메서드가 있는지 확인하고, 실행할 때는 실제 객체의 클래스에서부터 구현을 찾습니다.
  • 그래서 부모 타입 변수로는 자식에만 있는 메서드를 부를 수 없지만, 오버라이딩된 메서드는 자식의 구현이 실행됩니다.
  • 부모 타입 배열과 매개변수를 쓰면 하위 클래스를 몰라도 되는 코드를 만들 수 있고, 새 하위 클래스를 추가해도 그 코드를 고치지 않아도 됩니다.

다음 글에서는 부모 타입 변수에 담긴 객체의 자식 전용 메서드를 꼭 불러야 할 때 쓰는 다운캐스팅을 살펴봅니다.

연습 문제

문제 1: 게시글 종류와 조회 수 합계

Post 부모와 TextPost, CodePost 자식을 만들고, 부모 타입 배열에 담긴 게시글의 조회 수 합계를 계산하세요. 각 자식은 kind()를 오버라이딩해 text와 code를 돌려주고, 반복문은 게시글마다 kind()의 결과를 출력합니다.

해설 보기
src/PolymorphicReport.java
public final class PolymorphicReport {
    public static void main(String[] args) {
        Post[] posts = {
                new TextPost(40),
                new CodePost(50)
        };

        int total = 0;
        for (Post post : posts) {
            System.out.println(post.kind());
            total += post.viewCount;
        }
        System.out.println("total=" + total);
    }

    private static class Post {
        private final int viewCount;

        Post(int viewCount) {
            this.viewCount = viewCount;
        }

        String kind() {
            return "board";
        }
    }

    private static final class TextPost extends Post {
        TextPost(int viewCount) { super(viewCount); }
        @Override String kind() { return "text"; }
    }

    private static final class CodePost extends Post {
        CodePost(int viewCount) { super(viewCount); }
        @Override String kind() { return "code"; }
    }
}
text
code
total=90

반복문은 Post 타입 변수만 쓰지만, post.kind()는 실행할 때 실제 객체의 구현이 선택되어 text와 code가 출력됩니다. 조회 수는 부모가 한 번만 선언한 viewCount 필드에서 읽어 40과 50을 더합니다.

viewCount는 private인데 main에서 post.viewCount로 읽을 수 있는 이유는 main과 Post가 같은 바깥 클래스 PolymorphicReport 안에 있기 때문입니다. 앞 글에서 본 중첩 클래스의 규칙이며, private 필드가 상속되거나 공개된 것이 아닙니다. 클래스를 각각의 파일로 나누면 이 코드는 컴파일되지 않으므로, 그때는 앞의 PolymorphicPostArray처럼 viewCount() 메서드를 두고 그것을 호출합니다.

문제 2: 어느 줄이 컴파일될까

다음 main에서 번호를 붙인 다섯 줄 가운데 컴파일 오류가 나는 줄을 모두 고르고, 이유를 설명하세요. Post에는 publish()가, CodePost에는 commit()이 있으며 CodePost는 Post를 상속합니다.

CodePost codePost = new CodePost();
Post post = codePost;      // 1
post.publish();            // 2
post.commit();             // 3
codePost.publish();        // 4
CodePost other = post;     // 5
해설 보기

3번과 5번에서 컴파일 오류가 납니다.

1번은 자식 타입 값을 부모 타입 변수에 담는 업캐스팅이므로 자동으로 허용됩니다. 2번은 post의 타입 Post에 publish()가 있으므로 컴파일됩니다. 3번은 post가 실제로 CodePost를 가리키고 있어도 컴파일러는 변수 타입 Post에서 commit()을 찾으므로 cannot find symbol 오류가 납니다. 4번은 CodePost가 Post의 publish()를 물려받았으므로 컴파일됩니다. 5번은 Post 타입 값을 CodePost 변수에 담으려는 것인데, 컴파일러는 post에 어떤 하위 클래스의 객체가 들어 있을지 알 수 없으므로 incompatible types: Post cannot be converted to CodePost 오류를 냅니다. 사람 눈에는 바로 위에서 CodePost를 담은 것이 보이지만, 컴파일러는 변수 타입만으로 판단합니다. 5번을 허용하는 방법이 다음 글의 다운캐스팅입니다.