다형성과 추상 클래스
타입별 반복 코드를 부모 배열과 오버라이딩으로 제거하고 인스턴스화하면 의미 없는 부모를 추상 클래스로 제한합니다.
다형성의 실용적 가치는 여러 구현을 하나의 계약으로 처리하는 데 있습니다.
각 타입마다 비슷한 처리 함수를 복사하는 대신 부모 타입 매개변수 하나와 오버라이딩 메서드를 사용합니다.
공통 부모 자체로는 완전한 객체 의미가 없다면 abstract로 생성과 구현 누락을 막습니다.
이 절의 예제는 타입별 처리 중복, 부모 배열 통합, 추상 메서드 강제라는 세 단계로 같은 문제를 개선합니다.
코드 모양보다 새 게시글을 추가할 때 기존 처리기가 얼마나 적게 바뀌는지에 주목합니다.
클라이언트가 타입마다 공통 흐름을 복사하는 대신 부모 매개변수 하나가 달라지는 행동을 동적으로 호출합니다.
- 1타입별 함수
processTextPost·processCodePost · 새 타입마다 흐름 복사
- 2부모 함수
process(Post) · publish 동적 호출 · 기존 흐름 유지
추상 클래스의 생성 제한
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 instantiatedPost는 공통 상태와 계약을 정의하지만 “일반 게시글”을 어떻게 수행할지 구현하지 않았습니다.
추상 클래스는 new로 직접 만들 수 없고 구체 자식 생성자의 부모 부분으로만 초기화됩니다.
부모 타입 변수와 매개변수로 사용하는 것은 가능합니다.
부모 매개변수와 공통 처리
다형성을 사용하지 않으면 다음과 같은 메서드가 늘어납니다.
static void processTextPost(TextPost post) { ... }
static void processCodePost(CodePost post) { ... }
static void processVideoPost(VideoPost post) { ... }공통 처리 순서가 같고 중간 행동만 다르면 부모 계약으로 합칩니다.
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=50process는 새 게시글 종류가 추가돼도 부모의 공개 API를 지키면 수정할 필요가 없습니다.
공통 검증·저장 흐름은 한 번만 구현되고 구체 발행만 각 자식이 제공합니다.
부모가 시작·저장 순서를 고정하고 자식은 가운데 publish 한 단계만 구체화합니다.
- 1공통 검증
title 확인
- 2추상 publish
달라지는 계약
- 3실제 자식
text / code / video
- 4공통 저장
viewCount 저장
추상 타입 배열
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 분기가 필요 없습니다.
추상 메서드
공통 상태는 제공하되 수행 방식이 없는 Post 자체는 완전한 객체가 아니므로 직접 생성을 막습니다.
- abstract 부모
title·viewCount 공통 상태 · new 금지
- abstract publish
본문 없음 · 구현 의무
- 구체 자식
@Override 완료 · 생성 가능
- 부모 참조
자식 저장·다형 실행
일반 부모 메서드에 빈 기본 구현을 두면 자식이 오버라이딩을 빼먹어도 컴파일됩니다.
void publish() {
// 아무 작업도 하지 않음
}새 게시글이 조용히 아무것도 하지 않는 결함이 실행 중에 발견됩니다.
abstract void publish();은 본문이 없고 구체 자식이 반드시 구현해야 합니다.
구현하지 않은 자식도 abstract로 선언해 책임을 다음 하위 타입에 넘겨야 합니다.
추상 클래스는 추상 메서드뿐 아니라 생성자, 필드, 완성된 일반 메서드도 가질 수 있습니다.
공통 상태와 공통 구현을 재사용하면서 일부 행동만 자식에 맡길 때 적합합니다.
추상 메서드가 하나도 없어도 직접 생성을 막기 위해 abstract 클래스로 만들 수 있습니다.
추상 게시글 저장
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=90Board는 구체 자식 이름을 알지 못하고 Post 배열만 소유합니다.
새 게시글 구현을 추가해도 저장·합계·일괄 발행 로직은 부모 타입에 정의된 기능을 그대로 사용합니다.
Board는 TextPost·CodePost·VideoPost의 세부 구현이 아니라 Post 계약과 공통 viewCount만 압니다.
- TextPost
본문 게시
- CodePost
코드 게시
- VideoPost
영상 게시
- 공통 처리
publish 호출 + viewCount 합계
연습 문제
추상 ReportItem에 final 필드 title과 추상 메서드 render()를 두고 TextItem과 NumberItem이 구현하게 하세요.
부모 배열에서 두 항목을 렌더링합니다.
해설 보기
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은 부모가 초기화하고 값 형식별 렌더링은 구체 자식이 책임집니다.