본문 바로가기

다형성과 추상 클래스

타입별 반복 코드를 부모 배열과 오버라이딩으로 제거하고 인스턴스화하면 의미 없는 부모를 추상 클래스로 제한합니다.

다형성의 실용적 가치는 여러 구현을 하나의 계약으로 처리하는 데 있습니다.

각 타입마다 비슷한 처리 함수를 복사하는 대신 부모 타입 매개변수 하나와 오버라이딩 메서드를 사용합니다.

공통 부모 자체로는 완전한 객체 의미가 없다면 abstract로 생성과 구현 누락을 막습니다.

이 절의 예제는 타입별 처리 중복, 부모 배열 통합, 추상 메서드 강제라는 세 단계로 같은 문제를 개선합니다.

코드 모양보다 새 게시글을 추가할 때 기존 처리기가 얼마나 적게 바뀌는지에 주목합니다.


추상 클래스의 생성 제한

lab/InstantiateAbstractPost.java
public final class InstantiateAbstractPost {
    public static void main(String[] args) {
        Post post = new Post("general", 10);
        System.out.println(post);
    }

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

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

        abstract void publish();
    }
}
컴파일 실패 관찰
error: Post is abstract; cannot be instantiated

Post는 공통 상태와 계약을 정의하지만 “일반 게시글”을 어떻게 발행할지 구현하지 않았습니다.

추상 클래스는 new로 직접 만들 수 없고 구체 자식 생성자의 부모 부분으로만 초기화됩니다.

부모 타입 변수와 매개변수로 사용하는 것은 가능합니다.


부모 매개변수와 공통 처리

다형성을 사용하지 않으면 다음과 같은 메서드가 늘어납니다.

static void processTextPost(TextPost post) { ... }
static void processCodePost(CodePost post) { ... }
static void processVideoPost(VideoPost post) { ... }

공통 처리 순서가 같고 중간 행동만 다르면 부모 계약으로 합칩니다.

src/PolymorphicProcessor.java
public final class PolymorphicProcessor {
    public static void main(String[] args) {
        process(new TextPost("가입 인사", 40));
        process(new CodePost("로그인 구현", 50));
    }

    private static void process(Post post) {
        System.out.println("validate=" + post.title());
        post.publish();
        System.out.println("saved=" + post.viewCount());
    }

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

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

        String title() { return title; }
        int viewCount() { return viewCount; }
        abstract void publish();
    }

    private static final class TextPost extends Post {
        TextPost(String title, int viewCount) { super(title, viewCount); }
        @Override void publish() { System.out.println("publish=text"); }
    }

    private static final class CodePost extends Post {
        CodePost(String title, int viewCount) { super(title, viewCount); }
        @Override void publish() { System.out.println("publish=code"); }
    }
}
validate=가입 인사
publish=text
saved=40
validate=로그인 구현
publish=code
saved=50

새 게시글 종류가 부모의 공개 API를 지키면, 이를 추가해도 process를 수정할 필요가 없습니다.

공통 처리의 전후 출력과 자식별 publish 호출

TextPost 처리 후 CodePost 처리에서 공통 시작 출력→자식 publish→공통 마무리 출력이 이어집니다. validate/saved는 실제 검증/저장이 아니라 관찰 메시지라는 조건을 유지한 실행 서사입니다.

공통 처리의 전후 출력과 자식별 publish 호출

PolymorphicProcessor의 process는 제목 출력, 실제 자식 publish 호출, 조회 수 출력을 공유한다. validate와 saved는 출력 문자열이며 실제 유효성 검사와 저장 작업을 수행하지 않는다.

공통 전후 출력 사이에서 실제 자식의 publish를 호출합니다

PolymorphicProcessor의 process(Post post)는 세 단계를 공유합니다. main은 TextPost 호출을 모두 끝낸 뒤 CodePost 호출을 시작합니다.

TextPost와 CodePost에 같은 process를 적용한 단계별 실제 출력
순서와 작업TextPost · 가입 인사, 40CodePost · 로그인 구현, 50
1. 시작 출력

post.title()

validate=가입 인사validate=로그인 구현
2. 자식별 동작

post.publish()

TextPost.publish()

publish=text

