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

안동민 개발노트

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

다형성과 추상 클래스

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

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

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

공통 부모 자체로는 완전한 객체 의미가 없다면 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

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

공통 검증·저장 흐름은 한 번만 구현되고 구체 발행만 각 자식이 제공합니다.


추상 타입 배열

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

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

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

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


추상 메서드

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

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 배열만 소유합니다.

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


연습 문제

추상 ReportItemfinal 필드 title과 추상 메서드 render()를 두고 TextItemNumberItem이 구현하게 하세요.

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

해설 보기
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은 부모가 초기화하고 값 형식별 렌더링은 구체 자식이 책임집니다.