본문으로 건너뛰기

안동민 개발노트

본문 시작

다형성과 추상 클래스

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

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

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

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

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

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

타입별 처리 복사와 부모 매개변수 하나의 변경 비용이 다르다

클라이언트가 타입마다 공통 흐름을 복사하는 대신 부모 매개변수 하나가 달라지는 행동을 동적으로 호출합니다.

  1. 1
    타입별 함수

    processTextPost·processCodePost · 새 타입마다 흐름 복사

  2. 2
    부모 함수

    process(Post) · publish 동적 호출 · 기존 흐름 유지


추상 클래스의 생성 제한

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를 지키면 수정할 필요가 없습니다.

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

다형성 처리기는 공통 전후 흐름 사이에 구현 행동을 호출한다

부모가 시작·저장 순서를 고정하고 자식은 가운데 publish 한 단계만 구체화합니다.

  1. 1
    공통 검증

    title 확인

  2. 2
    추상 publish

    달라지는 계약

  3. 3
    실제 자식

    text / code / video

  4. 4
    공통 저장

    viewCount 저장


추상 타입 배열

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 분기가 필요 없습니다.


추상 메서드

추상 클래스는 의미 없는 부모 인스턴스 생성을 차단한다

공통 상태는 제공하되 수행 방식이 없는 Post 자체는 완전한 객체가 아니므로 직접 생성을 막습니다.

  1. abstract 부모

    title·viewCount 공통 상태 · new 금지

  2. abstract publish

    본문 없음 · 구현 의무

  3. 구체 자식

    @Override 완료 · 생성 가능

  4. 부모 참조

    자식 저장·다형 실행

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

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

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

Polymorphic Board는 구체 타입 대신 추상 게시글 계약을 저장한다

Board는 TextPost·CodePost·VideoPost의 세부 구현이 아니라 Post 계약과 공통 viewCount만 압니다.

  1. TextPost

    본문 게시

  2. CodePost

    코드 게시

  3. VideoPost

    영상 게시

  4. 공통 처리

    publish 호출 + viewCount 합계


연습 문제

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