CodePost.publish()

publish=code

3. 마무리 출력

post.viewCount()

saved=40saved=50
공통 전후 출력 사이에서 실제 자식의 publish를 호출합니다 첫Text호출validate가입인사→자식publishtext→saved40,정상종료후Code호출validate로그인구현→publishcode→saved50;validate/saved출력라벨/실제검증·저장없음. 첫 호출 · TextPost process 시작 출력 validate=가입 인사 자식 publish 실행 publish=text process 마무리 출력 saved=40 다음 호출 · CodePost process 시작 출력 validate=로그인 구현 자식 publish 실행 publish=code process 마무리 출력 saved=50 첫 호출 정상 종료 뒤 다음 호출 validate/saved: 실제 저장 아님
공통 전후 출력 사이에서 실제 자식의 publish를 호출합니다 첫Text호출validate가입인사→자식publishtext→saved40,정상종료후Code호출validate로그인구현→publishcode→saved50;validate/saved출력라벨/실제검증·저장없음. 첫 호출 · TextPost process 시작 출력 validate=가입 인사 자식 publish 실행 publish=text process 마무리 출력 saved=40 다음 호출 · CodePost process 시작 출력 validate=로그인 구현 자식 publish 실행 publish=code process 마무리 출력 saved=50 첫 호출 정상 종료 뒤 다음 호출 validate/saved: 실제 저장 아님
  1. 첫 호출: TextPost("가입 인사", 40)

    제목 출력 → TextPost.publish() 실행 → 조회 수 출력 순서입니다.

    validate=가입 인사
    publish=text
    saved=40
  2. 다음 호출: CodePost("로그인 구현", 50)

    첫 호출이 끝난 뒤 같은 순서에서 CodePost.publish()을 실행합니다.

    validate=로그인 구현
    publish=code
    saved=50

validate=와 saved=는 관찰용 출력 문자열입니다. 이 프로그램에는 실제 입력 검증이나 저장 작업이 없으며, 가운데 publish()도 각 자식이 문자열을 출력하는 구현입니다.

이 예의 조건

  • validate와 saved는 콘솔 라벨이며 실제 검증·저장은 없습니다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2첫 Text 호출은 validate 가입 인사→자식 publish text→saved 40이고, 정상 종료 후 Code 호출은 validate 로그인 구현→publish code→saved 50이다. validate/saved는 출력 라벨이며 실제 검증·저장은 없다.
원 도표의 설명과 주석
구분내용
설명첫 Text 호출은 validate 가입 인사→자식 publish text→saved 40이고, 정상 종료 후 Code 호출은 validate 로그인 구현→publish code→saved 50이다. validate/saved는 출력 라벨이며 실제 검증·저장은 없다.
전체 읽기 정보공통 처리의 전후 출력과 자식별 publish 호출 공통 처리의 전후 출력과 자식별 publish 호출 PolymorphicProcessor의 process는 제목 출력, 실제 자식 publish 호출, 조회 수 출력을 공유한다. validate와 saved는 출력 문자열이며 실제 유효성 검사와 저장 작업을 수행하지 않는다. 공통 전후 출력 사이에서 실제 자식의 publish를 호출합니다 PolymorphicProcessor 의 process(Post post) 는 세 단계를 공유합니다. main 은 TextPost 호출을 모두 끝낸 뒤 CodePost 호출을 시작합니다. TextPost와 CodePost에 같은 process를 적용한 단계별 실제 출력 순서와 작업 TextPost · 가입 인사, 40 CodePost · 로그인 구현, 50 1. 시작 출력 post.title() validate=가입 인사 validate=로그인 구현 2. 자식별 동작 post.publish() TextPost.publish() publish=text CodePost.publish() publish=code 3. 마무리 출력 post.viewCount() saved=40 saved=50 첫 호출: TextPost("가입 인사", 40) 제목 출력 → TextPost.publish() 실행 → 조회 수 출력 순서입니다. validate=가입 인사 publish=text saved=40 다음 호출: CodePost("로그인 구현", 50) 첫 호출이 끝난 뒤 같은 순서에서 CodePost.publish() 을 실행합니다. validate=로그인 구현 publish=code saved=50 validate= 와 saved= 는 관찰용 출력 문자열입니다. 이 프로그램에는 실제 입력 검증이나 저장 작업이 없으며, 가운데 publish() 도 각 자식이 문자열을 출력하는 구현입니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
공통 시작

