인터페이스 다중 구현
클래스 다중 상속의 모호함을 피하고 여러 인터페이스 역할을 게시글에 조합해 확장 가능한 게시판을 완성합니다.
자바 클래스는 부모 클래스를 하나만 상속하지만 인터페이스는 여러 개 구현할 수 있습니다.
공통 상태 계층은 단일 클래스 상속으로 유지하고, 서로 독립적인 능력은 작은 인터페이스 역할로 조합합니다.
게시글에 검수·내보내기·발행 역할을 선택적으로 붙여 종합합니다.
다중 클래스 상속 제한
public final class MultipleClassInheritance {
private static class Timed {}
private static class Named {}
private static final class Post extends Timed, Named {}
}error: '{' expected자바의 extends 뒤에는 부모 클래스 하나만 올 수 있습니다.
두 부모가 같은 이름의 필드나 메서드를 제공할 때 선택 규칙이 복잡해지는 다이아몬드 문제를 피하고 클래스 계층을 단순하게 유지합니다.
공통 상태가 필요하면 하나의 부모에 모으고, 다른 능력은 인터페이스로 표현합니다.
final class CodePost extends Post implements Moderatable, Exportable {
}쉼표로 여러 인터페이스를 구현할 수 있습니다.
각 인터페이스의 추상 메서드 계약을 모두 만족해야 합니다.
다중 인터페이스 구현
public final class MultipleInterfaceRoles {
public static void main(String[] args) {
CodePost codePost = new CodePost("로그인 구현", 50);
publish(codePost);
moderate(codePost);
export(codePost);
}
private static void publish(Publishable post) {
post.publish();
}
private static void moderate(Moderatable value) {
System.out.println("priority=" + value.priority());
}
private static void export(Exportable value) {
System.out.println(value.exportText());
}
private interface Publishable { void publish(); }
private interface Moderatable { int priority(); }
private interface Exportable { String exportText(); }
private static final class CodePost implements Publishable, Moderatable, Exportable {
private final String title;
private final int viewCount;
CodePost(String title, int viewCount) {
this.title = title;
this.viewCount = viewCount;
}
@Override public void publish() { System.out.println("publish=" + title); }
@Override public int priority() { return 100; }
@Override public String exportText() { return title + "," + viewCount; }
}
}publish=로그인 구현
priority=100
로그인 구현,50같은 객체 참조를 Publishable, Moderatable, Exportable 세 타입 관점으로 전달했습니다.
각 클라이언트는 자신에게 필요한 역할만 압니다.
export 함수는 발행이나 검수 우선순위에 의존하지 않습니다.
역할이 작을수록 구현 클래스와 클라이언트가 불필요한 메서드에 결합하지 않습니다.
상속과 인터페이스 조합
public final class ClassAndInterfaces {
public static void main(String[] args) {
CodePost post = new CodePost("로그인 구현", 50, "feature/login");
Post base = post;
Moderatable moderated = post;
Exportable exported = post;
base.publish();
System.out.println("priority=" + moderated.priority());
System.out.println("export=" + exported.exportText());
}
private interface Moderatable { int priority(); }
private interface Exportable { String exportText(); }
private abstract static class Post {
private final String title;
private final int viewCount;
Post(String title, int viewCount) {
this.title = title;
this.viewCount = viewCount;
}
protected String title() { return title; }
protected int viewCount() { return viewCount; }
abstract void publish();
}
private static final class CodePost extends Post implements Moderatable, Exportable {
private final String branch;
CodePost(String title, int viewCount, String branch) {
super(title, viewCount);
this.branch = branch;
}
@Override void publish() { System.out.println("code=" + branch); }
@Override public int priority() { return 100; }
@Override public String exportText() { return title() + ":" + viewCount(); }
}
}code=feature/login
priority=100
export=로그인 구현:50Post는 title·viewCount 공통 상태와 publish 계층을 제공합니다.
Moderatable과 Exportable은 계층과 독립적인 역할을 더합니다.
CodePost 객체 하나가 네 타입(CodePost 포함)으로 사용됩니다.
인터페이스 default 메서드 이름이 충돌하면 구현 클래스가 해당 메서드를 직접 오버라이딩해 모호함을 해결해야 합니다.
클래스의 구체 메서드와 인터페이스 default가 충돌하면 클래스 구현이 우선하지만, 역할 설계 단계에서 이름과 의미가 같은지 검토합니다.
역할 기반 다형성
public final class RoleBasedBoard {
public static void main(String[] args) {
Board board = new Board(3);
board.add(new TextPost("가입 인사", 40));
board.add(new CodePost("로그인 구현", 50));
board.publishAll();
board.exportAll();
System.out.println(board.summary());
}
private static final class Board {
private final Post[] posts;
private int size;
Board(int capacity) { posts = new Post[capacity]; }
void add(Post post) {
if (size == posts.length) throw new IllegalStateException("full");
posts[size++] = post;
}
void publishAll() {
for (int index = 0; index < size; index++) {
posts[index].publish();
}
}
void exportAll() {
for (int index = 0; index < size; index++) {
Post post = posts[index];
if (post instanceof Exportable exported) {
System.out.println("export=" + exported.exportText());
}
}
}
String summary() {
int total = 0;
for (int index = 0; index < size; index++) {
total += posts[index].viewCount();
}
return "count=" + size + ", total=" + total;
}
}
private interface Exportable { String exportText(); }
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:" + title()); }
}
private static final class CodePost extends Post implements Exportable {
CodePost(String title, int viewCount) { super(title, viewCount); }
@Override void publish() { System.out.println("publish=code:" + title()); }
@Override public String exportText() { return title() + "," + viewCount(); }
}
}publish=text:가입 인사
publish=code:로그인 구현
export=로그인 구현,50
count=2, total=90모든 게시글은 공통 publish와 viewCount를 제공하고 CodePost만 Exportable 역할을 추가합니다.
선택 역할이 필요한 경계에서만 패턴 매칭을 사용합니다.
새로운 Exportable 게시글을 추가해도 exportAll은 인터페이스에 정의된 기능으로 처리합니다.
계층과 역할의 평가
- 모든 자식이 부모 의미를 지키는가?
- 공통 상태가 실제로 모든 자식에 필요한가?
- 인터페이스 메서드를 모든 구현이 자연스럽게 수행하는가?
- 클라이언트가 필요하지 않은 큰 인터페이스에 의존하지 않는가?
- 새 구현 추가 때 타입별
switch가 늘어나지 않는가? - 상속보다 구성으로 객체를 가지는 편이 관계를 더 정확히 표현하지 않는가?
상속·추상 클래스·인터페이스는 중복 제거 도구이기 전에 타입 관계와 변경 경계를 표현합니다.
다음 장에서는 역할과 구현을 분리해 클라이언트 수정 없이 구현을 교체하는 설계로 확장합니다.
연습 문제
Moderatable과 Exportable 인터페이스를 만들고 PublishedPost가 둘 다 구현하게 하세요.
moderate와 export 함수가 각각 필요한 역할 타입만 받도록 합니다.
해설 보기
public final class CombinedPostRoles {
public static void main(String[] args) {
PublishedPost post = new PublishedPost("가입 인사");
moderate(post);
export(post);
}
private static void moderate(Moderatable value) {
System.out.println("priority=" + value.priority());
}
private static void export(Exportable value) {
System.out.println("export=" + value.exportText());
}
private interface Moderatable { int priority(); }
private interface Exportable { String exportText(); }
private static final class PublishedPost implements Moderatable, Exportable {
private final String title;
PublishedPost(String title) { this.title = title; }
@Override public int priority() { return 80; }
@Override public String exportText() { return title; }
}
}priority=80
export=가입 인사두 함수는 같은 구체 클래스가 아니라 자기 역할만 압니다.
구현 객체는 필요에 따라 독립 역할을 조합합니다.