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

안동민 개발노트

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

다형성과 추상 클래스

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

다형성의 실용적 가치는 여러 구현을 하나의 계약으로 처리하는 데 있습니다. 각 타입마다 비슷한 처리 함수를 복사하는 대신 부모 타입 매개변수 하나와 오버라이딩 메서드를 사용합니다. 공통 부모 자체로는 완전한 객체 의미가 없다면 abstract로 생성과 구현 누락을 막습니다.

이 절의 예제는 타입별 처리 중복, 부모 배열 통합, 추상 메서드 강제라는 세 단계로 같은 문제를 개선합니다. 코드 모양보다 새 활동을 추가할 때 기존 처리기가 얼마나 적게 바뀌는지에 주목합니다.

추상 클래스의 생성 제한

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

    private abstract static class StudyActivity {
        private final String topic;
        private final int minutes;

        StudyActivity(String topic, int minutes) {
            this.topic = topic;
            this.minutes = minutes;
        }

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

StudyActivity는 공통 상태와 계약을 정의하지만 “일반 학습”을 어떻게 수행할지 구현하지 않았습니다. 추상 클래스는 new로 직접 만들 수 없고 구체 자식 생성자의 부모 부분으로만 초기화됩니다. 부모 타입 변수와 매개변수로 사용하는 것은 가능합니다.

부모 매개변수와 공통 처리

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

static void processReading(ReadingActivity activity) { ... }
static void processCoding(CodingActivity activity) { ... }
static void processVideo(VideoActivity activity) { ... }

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

src/PolymorphicProcessor.java
public final class PolymorphicProcessor {
    public static void main(String[] args) {
        process(new ReadingActivity("Java", 40));
        process(new CodingActivity("Spring", 50));
    }

    private static void process(StudyActivity activity) {
        System.out.println("start=" + activity.topic());
        activity.perform();
        System.out.println("saved=" + activity.minutes());
    }

    private abstract static class StudyActivity {
        private final String topic;
        private final int minutes;

        StudyActivity(String topic, int minutes) {
            this.topic = topic;
            this.minutes = minutes;
        }

        String topic() { return topic; }
        int minutes() { return minutes; }
        abstract void perform();
    }

    private static final class ReadingActivity extends StudyActivity {
        ReadingActivity(String topic, int minutes) { super(topic, minutes); }
        @Override void perform() { System.out.println("read"); }
    }

    private static final class CodingActivity extends StudyActivity {
        CodingActivity(String topic, int minutes) { super(topic, minutes); }
        @Override void perform() { System.out.println("code"); }
    }
}
start=Java
read
saved=40
start=Spring
code
saved=50

process는 새 활동 종류가 추가돼도 부모의 공개 API를 지키면 수정할 필요가 없습니다. 공통 시작·저장 흐름은 한 번만 구현되고 구체 수행만 각 자식이 제공합니다.

추상 타입 배열

src/ActivityBatch.java
public final class ActivityBatch {
    public static void main(String[] args) {
        StudyActivity[] activities = {
                new ReadingActivity(40),
                new CodingActivity(50),
                new VideoActivity(30)
        };

        int total = 0;
        for (StudyActivity activity : activities) {
            System.out.println(activity.kind());
            activity.perform();
            total += activity.minutes();
        }
        System.out.println("total=" + total);
    }

    private abstract static class StudyActivity {
        private final int minutes;
        StudyActivity(int minutes) { this.minutes = minutes; }
        int minutes() { return minutes; }
        abstract String kind();
        abstract void perform();
    }

    private static final class ReadingActivity extends StudyActivity {
        ReadingActivity(int minutes) { super(minutes); }
        @Override String kind() { return "reading"; }
        @Override void perform() { System.out.println("read"); }
    }

    private static final class CodingActivity extends StudyActivity {
        CodingActivity(int minutes) { super(minutes); }
        @Override String kind() { return "coding"; }
        @Override void perform() { System.out.println("code"); }
    }

    private static final class VideoActivity extends StudyActivity {
        VideoActivity(int minutes) { super(minutes); }
        @Override String kind() { return "video"; }
        @Override void perform() { System.out.println("watch"); }
    }
}
reading
read
coding
code
video
watch
total=120

배열 타입, 반복 변수, 합계 로직은 한 번만 존재합니다. 타입별 배열과 반복문을 따로 두는 중복이 사라졌습니다. 오버라이딩이 실제 객체의 행동을 선택하므로 switchinstanceof 분기가 필요 없습니다.

추상 메서드

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

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

새 활동이 조용히 아무것도 하지 않는 결함이 실행 중에 발견됩니다. abstract void perform();은 본문이 없고 구체 자식이 반드시 구현해야 합니다. 구현하지 않은 자식도 abstract로 선언해 책임을 다음 하위 타입에 넘겨야 합니다.

추상 클래스는 추상 메서드뿐 아니라 생성자, 필드, 완성된 일반 메서드도 가질 수 있습니다. 공통 상태와 공통 구현을 재사용하면서 일부 행동만 자식에 맡길 때 적합합니다. 추상 메서드가 하나도 없어도 직접 생성을 막기 위해 abstract 클래스로 만들 수 있습니다.

추상 활동 저장

src/PolymorphicStudyLog.java
public final class PolymorphicStudyLog {
    public static void main(String[] args) {
        StudyLog log = new StudyLog(3);
        log.add(new ReadingActivity("Java", 40));
        log.add(new CodingActivity("Spring", 50));

        log.performAll();
        System.out.println(log.summary());
    }

    private static final class StudyLog {
        private final StudyActivity[] entries;
        private int size;

        StudyLog(int capacity) { entries = new StudyActivity[capacity]; }

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

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

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

    private abstract static class StudyActivity {
        private final String topic;
        private final int minutes;
        StudyActivity(String topic, int minutes) { this.topic = topic; this.minutes = minutes; }
        int minutes() { return minutes; }
        String topic() { return topic; }
        abstract void perform();
    }

    private static final class ReadingActivity extends StudyActivity {
        ReadingActivity(String topic, int minutes) { super(topic, minutes); }
        @Override void perform() { System.out.println("read=" + topic()); }
    }

    private static final class CodingActivity extends StudyActivity {
        CodingActivity(String topic, int minutes) { super(topic, minutes); }
        @Override void perform() { System.out.println("code=" + topic()); }
    }
}
read=Java
code=Spring
count=2, total=90

StudyLog는 구체 자식 이름을 알지 못하고 StudyActivity 배열만 소유합니다. 새 활동 구현을 추가해도 저장·합계·일괄 수행 로직은 부모 타입에 정의된 기능을 그대로 사용합니다.

연습 문제

추상 ReportItemfinal 필드 title과 추상 메서드 render()를 두고 TextItemNumberItem이 구현하게 하세요. 부모 배열에서 두 항목을 렌더링합니다.

해설 보기
src/AbstractReportItems.java
public final class AbstractReportItems {
    public static void main(String[] args) {
        ReportItem[] items = {
                new TextItem("topic", "arrays"),
                new NumberItem("minutes", 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; }
    }
}
topic=arrays
minutes=40

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