process가 각 게시글의 시작 문자열을 출력합니다.

  • Text: validate=가입 인사
  • Code: validate=로그인 구현
  • Text 호출 정상 종료 뒤 Code를 처리합니다.
동적 publish

실제 자식의 publish가 출력합니다.

  • Text: publish=text
  • Code: publish=code
  • 공통 process가 실제 객체 구현에 호출을 위임합니다.
공통 마무리

process가 조회 수를 읽어 saved를 출력합니다.

  • Text: saved=40
  • Code: saved=50
  • 출력 라벨을 저장소 완료로 해석하지 않습니다.

공통 처리의 전후 출력과 자식별 publish 호출

TextPost 처리 후 CodePost 처리에서 공통 시작 출력→자식 publish→공통 마무리 출력이 이어집니다. validate/saved는 실제 검증/저장이 아니라 관찰 메시지라는 조건을 유지한 실행 서사입니다.

공통 처리의 전후 출력과 자식별 publish 호출

PolymorphicProcessor의 process는 제목 출력, 실제 자식 publish 호출, 조회 수 출력을 공유한다. validate와 saved는 출력 문자열이며 실제 유효성 검사와 저장 작업을 수행하지 않는다.

공통 전후 출력 사이에서 실제 자식의 publish를 호출합니다

PolymorphicProcessor의 process(Post post)는 세 단계를 공유합니다. main은 TextPost 호출을 모두 끝낸 뒤 CodePost 호출을 시작합니다.

TextPost와 CodePost에 같은 process를 적용한 단계별 실제 출력
순서와 작업TextPost · 가입 인사, 40CodePost · 로그인 구현, 50
1. 시작 출력

post.title()

validate=가입 인사validate=로그인 구현
2. 자식별 동작

post.publish()

TextPost.publish()

publish=text

CodePost.publish()

publish=code

3. 마무리 출력

post.viewCount()

saved=40saved=50
공통 전후 출력 사이에서 실제 자식의 publish를 호출합니다 첫Text호출validate가입인사→자식publishtext→saved40,정상종료후Code호출validate로그인구현→publishcode→saved50;validate/saved출력라벨/실제검증·저장없음. 첫 호출 · TextPost process 시작 출력 validate=가입 인사 자식 publish 실행 publish=text process 마무리 출력 saved=40 다음 호출 · CodePost process 시작 출력 validate=로그인 구현 자식 publish 실행 publish=code process 마무리 출력 saved=50 첫 호출 정상 종료 뒤 다음 호출 validate/saved: 실제 저장 아님
공통 전후 출력 사이에서 실제 자식의 publish를 호출합니다 첫Text호출validate가입인사→자식publishtext→saved40,정상종료후Code호출validate로그인구현→publishcode→saved50;validate/saved출력라벨/실제검증·저장없음. 첫 호출 · TextPost process 시작 출력 validate=가입 인사 자식 publish 실행 publish=text process 마무리 출력 saved=40 다음 호출 · CodePost process 시작 출력 validate=로그인 구현 자식 publish 실행 publish=code process 마무리 출력 saved=50 첫 호출 정상 종료 뒤 다음 호출 validate/saved: 실제 저장 아님
  1. 첫 호출: TextPost("가입 인사", 40)

    제목 출력 → TextPost.publish() 실행 → 조회 수 출력 순서입니다.

    validate=가입 인사
    publish=text
    saved=40
  2. 다음 호출: CodePost("로그인 구현", 50)

    첫 호출이 끝난 뒤 같은 순서에서 CodePost.publish()을 실행합니다.

    validate=로그인 구현
    publish=code
    saved=50

validate=와 saved=는 관찰용 출력 문자열입니다. 이 프로그램에는 실제 입력 검증이나 저장 작업이 없으며, 가운데 publish()도 각 자식이 문자열을 출력하는 구현입니다.

