안동민 개발노트

본문 시작

다형성과 추상 클래스

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

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

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

공통 부모 자체로는 완전한 객체 의미가 없다면 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를 지키면 수정할 필요가 없습니다.

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

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

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
  1. 첫 호출: TextPost("가입 인사", 40)

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

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

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

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

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

이 예제는 공통 전후 출력 흐름을 한 번만 구현하고, 구체 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 배열만 소유합니다.

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

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