인터페이스 다중 구현
클래스 다중 상속의 모호함을 피하고 여러 인터페이스 역할을 한 Study Activity에 조합해 확장 가능한 Study Log를 완성합니다.
자바 클래스는 부모 클래스를 하나만 상속하지만 인터페이스는 여러 개 구현할 수 있습니다. 공통 상태 계층은 단일 클래스 상속으로 유지하고, 서로 독립적인 능력은 작은 인터페이스 역할로 조합합니다. Study Activity에 추적·내보내기·실행 역할을 선택적으로 붙여 종합합니다.
다중 클래스 상속 제한
public final class MultipleClassInheritance {
private static class Timed {}
private static class Named {}
private static final class StudyActivity extends Timed, Named {}
}error: '{' expected자바의 extends 뒤에는 부모 클래스 하나만 올 수 있습니다.
두 부모가 같은 이름의 필드나 메서드를 제공할 때 선택 규칙이 복잡해지는 다이아몬드 문제를 피하고 클래스 계층을 단순하게 유지합니다.
공통 상태가 필요하면 하나의 부모에 모으고, 다른 능력은 인터페이스로 표현합니다.
final class CodingActivity extends StudyActivity implements Trackable, Exportable {
}쉼표로 여러 인터페이스를 구현할 수 있습니다. 각 인터페이스의 추상 메서드 계약을 모두 만족해야 합니다.
다중 인터페이스 구현
public final class MultipleInterfaceRoles {
public static void main(String[] args) {
CodingActivity coding = new CodingActivity("Spring", 50);
run(coding);
track(coding);
export(coding);
}
private static void run(StudyTask task) {
task.execute();
}
private static void track(Trackable value) {
System.out.println("progress=" + value.progressPercent());
}
private static void export(Exportable value) {
System.out.println(value.exportText());
}
private interface StudyTask { void execute(); }
private interface Trackable { int progressPercent(); }
private interface Exportable { String exportText(); }
private static final class CodingActivity implements StudyTask, Trackable, Exportable {
private final String topic;
private final int minutes;
CodingActivity(String topic, int minutes) {
this.topic = topic;
this.minutes = minutes;
}
@Override public void execute() { System.out.println("code=" + topic); }
@Override public int progressPercent() { return 100; }
@Override public String exportText() { return topic + "," + minutes; }
}
}code=Spring
progress=100
Spring,50같은 객체 참조를 StudyTask, Trackable, Exportable 세 타입 관점으로 전달했습니다.
각 클라이언트는 자신에게 필요한 역할만 압니다.
export 함수는 execute나 진행률에 의존하지 않습니다.
역할이 작을수록 구현 클래스와 클라이언트가 불필요한 메서드에 결합하지 않습니다.
상속과 인터페이스 조합
public final class ClassAndInterfaces {
public static void main(String[] args) {
CodingActivity activity = new CodingActivity("Spring", 50, "feature/login");
StudyActivity base = activity;
Trackable tracked = activity;
Exportable exported = activity;
base.perform();
System.out.println("progress=" + tracked.progressPercent());
System.out.println("export=" + exported.exportText());
}
private interface Trackable { int progressPercent(); }
private interface Exportable { String exportText(); }
private abstract static class StudyActivity {
private final String topic;
private final int minutes;
StudyActivity(String topic, int minutes) {
this.topic = topic;
this.minutes = minutes;
}
protected String topic() { return topic; }
protected int minutes() { return minutes; }
abstract void perform();
}
private static final class CodingActivity extends StudyActivity implements Trackable, Exportable {
private final String branch;
CodingActivity(String topic, int minutes, String branch) {
super(topic, minutes);
this.branch = branch;
}
@Override void perform() { System.out.println("code=" + branch); }
@Override public int progressPercent() { return 100; }
@Override public String exportText() { return topic() + ":" + minutes(); }
}
}code=feature/login
progress=100
export=Spring:50StudyActivity는 topic·minutes 공통 상태와 perform 계층을 제공합니다.
Trackable과 Exportable은 계층과 독립적인 역할을 더합니다.
CodingActivity 객체 하나가 네 타입(CodingActivity 포함)으로 사용됩니다.
인터페이스 default 메서드 이름이 충돌하면 구현 클래스가 해당 메서드를 직접 오버라이딩해 모호함을 해결해야 합니다.
클래스의 구체 메서드와 인터페이스 default가 충돌하면 클래스 구현이 우선하지만, 역할 설계 단계에서 이름과 의미가 같은지 검토합니다.
역할 기반 다형성
public final class RoleBasedStudyLog {
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();
log.exportAll();
System.out.println(log.summary());
}
private static final class StudyLog {
private final StudyActivity[] activities;
private int size;
StudyLog(int capacity) { activities = new StudyActivity[capacity]; }
void add(StudyActivity activity) {
if (size == activities.length) throw new IllegalStateException("full");
activities[size++] = activity;
}
void performAll() {
for (int index = 0; index < size; index++) {
activities[index].perform();
}
}
void exportAll() {
for (int index = 0; index < size; index++) {
StudyActivity activity = activities[index];
if (activity instanceof Exportable exported) {
System.out.println("export=" + exported.exportText());
}
}
}
String summary() {
int total = 0;
for (int index = 0; index < size; index++) {
total += activities[index].minutes();
}
return "count=" + size + ", total=" + total;
}
}
private interface Exportable { String exportText(); }
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=" + topic()); }
}
private static final class CodingActivity extends StudyActivity implements Exportable {
CodingActivity(String topic, int minutes) { super(topic, minutes); }
@Override void perform() { System.out.println("code=" + topic()); }
@Override public String exportText() { return topic() + "," + minutes(); }
}
}read=Java
code=Spring
export=Spring,50
count=2, total=90모든 활동은 공통 perform과 minutes를 제공하고 CodingActivity만 Exportable 역할을 추가합니다.
선택 역할이 필요한 경계에서만 패턴 매칭을 사용합니다.
새로운 Exportable 활동을 추가해도 exportAll은 인터페이스에 정의된 기능으로 처리합니다.
계층과 역할의 평가
- 모든 자식이 부모 의미를 지키는가?
- 공통 상태가 실제로 모든 자식에 필요한가?
- 인터페이스 메서드를 모든 구현이 자연스럽게 수행하는가?
- 클라이언트가 필요하지 않은 큰 인터페이스에 의존하지 않는가?
- 새 구현 추가 때 타입별
switch가 늘어나지 않는가? - 상속보다 구성으로 객체를 가지는 편이 관계를 더 정확히 표현하지 않는가?
상속·추상 클래스·인터페이스는 중복 제거 도구이기 전에 타입 관계와 변경 경계를 표현합니다. 다음 장에서는 역할과 구현을 분리해 클라이언트 수정 없이 구현을 교체하는 설계로 확장합니다.
연습 문제
Savable과 Notifiable 인터페이스를 만들고 CompletedActivity가 둘 다 구현하게 하세요.
save와 notify 함수가 각각 필요한 역할 타입만 받도록 합니다.
해설 보기
public final class CombinedActivityRoles {
public static void main(String[] args) {
CompletedActivity activity = new CompletedActivity("arrays");
save(activity);
notifyUser(activity);
}
private static void save(Savable value) {
System.out.println(value.saveText());
}
private static void notifyUser(Notifiable value) {
value.notifyCompletion();
}
private interface Savable { String saveText(); }
private interface Notifiable { void notifyCompletion(); }
private static final class CompletedActivity implements Savable, Notifiable {
private final String topic;
CompletedActivity(String topic) { this.topic = topic; }
@Override public String saveText() { return "saved=" + topic; }
@Override public void notifyCompletion() { System.out.println("notified=" + topic); }
}
}saved=arrays
notified=arrays두 함수는 같은 구체 클래스가 아니라 자기 역할만 압니다. 구현 객체는 필요에 따라 독립 역할을 조합합니다.