이 예의 조건

  • validate와 saved는 콘솔 라벨이며 실제 검증·저장은 없습니다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2첫 Text 호출은 validate 가입 인사→자식 publish text→saved 40이고, 정상 종료 후 Code 호출은 validate 로그인 구현→publish code→saved 50이다. validate/saved는 출력 라벨이며 실제 검증·저장은 없다.
원 도표의 설명과 주석
구분내용
설명첫 Text 호출은 validate 가입 인사→자식 publish text→saved 40이고, 정상 종료 후 Code 호출은 validate 로그인 구현→publish code→saved 50이다. validate/saved는 출력 라벨이며 실제 검증·저장은 없다.
전체 읽기 정보공통 처리의 전후 출력과 자식별 publish 호출 공통 처리의 전후 출력과 자식별 publish 호출 PolymorphicProcessor의 process는 제목 출력, 실제 자식 publish 호출, 조회 수 출력을 공유한다. validate와 saved는 출력 문자열이며 실제 유효성 검사와 저장 작업을 수행하지 않는다. 공통 전후 출력 사이에서 실제 자식의 publish를 호출합니다 PolymorphicProcessor 의 process(Post post) 는 세 단계를 공유합니다. main 은 TextPost 호출을 모두 끝낸 뒤 CodePost 호출을 시작합니다. TextPost와 CodePost에 같은 process를 적용한 단계별 실제 출력 순서와 작업 TextPost · 가입 인사, 40 CodePost · 로그인 구현, 50 1. 시작 출력 post.title() validate=가입 인사 validate=로그인 구현 2. 자식별 동작 post.publish() TextPost.publish() publish=text CodePost.publish() publish=code 3. 마무리 출력 post.viewCount() saved=40 saved=50 첫 호출: TextPost("가입 인사", 40) 제목 출력 → TextPost.publish() 실행 → 조회 수 출력 순서입니다. validate=가입 인사 publish=text saved=40 다음 호출: CodePost("로그인 구현", 50) 첫 호출이 끝난 뒤 같은 순서에서 CodePost.publish() 을 실행합니다. validate=로그인 구현 publish=code saved=50 validate= 와 saved= 는 관찰용 출력 문자열입니다. 이 프로그램에는 실제 입력 검증이나 저장 작업이 없으며, 가운데 publish() 도 각 자식이 문자열을 출력하는 구현입니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
공통 시작

process가 각 게시글의 시작 문자열을 출력합니다.

  • Text: validate=가입 인사
  • Code: validate=로그인 구현
  • Text 호출 정상 종료 뒤 Code를 처리합니다.
동적 publish

실제 자식의 publish가 출력합니다.

  • Text: publish=text
  • Code: publish=code
  • 공통 process가 실제 객체 구현에 호출을 위임합니다.
공통 마무리

process가 조회 수를 읽어 saved를 출력합니다.

  • Text: saved=40
  • Code: saved=50
  • 출력 라벨을 저장소 완료로 해석하지 않습니다.

이 예제는 공통 전후 출력 흐름을 한 번만 구현하고, 구체 publish 호출은 각 자식이 제공합니다. validate=와 saved=는 관찰용 메시지이며 실제 검증이나 저장은 수행하지 않습니다.


추상 타입 배열

src/PostBatch.java
public final class PostBatch {
    public static void main(String[] args) {
        Post[] posts = {
                new TextPost(40),
                new CodePost(50),
                new VideoPost(30)
        };

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

    private abstract static class Post {
        private final int viewCount;
        Post(int viewCount) { this.viewCount = viewCount; }
        int viewCount() { return viewCount; }
        abstract String kind();
        abstract void publish();
    }

    private static final class TextPost extends Post {
        TextPost(int viewCount) { super(viewCount); }
        @Override String kind() { return "text"; }
        @Override void publish() { System.out.println("publish=text"); }
    }

    private static final class CodePost extends Post {
        CodePost(int viewCount) { super(viewCount); }
        @Override String kind() { return "code"; }
        @Override void publish() { System.out.println("publish=code"); }
    }

    private static final class VideoPost extends Post {
        VideoPost(int viewCount) { super(viewCount); }
        @Override String kind() { return "video"; }
        @Override void publish() { System.out.println("publish=video"); }
    }
}
text
publish=text
code
publish=code
video
publish=video
total=120

배열 타입, 반복 변수, 합계 로직은 한 번만 존재합니다.

타입별 배열과 반복문을 따로 두는 중복이 사라졌습니다.

오버라이딩이 실제 객체의 행동을 선택하므로 switch나 instanceof 분기가 필요 없습니다.


추상 메서드

일반 부모 메서드에 빈 기본 구현을 두면 자식이 오버라이딩을 빼먹어도 컴파일됩니다.

void publish() {
    // 아무 작업도 하지 않음
}

새 게시글이 조용히 아무것도 하지 않는 결함이 실행 중에 발견됩니다.

abstract void publish(); 선언에는 본문이 없고 구체 자식이 반드시 구현해야 합니다.

구현하지 않은 자식도 abstract로 선언해 책임을 다음 하위 타입에 넘겨야 합니다.

추상 클래스는 추상 메서드뿐 아니라 생성자, 필드, 완성된 일반 메서드도 가질 수 있습니다.

공통 상태와 공통 구현을 재사용하면서 일부 행동만 자식에 맡길 때 적합합니다.

추상 메서드가 하나도 없어도 직접 생성을 막기 위해 abstract 클래스로 만들 수 있습니다.


추상 게시글 저장

src/PolymorphicBoard.java
public final class PolymorphicBoard {
    public static void main(String[] args) {
        Board board = new Board(3);
        board.add(new TextPost("가입 인사", 40));
        board.add(new CodePost("로그인 구현", 50));

        board.publishAll();
        System.out.println(board.summary());
    }

    private static final class Board {
        private final Post[] entries;
        private int size;

        Board(int capacity) { entries = new Post[capacity]; }

        void add(Post post) {
            if (size == entries.length) throw new IllegalStateException("full");
            entries[size++] = post;
        }

        void publishAll() {
            for (int index = 0; index < size; index++) {
                entries[index].publish();
            }
        }

        String summary() {
            int total = 0;
            for (int index = 0; index < size; index++) {
                total += entries[index].viewCount();
            }
            return "count=" + size + ", total=" + total;
        }
    }

    private abstract static class Post {
        private final String title;
        private final int viewCount;
        Post(String title, int viewCount) { this.title = title; this.viewCount = viewCount; }
        int viewCount() { return viewCount; }
        String title() { return title; }
        abstract void publish();
    }

    private static final class TextPost extends Post {
        TextPost(String title, int viewCount) { super(title, viewCount); }
        @Override void publish() { System.out.println("publish=text:" + title()); }
    }

    private static final class CodePost extends Post {
        CodePost(String title, int viewCount) { super(title, viewCount); }
        @Override void publish() { System.out.println("publish=code:" + title()); }
    }
}
publish=text:가입 인사
publish=code:로그인 구현
count=2, total=90

Board는 구체 자식 이름을 알지 못하고 Post 배열만 소유합니다.

용량은 3이지만 사용 개수 size는 2이므로 순회는 0·1번만 처리합니다. 세 번째 null 칸까지 entries.length로 순회하면 null 메서드 호출에 실패합니다. 이 최소 Board의 add는 가득 찼는지만 검사하고 null을 거절하지 않습니다. 따라서 예제처럼 실제 게시글을 넣는 조건에서 동작을 확인하며, 공개 입력 경계로 사용할 때는 null 검증도 필요합니다.

새 게시글 구현을 추가해도 저장·합계·일괄 발행 로직은 부모 타입에 정의된 기능을 그대로 사용합니다.

조건을 바꿔보기: 남은 칸과 full 검사

위 Board에 조회 수 30인 TextPost("추가 인사", 30)를 세 번째로 추가한 뒤 summary를 호출한다고 가정합니다. 그다음 네 번째 객체의 add가 full 예외를 던지고 호출자가 그 예외를 잡습니다. 각각 count와 total은 얼마이며, 실패한 네 번째 호출 뒤 size가 4가 될까요?

풀이: 세 번째 성공 뒤 count=3, total=120입니다. 네 번째는 size와 배열 길이가 모두 3이어서 대입·증가 문장 전에 실패합니다. 따라서 이후에도 count=3, total=120입니다. 새로운 종류의 행동 계약과 저장 용량 제한은 서로 다른 조건입니다.

Post 배열의 실제 자식 객체와 사용 중인 두 슬롯

PolymorphicBoard는 용량3 Post 배열의0번과1번에 TextPost와 CodePost 참조를 보관하고 size는2다.2번은null이며 publishAll은 사용 중인 두 슬롯의 재정의 메서드를 호출한다.

부모 타입 배열은 서로 다른 자식 객체의 참조를 보관합니다

PolymorphicBoard에서 두 add를 마치고 publishAll()을 호출하기 전입니다. Board(3)의 배열에는 두 자식 객체의 참조와 미사용 슬롯 하나가 있습니다.

Board가 Post 배열을 통해 참조하는 TextPost와 CodePost size2인 Board의 entries는 길이3 Post 배열을 가리킨다.0번은 가입인사 조회수40 TextPost,1번은 로그인구현 조회수50 CodePost의 참조이고2번은null이다. 각 자식은 자기 publish 출력 구현을 제공한다. Board 객체 size = 2 final Post[] entries Post[] · length = 3 [0] 참조 [1] 참조 [2] null TextPost 객체 title: 가입 인사 viewCount: 40 publish()을 호출하면 publish=text:가입 인사 CodePost 객체 title: 로그인 구현 viewCount: 50 publish()을 호출하면 publish=code:로그인 구현
부모 타입 배열은 서로 다른 자식 객체의 참조를 보관합니다 size2 Board→길이3Post배열,slot0→별개Text가입인사40/slot1→별개Code로그인구현50,slot2 null noedge/noiteration;publish각자식·summary90.실선참조/표현줄분리는실제출력줄아님. Board 객체 size = 2 entries 참조 Post[] · length=3 [0] TextPost 참조 [1] CodePost 참조 [2] null · 미사용 TextPost 객체 title: 가입 인사 viewCount:40 publish=text: 가입 인사 CodePost 객체 title: 로그인 구현 viewCount:50 publish=code: 로그인 구현 실선: 객체·배열 참조 순회는 0·1번만 호출 count=2, total=90
Board와 배열
size = 2인 Board의 final Post[] entries는 길이 3인 배열을 가리킵니다. 배열 칸의 정적 타입은 Post입니다.
entries[0] · TextPost 참조
제목 가입 인사, 조회 수 40인 객체입니다. publish() 호출 시 publish=text:가입 인사를 출력합니다.
entries[1] · CodePost 참조
제목 로그인 구현, 조회 수 50인 별개 객체입니다. publish() 호출 시 publish=code:로그인 구현를 출력합니다.
entries[2] · 미사용
null이며 size = 2인 이 순회에서는 접근하지 않습니다.

publishAll()은 인덱스 0, 1에서 실제 자식의 publish()를 호출합니다. 이어지는 summary() 출력은 count=2, total=90입니다. 추상 Post 객체를 직접 만든 것이 아니라 자식 객체를 부모 타입 배열로 참조합니다.


연습 문제

추상 ReportItem에 final 필드 title과 추상 메서드 render()를 두고 TextItem과 NumberItem이 구현하게 하세요.

부모 배열에서 두 항목을 렌더링합니다.

해설 보기
src/AbstractReportItems.java
public final class AbstractReportItems {
    public static void main(String[] args) {
        ReportItem[] items = {
                new TextItem("title", "가입 인사"),
                new NumberItem("viewCount", 40)
        };
        for (ReportItem item : items) {
            System.out.println(item.render());
        }
    }

    private abstract static class ReportItem {
        protected final String title;
        ReportItem(String title) { this.title = title; }
        abstract String render();
    }

    private static final class TextItem extends ReportItem {
        private final String value;
        TextItem(String title, String value) { super(title); this.value = value; }
        @Override String render() { return title + "=" + value; }
    }

    private static final class NumberItem extends ReportItem {
        private final int value;
        NumberItem(String title, int value) { super(title); this.value = value; }
        @Override String render() { return title + "=" + value; }
    }
}
title=가입 인사
viewCount=40

공통 title은 부모가 초기화하고 값 형식별 렌더링은 구체 자식이 책임집니다.