인터페이스 다중 구현
클래스 상속은 하나만 허용하면서 인터페이스는 여러 개 구현하게 한 이유를 알아보고, 게시글에 필요한 역할만 골라 붙이는 방법과 default 메서드가 겹칠 때 해결하는 방법을 배웁니다.
게시판의 게시글에는 여러 기능이 필요합니다. 모든 게시글은 발행할 수 있어야 하고, 신고가 들어온 게시글은 검수 우선순위를 알려 줘야 하며, 코드 게시글은 다른 형식으로 내보낼 수 있어야 합니다. 그런데 이 기능들은 서로 관련이 없고, 모든 게시글에 다 필요한 것도 아닙니다. 이런 기능을 모두 Post 부모 클래스에 넣으면 내보낼 수 없는 게시글도 내보내기 메서드를 갖게 됩니다. 그렇다고 기능마다 부모 클래스를 따로 만들면, 자바 클래스는 부모를 하나만 상속할 수 있다는 규칙에 걸립니다.
추상 클래스와 인터페이스에서 인터페이스는 클래스 상속과 별개라서 이미 부모가 있는 클래스도 구현할 수 있다고 했습니다. 자바가 이 문제에 내놓는 답도 여기서 나옵니다. 공통 상태는 부모 클래스 하나로 물려받고, 서로 독립적인 기능은 작은 인터페이스로 나눠 필요한 것만 골라 구현하는 것입니다. 이 글에서는 먼저 클래스 다중 상속이 왜 막혀 있는지 보고, 인터페이스 여러 개를 구현하는 방법과 그때 생길 수 있는 default 메서드 충돌을 다룹니다. 마지막으로 일부 게시글에만 있는 기능을 게시판에서 처리해 봅니다.
클래스는 부모를 하나만 상속한다
먼저 extends 뒤에 부모 클래스를 두 개 적으면 어떻게 되는지 확인해 보겠습니다. 다음 코드의 Post는 extends 뒤에 Timed와 Named를 쉼표로 이어 적었습니다. 두 부모 클래스는 본문이 비어 있으므로, 오류가 난다면 원인은 Post를 선언한 이 한 줄에 있습니다.
public final class MultipleClassInheritance {
private static class Timed {}
private static class Named {}
private static final class Post extends Timed, Named {}
}lab/MultipleClassInheritance.java:5: error: '{' expected
private static final class Post extends Timed, Named {}
^
1 error메시지는 다중 상속이 금지되었다고 말하지 않고 '{' expected, 즉 '{가 와야 한다'고만 알려 줍니다. 자바 문법에서 extends 뒤에는 클래스 이름이 하나만 올 수 있으므로, 컴파일러는 Timed를 읽은 뒤 implements처럼 이어지는 다른 절이나 클래스 본문을 여는 {가 나오기를 기대합니다. 그런데 쉼표가 나왔으니 문법 오류로 처리한 것입니다. 오류 위치를 가리키는 ^가 쉼표 아래에 있다는 점을 보면 원인을 찾을 수 있습니다.
자바가 클래스 다중 상속을 막는 이유는 상속과 메서드 오버라이딩에서 본 다이아몬드 문제입니다. 두 부모가 같은 이름의 메서드를 서로 다르게 구현했다면 자식이 어느 쪽을 물려받을지 정할 수 없습니다. 필드는 더 곤란합니다. 두 부모가 모두 title 필드를 가지고 있다면 자식 객체에는 title이 두 개 있어야 할까요, 하나로 합쳐야 할까요? 두 부모의 생성자가 같은 필드를 서로 다르게 초기화한다면 어느 쪽이 우선할까요? 자바는 이런 규칙을 만들어 언어를 복잡하게 하는 대신 클래스의 부모를 하나로 제한했습니다.
반면 인터페이스는 여러 개를 구현할 수 있습니다. 인터페이스에서는 이런 문제가 거의 생기지 않기 때문입니다. 인터페이스에는 인스턴스 필드가 없으니 상태가 겹칠 일이 없습니다. 또 추상 메서드에는 구현이 없으니, 두 인터페이스가 같은 메서드를 선언해도 둘 중 하나를 고를 필요가 없습니다. 예를 들어 Moderatable과 Reviewable이 둘 다 int priority();를 선언했다면, 두 인터페이스를 모두 구현하는 클래스는 priority()를 하나만 구현하면 되고 그 구현이 두 인터페이스를 동시에 만족합니다. 다만 한쪽은 int priority(), 다른 쪽은 String priority()처럼 반환 타입만 다르다면 메서드 하나로 둘을 함께 만족할 수 없으므로, 컴파일러가 두 인터페이스를 함께 구현하지 못하게 합니다. 어느 구현을 쓸지가 문제 되는 곳은 구현을 가진 default 메서드뿐이고, 이 경우는 뒤에서 따로 다룹니다.
부모 클래스와 인터페이스를 함께 쓸 때는 extends를 먼저, implements를 나중에 적고, 인터페이스는 쉼표로 구분해 여러 개를 나열합니다.
final class CodePost extends Post implements Moderatable, Exportable {
// Post의 추상 메서드와 두 인터페이스의 추상 메서드를 모두 구현한다
}CodePost가 구체 클래스라면 Post의 추상 메서드와 Moderatable, Exportable의 추상 메서드를 하나도 빠짐없이 구현해야 합니다. 순서를 바꿔 implements Moderatable extends Post라고 쓰면 앞의 예제와 같은 '{' expected 오류가 납니다. 한편 인터페이스끼리는 interface ReviewedExport extends Moderatable, Exportable {}처럼 extends로 여러 인터페이스를 한꺼번에 물려받을 수 있습니다. 이때도 물려받는 것은 메서드 목록뿐이라서 상태가 겹치는 문제는 생기지 않습니다.
한 객체를 세 가지 역할로 건네기
다음 예제의 CodePost는 부모 클래스 없이 Publishable, Moderatable, Exportable 세 인터페이스를 구현합니다. main은 객체를 하나만 만들고, 그 객체를 매개변수 타입이 서로 다른 세 메서드에 차례로 넘깁니다.
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세 메서드는 같은 codePost 객체를 받지만 매개변수 타입이 각각 Publishable, Moderatable, Exportable입니다. publish가 부르는 post.publish()는 구현 안에서 직접 publish=로그인 구현을 출력합니다. 반면 moderate는 priority()가 돌려준 정수 100에 priority=를 붙여 출력하고, export는 exportText()가 돌려준 문자열 로그인 구현,50을 그대로 출력합니다. 객체를 역할마다 새로 만들거나 복사한 것이 아니라, 한 객체의 참조를 서로 다른 타입의 매개변수로 넘긴 것입니다.
세 클라이언트는 같은 객체에서 자기 역할의 메서드만 호출한다
같은 CodePost를 세 작은 역할 클라이언트에 순서대로 전달하여 publish·priority·export 결과를 얻습니다. 객체는 고정하고 현재 역할/반환값만 바꾸면 각 클라이언트의 좁은 의존을 설명할 수 있습니다.
MultipleInterfaceRoles는 CodePost 한 객체를 Publishable, Moderatable, Exportable 매개변수로 차례로 전달하고 역할마다 다른 메서드와 출력을 사용합니다.
한 객체를 세 역할로 전달하고 필요한 메서드만 호출합니다
MultipleInterfaceRoles · 세 호출 모두 처음 만든 같은 codePost를 전달합니다. 객체를 역할마다 새로 만들지 않습니다.
| 호출 순서 | 매개변수 타입 | 사용하는 메서드 | 콘솔 출력 |
|---|---|---|---|
| 1 · publish(codePost) | Publishable | post.publish() | publish=로그인 구현 |
| 2 · moderate(codePost) | Moderatable | value.priority() | priority=100 |
| 3 · export(codePost) | Exportable | value.exportText() | 로그인 구현,50 |
- 1 · publish(codePost)
- 매개변수 타입:
Publishable사용하는 메서드:post.publish()콘솔 출력:publish=로그인 구현 - 2 · moderate(codePost)
- 매개변수 타입:
Moderatable사용하는 메서드:value.priority()콘솔 출력:priority=100 - 3 · export(codePost)
- 매개변수 타입:
Exportable사용하는 메서드:value.exportText()콘솔 출력:로그인 구현,50
priority()는 정수 100을, exportText()는 문자열을 반환하며 해당 클라이언트가 출력합니다. publish()는 구현 안에서 직접 출력합니다.
세 매개변수는 역할별로 사용할 수 있는 메서드를 제한합니다. 이 예제의 export 클라이언트는 발행이나 우선순위 메서드를 호출하지 않습니다.
이 예의 조건
- main은 동일 CodePost 참조를 publish→moderate→export에 전달합니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 동일한 CodePost 객체 참조를 main의 publish/moderate/export에 3개 role 매개변수로 전달한다. Publishable.publish는 직접 출력하고 priority 100과 exportText의 로그인 구현 50은 호출자가 출력한다. 각 클라이언트는 필요한 계약만 사용한다. |
| 구분 | 내용 |
|---|---|
| 설명 | 동일한 CodePost 객체 참조를 main의 publish/moderate/export에 3개 role 매개변수로 전달한다. Publishable.publish는 직접 출력하고 priority 100과 exportText의 로그인 구현 50은 호출자가 출력한다. 각 클라이언트는 필요한 계약만 사용한다. |
| 전체 읽기 정보 | 세 클라이언트는 같은 객체에서 자기 역할의 메서드만 호출한다 세 클라이언트는 같은 객체에서 자기 역할의 메서드만 호출한다 MultipleInterfaceRoles는 CodePost 한 객체를 Publishable, Moderatable, Exportable 매개변수로 차례로 전달하고 역할마다 다른 메서드와 출력을 사용합니다. 한 객체를 세 역할로 전달하고 필요한 메서드만 호출합니다 MultipleInterfaceRoles · 세 호출 모두 처음 만든 같은 codePost 를 전달합니다. 객체를 역할마다 새로 만들지 않습니다. main의 호출 순서와 클라이언트가 사용하는 계약 호출 순서 매개변수 타입 사용하는 메서드 콘솔 출력 1 · publish(codePost) Publishable post.publish() publish=로그인 구현 2 · moderate(codePost) Moderatable value.priority() priority=100 3 · export(codePost) Exportable value.exportText() 로그인 구현,50 1 · publish(codePost) 매개변수 타입: Publishable 사용하는 메서드: post.publish() 콘솔 출력: publish=로그인 구현 2 · moderate(codePost) 매개변수 타입: Moderatable 사용하는 메서드: value.priority() 콘솔 출력: priority=100 3 · export(codePost) 매개변수 타입: Exportable 사용하는 메서드: value.exportText() 콘솔 출력: 로그인 구현,50 priority() 는 정수 100 을, exportText() 는 문자열을 반환하며 해당 클라이언트가 출력합니다. publish() 는 구현 안에서 직접 출력합니다. 세 매개변수는 역할별로 사용할 수 있는 메서드를 제한합니다. 이 예제의 export 클라이언트는 발행이나 우선순위 메서드를 호출하지 않습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 발행 역할 Publishable 매개변수가 publish를 호출합니다. |
|
|
| 조정 역할 Moderatable 매개변수가 priority를 읽습니다. |
|
|
| 내보내기 역할 Exportable 매개변수가 exportText를 읽습니다. |
|
|
세 클라이언트는 같은 객체에서 자기 역할의 메서드만 호출한다
같은 CodePost를 세 작은 역할 클라이언트에 순서대로 전달하여 publish·priority·export 결과를 얻습니다. 객체는 고정하고 현재 역할/반환값만 바꾸면 각 클라이언트의 좁은 의존을 설명할 수 있습니다.
MultipleInterfaceRoles는 CodePost 한 객체를 Publishable, Moderatable, Exportable 매개변수로 차례로 전달하고 역할마다 다른 메서드와 출력을 사용합니다.
한 객체를 세 역할로 전달하고 필요한 메서드만 호출합니다
MultipleInterfaceRoles · 세 호출 모두 처음 만든 같은 codePost를 전달합니다. 객체를 역할마다 새로 만들지 않습니다.
| 호출 순서 | 매개변수 타입 | 사용하는 메서드 | 콘솔 출력 |
|---|---|---|---|
| 1 · publish(codePost) | Publishable | post.publish() | publish=로그인 구현 |
| 2 · moderate(codePost) | Moderatable | value.priority() | priority=100 |
| 3 · export(codePost) | Exportable | value.exportText() | 로그인 구현,50 |
- 1 · publish(codePost)
- 매개변수 타입:
Publishable사용하는 메서드:post.publish()콘솔 출력:publish=로그인 구현 - 2 · moderate(codePost)
- 매개변수 타입:
Moderatable사용하는 메서드:value.priority()콘솔 출력:priority=100 - 3 · export(codePost)
- 매개변수 타입:
Exportable사용하는 메서드:value.exportText()콘솔 출력:로그인 구현,50
priority()는 정수 100을, exportText()는 문자열을 반환하며 해당 클라이언트가 출력합니다. publish()는 구현 안에서 직접 출력합니다.
세 매개변수는 역할별로 사용할 수 있는 메서드를 제한합니다. 이 예제의 export 클라이언트는 발행이나 우선순위 메서드를 호출하지 않습니다.
이 예의 조건
- main은 동일 CodePost 참조를 publish→moderate→export에 전달합니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 동일한 CodePost 객체 참조를 main의 publish/moderate/export에 3개 role 매개변수로 전달한다. Publishable.publish는 직접 출력하고 priority 100과 exportText의 로그인 구현 50은 호출자가 출력한다. 각 클라이언트는 필요한 계약만 사용한다. |
| 구분 | 내용 |
|---|---|
| 설명 | 동일한 CodePost 객체 참조를 main의 publish/moderate/export에 3개 role 매개변수로 전달한다. Publishable.publish는 직접 출력하고 priority 100과 exportText의 로그인 구현 50은 호출자가 출력한다. 각 클라이언트는 필요한 계약만 사용한다. |
| 전체 읽기 정보 | 세 클라이언트는 같은 객체에서 자기 역할의 메서드만 호출한다 세 클라이언트는 같은 객체에서 자기 역할의 메서드만 호출한다 MultipleInterfaceRoles는 CodePost 한 객체를 Publishable, Moderatable, Exportable 매개변수로 차례로 전달하고 역할마다 다른 메서드와 출력을 사용합니다. 한 객체를 세 역할로 전달하고 필요한 메서드만 호출합니다 MultipleInterfaceRoles · 세 호출 모두 처음 만든 같은 codePost 를 전달합니다. 객체를 역할마다 새로 만들지 않습니다. main의 호출 순서와 클라이언트가 사용하는 계약 호출 순서 매개변수 타입 사용하는 메서드 콘솔 출력 1 · publish(codePost) Publishable post.publish() publish=로그인 구현 2 · moderate(codePost) Moderatable value.priority() priority=100 3 · export(codePost) Exportable value.exportText() 로그인 구현,50 1 · publish(codePost) 매개변수 타입: Publishable 사용하는 메서드: post.publish() 콘솔 출력: publish=로그인 구현 2 · moderate(codePost) 매개변수 타입: Moderatable 사용하는 메서드: value.priority() 콘솔 출력: priority=100 3 · export(codePost) 매개변수 타입: Exportable 사용하는 메서드: value.exportText() 콘솔 출력: 로그인 구현,50 priority() 는 정수 100 을, exportText() 는 문자열을 반환하며 해당 클라이언트가 출력합니다. publish() 는 구현 안에서 직접 출력합니다. 세 매개변수는 역할별로 사용할 수 있는 메서드를 제한합니다. 이 예제의 export 클라이언트는 발행이나 우선순위 메서드를 호출하지 않습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 발행 역할 Publishable 매개변수가 publish를 호출합니다. |
|
|
| 조정 역할 Moderatable 매개변수가 priority를 읽습니다. |
|
|
| 내보내기 역할 Exportable 매개변수가 exportText를 읽습니다. |
|
|
매개변수 타입은 그 메서드 안에서 쓸 수 있는 기능을 정합니다. 부모 타입 변수로는 자식에만 있는 메서드를 부를 수 없었던 것처럼, Exportable 타입 매개변수로는 Exportable에 선언된 exportText()만 부를 수 있습니다. 실제 객체가 priority()를 가지고 있어도 마찬가지입니다. 예외는 toString()처럼 모든 객체가 가진 Object의 메서드뿐인데, 이 메서드들은 어떤 타입의 변수로든 부를 수 있습니다. export 안에서 value.priority()를 부르도록 바꾸면 컴파일러가 오류를 냅니다.
private static void export(Exportable value) {
System.out.println(value.priority()); // 컴파일 오류
}메시지의 핵심 줄만 옮기면 다음과 같습니다. Exportable 타입 변수 value에서 priority()라는 메서드를 찾을 수 없다는 뜻입니다.
error: cannot find symbol
symbol: method priority()
location: variable value of type Exportable이 제한은 불편해 보이지만, 덕분에 export가 무엇에 기대는지가 분명해집니다. export는 exportText() 하나만 쓰므로 발행 방식이나 검수 우선순위 계산이 바뀌어도 영향을 받지 않습니다. 또 발행이나 검수 기능 없이 Exportable 하나만 구현한 클래스의 객체도 export에 넘길 수 있습니다. 만약 세 메서드를 모두 담은 큰 인터페이스 하나를 만들었다면, 내보내기만 필요한 클래스도 발행과 검수 메서드를 억지로 구현해야 했을 것입니다. 이처럼 사용하는 쪽이 쓰지 않는 메서드에 기대지 않도록 인터페이스를 작게 나누라는 원칙을 인터페이스 분리 원칙(interface segregation principle)이라고 합니다.
부모 클래스 하나와 인터페이스 여러 개 함께 쓰기
이번에는 공통 상태를 가진 추상 클래스 Post와 두 인터페이스를 함께 씁니다. CodePost는 Post를 상속해 제목과 조회 수를 물려받고, Moderatable과 Exportable을 구현해 검수와 내보내기 기능을 더합니다.
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=로그인 구현:50main은 CodePost 객체 하나를 만든 뒤 그 참조를 Post, Moderatable, Exportable 타입 변수에 차례로 담습니다. 자식 타입을 부모 타입이나 인터페이스 타입으로 다루는 업캐스팅이므로 형변환 표시가 필요 없고, 네 변수 post, base, moderated, exported는 모두 같은 객체를 가리킵니다.
base.publish()는 실제 객체인 CodePost의 구현을 실행해 브랜치 이름이 들어간 code=feature/login을 출력합니다. moderated.priority()는 100을 돌려주고, exported.exportText()는 부모의 title()과 viewCount()로 읽은 값을 이어 로그인 구현:50을 돌려줍니다. 즉 CodePost 객체 하나를 CodePost, Post, Moderatable, Exportable 네 가지 타입으로 다룰 수 있습니다. 모든 클래스의 조상인 Object까지 치면 다섯 가지입니다.
ClassAndInterfaces에서 CodePost는 Post만 상속하고 Moderatable과 Exportable을 구현합니다. 타입 관계를 객체 세 개의 참조 관계와 혼동하지 않습니다.
부모 클래스 하나와 독립적인 인터페이스 역할 둘을 조합합니다
ClassAndInterfaces의 타입 선언 관계입니다. 화살표는 객체 참조나 실행 순서가 아니라 extends·implements를 뜻합니다.
- CodePost extends Post
- 부모 클래스는 하나입니다. 추상
Post는private title·viewCount와 추상publish()를 선언합니다. - CodePost implements Moderatable
int priority()역할을 구현합니다. 인터페이스는 별도 부모 객체를 만드는 것이 아닙니다.- CodePost implements Exportable
String exportText()역할을 구현합니다.- CodePost가 추가하는 것
private branch필드와publish()·priority()·exportText()의 구현을 제공합니다.
post·base·moderated·exported는 모두 같은 CodePost 객체를 가리킵니다. 부모에서 선언한 상태와 자식의 branch는 별도 객체가 아닙니다.
CodePost.exportText()는 부모의 protected title()·viewCount()를 통해 값을 읽습니다. 부모의 private 필드를 직접 접근하는 것은 아닙니다.
그림의 화살표는 객체 사이의 참조가 아니라 extends와 implements라는 타입 선언 관계입니다. 실제로 만들어지는 객체는 CodePost 하나뿐이고, 그 안에는 Post가 선언한 title과 viewCount, CodePost가 추가한 branch가 함께 들어 있습니다. 인터페이스에는 인스턴스 필드가 없으므로, 인터페이스를 구현한다고 해서 객체에 따로 생기는 부분은 없습니다. 클래스 상속은 상태와 구현을 함께 물려주고, 인터페이스 구현은 주로 그 객체를 다룰 수 있는 타입을 늘려 준다고 정리할 수 있습니다. 인터페이스가 물려줄 수 있는 구현은 앞 글에서 본 default 메서드뿐이고, 다음 절에서는 이 구현이 두 인터페이스에서 겹치는 경우를 봅니다.
두 인터페이스의 default 메서드가 겹칠 때
인터페이스를 여러 개 구현할 때 문제가 생길 수 있는 곳은 구현을 가진 default 메서드입니다. 다음 코드에서 Exportable과 Previewable은 서로 관련 없는 인터페이스인데, 둘 다 이름과 매개변수가 같은 default String label()을 가지고 있습니다. CodePost는 두 인터페이스를 구현하면서 label()을 직접 정의하지 않았습니다.
public final class UnrelatedDefaults {
private interface Exportable {
default String label() { return "export"; }
}
private interface Previewable {
default String label() { return "preview"; }
}
private static final class CodePost implements Exportable, Previewable {
}
}lab/UnrelatedDefaults.java:10: error: types Exportable and Previewable are incompatible;
private static final class CodePost implements Exportable, Previewable {
^
class CodePost inherits unrelated defaults for label() from types Exportable and Previewable
1 error클래스 다중 상속에서 걱정했던 상황이 그대로 나타났습니다. CodePost 객체의 label()을 부르면 "export"와 "preview" 중 무엇을 돌려줘야 할지 정할 수 없습니다. 그래서 컴파일러는 'CodePost가 관련 없는 두 인터페이스에서 label()의 default 구현을 함께 물려받는다'며 두 타입을 함께 구현할 수 없다고 알립니다. 자바는 어느 한쪽을 임의로 고르지 않고, 클래스가 직접 정하게 합니다.
해결 방법은 CodePost에서 label()을 오버라이딩하는 것입니다. 완전히 새로 구현해도 되고, 인터페이스 이름 뒤에 .super를 붙여 특정 인터페이스의 default 구현을 불러 쓸 수도 있습니다.
public final class DefaultRoleChoice {
public static void main(String[] args) {
System.out.println(new CodePost().label());
}
private interface Exportable {
default String label() { return "export"; }
}
private interface Previewable {
default String label() { return "preview"; }
}
private static final class CodePost implements Exportable, Previewable {
@Override public String label() {
return Exportable.super.label() + "+" + Previewable.super.label();
}
}
}export+previewExportable.super.label()은 'Exportable에 있는 label()의 default 구현'을 부르는 문법입니다. 이 문법은 클래스가 implements 뒤에 직접 적은 인터페이스에만 쓸 수 있습니다. CodePost는 두 구현을 모두 불러 +로 이었으므로 export+preview가 출력됩니다. 두 기본 구현을 이렇게 합칠지, 하나만 쓸지, 아예 새로 구현할지는 CodePost가 정한 것입니다. 인터페이스 두 개를 구현했다고 해서 이런 결합 방식이 자동으로 생기지는 않습니다.
이름만 같다고 모두 충돌하는 것은 아닙니다. label()과 label(String prefix)처럼 매개변수가 다르면 서로 다른 메서드, 즉 오버로딩이므로 두 default 메서드를 함께 물려받아도 문제가 없습니다. 충돌은 이름과 매개변수가 모두 같은 메서드의 구현이 두 곳에서 올 때만 생깁니다.
더 깊이: default 메서드 충돌을 푸는 규칙
두 default 구현이 겹친다고 해서 항상 오류가 나는 것은 아닙니다. 자바는 다음 순서로 어느 구현을 쓸지 정합니다.
- 클래스가 이깁니다. 클래스 자신이나 부모 클래스에 같은 메서드의 구현이 있으면, 인터페이스의 default 구현보다 그 구현이 우선합니다.
- 더 구체적인 인터페이스가 이깁니다. 한 인터페이스가 다른 인터페이스를 상속하면서 default 메서드를 다시 정의했다면, 하위 인터페이스의 구현이 쓰입니다.
- 그래도 하나로 정해지지 않으면 클래스가 직접 오버라이딩해야 합니다. 앞의
CodePost가 이 경우입니다.
다음 예제는 1번과 2번 규칙을 보여 줍니다.
public final class DefaultRules {
public static void main(String[] args) {
System.out.println(new NoticePost().label());
System.out.println(new CsvPost().label());
}
private interface Exportable {
default String label() { return "export"; }
}
private interface CsvExportable extends Exportable {
@Override
default String label() { return "csv"; }
}
private static class Post {
public String label() { return "post"; }
}
private static final class NoticePost extends Post implements Exportable {
}
private static final class CsvPost implements Exportable, CsvExportable {
}
}post
csvNoticePost는 부모 클래스 Post의 label()과 Exportable의 default label()을 함께 물려받지만, 클래스의 구현이 이기므로 post가 출력됩니다. 이때 Post의 label()은 인터페이스 메서드를 대신 구현하는 셈이라서 public이어야 합니다. 앞 글에서 본 것처럼 접근 범위가 더 좁으면 컴파일 오류가 납니다. CsvPost는 Exportable과 그 하위 인터페이스 CsvExportable을 함께 구현하는데, CsvExportable이 label()을 다시 정의했으므로 더 구체적인 쪽의 구현이 쓰여 csv가 출력됩니다. 두 클래스 모두 label()을 오버라이딩하지 않았는데도 컴파일됩니다.
이 규칙들을 보면 자바가 다중 상속을 어디까지 허용하는지 알 수 있습니다. Oracle의 Java 튜토리얼은 다중 상속을 상태(필드), 구현(메서드 본문), 타입의 세 가지로 나누어 설명합니다. 자바는 타입의 다중 상속을 처음부터 인터페이스로 허용했고, 구현의 다중 상속은 Java 8의 default 메서드로 제한적으로 허용하면서 위 규칙으로 모호함을 풉니다. 하지만 상태의 다중 상속은 지금도 허용하지 않습니다. 메서드는 클래스가 오버라이딩해 하나로 정할 수 있지만, 여러 부모의 생성자와 메서드가 같은 필드를 각자 초기화하고 바꾸는 상황은 그렇게 정리할 방법이 마땅치 않기 때문입니다. 정확한 규칙은 Java 언어 명세의 클래스의 메서드 상속 규칙에 있고, 이 교재에서는 기본 메서드와 호환성에서 다시 다룹니다.
일부 게시글에만 있는 역할 다루기
이제 게시판으로 돌아가겠습니다. 모든 게시글은 Post를 상속해 발행과 조회 수를 공통으로 제공하지만, 내보내기는 CodePost만 할 수 있다고 합시다. 다음 Board는 다형성과 추상 클래스에서 만든 게시판에 내보내기 기능을 더한 것입니다.
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=90main은 용량이 3인 게시판에 게시글 두 개를 넣고 발행, 내보내기, 요약을 차례로 실행합니다. publishAll은 모든 게시글의 publish()를 부르므로 발행 줄이 두 개 나오고, summary는 두 게시글의 조회 수 40과 50을 더해 count=2, total=90을 만듭니다. 세 반복문 모두 index < size 조건을 쓰므로, 비어 있는 2번 칸은 처음부터 순회하지 않습니다.
차이는 exportAll에서 생깁니다. Board는 게시글을 Post 타입으로 들고 있는데, Post에는 exportText()가 없으니 그대로는 내보낼 수 없습니다. 그래서 앞에서 배운 패턴 매칭으로 post instanceof Exportable exported를 검사합니다. 0번 칸의 TextPost는 Exportable을 구현하지 않으므로 검사 결과가 거짓이고, 아무것도 출력하지 않습니다. 1번 칸의 CodePost는 참이므로 Exportable 타입 변수 exported가 만들어지고 export=로그인 구현,50이 출력됩니다.
Board는 모든 게시글을 발행하고 Exportable만 내보낸다
발행 전체 순회 뒤 exportAll이 TextPost를 건너뛰고 CodePost만 내보낸 다음 summary가 두 원소를 합산합니다. 사용 size=2와 선택 역할의 통과 여부를 실제 순회로 추적할 수 있습니다.
RoleBasedBoard의 size는2, 배열 길이는3입니다. index0 TextPost와 index1 CodePost만 순회하고 CodePost만 Exportable 분기에 들어갑니다.
모든 게시글을 발행하고 Exportable 역할만 내보냅니다
RoleBasedBoard · 두 번의 add 직후 posts.length = 3, size = 2입니다.
| 배열 칸 · 객체 | publishAll() 출력 | exportAll() 판정과 출력 | summary() 합산 |
|---|---|---|---|
| 0 · TextPost | publish=text:가입 인사 | instanceof Exportable은 false. 내보내기 출력 없음. | viewCount() = 40 |
| 1 · CodePost | publish=code:로그인 구현 | instanceof Exportable은 true.
| viewCount() = 50 |
| 2 · null | 순회하지 않음 | 검사하지 않음 | 합산하지 않음 |
- 0 · TextPost
- publishAll() 출력:
publish=text:가입 인사exportAll() 판정과 출력:instanceof Exportable은 false. 내보내기 출력 없음.summary() 합산:viewCount() = 40 - 1 · CodePost
- publishAll() 출력:
publish=code:로그인 구현exportAll() 판정과 출력:instanceof Exportable은 true.export=로그인 구현,50summary() 합산:viewCount() = 50 - 2 · null
- publishAll() 출력: 순회하지 않음exportAll() 판정과 출력: 검사하지 않음summary() 합산: 합산하지 않음
세 반복문 모두 index < size를 사용합니다. 비어 있는 2번 칸은 실제 검사나 메서드 호출 대상이 아닙니다.
main은 publishAll(), exportAll(), 요약 출력 순으로 실행합니다. 마지막 출력은 count=2, total=90이며, 내보내기 가능 여부와 관계없이 두 게시글의 조회 수를 합합니다.
이 예의 조건
- size 2, length 3이며 모든 순회는 미사용 slot 2를 검사하지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | publishAll에서 두 게시글 공통 발행→exportAll의 index 0 Text false/skip, index 1 Code true/export 로그인 구현 50→summary에서 둘의 조회 수 40+50=90, count 2이다. size 2/length 3의 slot 2는 null이며 각 순회에서 검사하지 않는다. |
| 4 | TextPost |
| 5 | CodePost |
| 6 | exportAll · index 0 검사 뒤 index 1 검사 |
| 17 | 세 순회 모두 index < size · slot 2 null을 검사하지 않음 |
| 구분 | 내용 |
|---|---|
| 설명 | publishAll에서 두 게시글 공통 발행→exportAll의 index 0 Text false/skip, index 1 Code true/export 로그인 구현 50→summary에서 둘의 조회 수 40+50=90, count 2이다. size 2/length 3의 slot 2는 null이며 각 순회에서 검사하지 않는다. |
| 전체 읽기 정보 | Board는 모든 게시글을 발행하고 Exportable만 내보낸다 Board는 모든 게시글을 발행하고 Exportable만 내보낸다 RoleBasedBoard의 size는 2, 배열 길이는 3입니다. index 0 TextPost와 index 1 CodePost만 순회하고 CodePost만 Exportable 분기에 들어갑니다. 모든 게시글을 발행하고 Exportable 역할만 내보냅니다 RoleBasedBoard · 두 번의 add 직후 posts.length = 3 , size = 2 입니다. 각 배열 칸에서 공통 처리와 선택 처리가 갈리는 지점 배열 칸 · 객체 publishAll() 출력 exportAll() 판정과 출력 summary() 합산 0 · TextPost publish=text:가입 인사 instanceof Exportable 은 false . 내보내기 출력 없음. viewCount() = 40 1 · CodePost publish=code:로그인 구현 instanceof Exportable 은 true . export=로그인 구현,50 viewCount() = 50 2 · null 순회하지 않음 검사하지 않음 합산하지 않음 0 · TextPost publishAll() 출력: publish=text:가입 인사 exportAll() 판정과 출력: instanceof Exportable 은 false . 내보내기 출력 없음. summary() 합산: viewCount() = 40 1 · CodePost publishAll() 출력: publish=code:로그인 구현 exportAll() 판정과 출력: instanceof Exportable 은 true . export=로그인 구현,50 summary() 합산: viewCount() = 50 2 · null publishAll() 출력: 순회하지 않음 exportAll() 판정과 출력: 검사하지 않음 summary() 합산: 합산하지 않음 세 반복문 모두 index < size 를 사용합니다. 비어 있는 2번 칸은 실제 검사나 메서드 호출 대상이 아닙니다. main 은 publishAll() , exportAll() , 요약 출력 순으로 실행합니다. 마지막 출력은 count=2, total=90 이며, 내보내기 가능 여부와 관계없이 두 게시글의 조회 수를 합합니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 공통 발행 publishAll이 Text와 Code 두 원소를 발행합니다. |
|
|
| 선택 역할 exportAll이 Text false를 건너뛰고 Code true를 내보냅니다. |
|
|
| 전체 집계 summary는 두 원소 조회 수를 모읍니다. |
|
|
Board는 모든 게시글을 발행하고 Exportable만 내보낸다
발행 전체 순회 뒤 exportAll이 TextPost를 건너뛰고 CodePost만 내보낸 다음 summary가 두 원소를 합산합니다. 사용 size=2와 선택 역할의 통과 여부를 실제 순회로 추적할 수 있습니다.
RoleBasedBoard의 size는2, 배열 길이는3입니다. index0 TextPost와 index1 CodePost만 순회하고 CodePost만 Exportable 분기에 들어갑니다.
모든 게시글을 발행하고 Exportable 역할만 내보냅니다
RoleBasedBoard · 두 번의 add 직후 posts.length = 3, size = 2입니다.
| 배열 칸 · 객체 | publishAll() 출력 | exportAll() 판정과 출력 | summary() 합산 |
|---|---|---|---|
| 0 · TextPost | publish=text:가입 인사 | instanceof Exportable은 false. 내보내기 출력 없음. | viewCount() = 40 |
| 1 · CodePost | publish=code:로그인 구현 | instanceof Exportable은 true.
| viewCount() = 50 |
| 2 · null | 순회하지 않음 | 검사하지 않음 | 합산하지 않음 |
- 0 · TextPost
- publishAll() 출력:
publish=text:가입 인사exportAll() 판정과 출력:instanceof Exportable은 false. 내보내기 출력 없음.summary() 합산:viewCount() = 40 - 1 · CodePost
- publishAll() 출력:
publish=code:로그인 구현exportAll() 판정과 출력:instanceof Exportable은 true.export=로그인 구현,50summary() 합산:viewCount() = 50 - 2 · null
- publishAll() 출력: 순회하지 않음exportAll() 판정과 출력: 검사하지 않음summary() 합산: 합산하지 않음
세 반복문 모두 index < size를 사용합니다. 비어 있는 2번 칸은 실제 검사나 메서드 호출 대상이 아닙니다.
main은 publishAll(), exportAll(), 요약 출력 순으로 실행합니다. 마지막 출력은 count=2, total=90이며, 내보내기 가능 여부와 관계없이 두 게시글의 조회 수를 합합니다.
이 예의 조건
- size 2, length 3이며 모든 순회는 미사용 slot 2를 검사하지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | publishAll에서 두 게시글 공통 발행→exportAll의 index 0 Text false/skip, index 1 Code true/export 로그인 구현 50→summary에서 둘의 조회 수 40+50=90, count 2이다. size 2/length 3의 slot 2는 null이며 각 순회에서 검사하지 않는다. |
| 4 | TextPost |
| 5 | CodePost |
| 6 | exportAll · index 0 검사 뒤 index 1 검사 |
| 17 | 세 순회 모두 index < size · slot 2 null을 검사하지 않음 |
| 구분 | 내용 |
|---|---|
| 설명 | publishAll에서 두 게시글 공통 발행→exportAll의 index 0 Text false/skip, index 1 Code true/export 로그인 구현 50→summary에서 둘의 조회 수 40+50=90, count 2이다. size 2/length 3의 slot 2는 null이며 각 순회에서 검사하지 않는다. |
| 전체 읽기 정보 | Board는 모든 게시글을 발행하고 Exportable만 내보낸다 Board는 모든 게시글을 발행하고 Exportable만 내보낸다 RoleBasedBoard의 size는 2, 배열 길이는 3입니다. index 0 TextPost와 index 1 CodePost만 순회하고 CodePost만 Exportable 분기에 들어갑니다. 모든 게시글을 발행하고 Exportable 역할만 내보냅니다 RoleBasedBoard · 두 번의 add 직후 posts.length = 3 , size = 2 입니다. 각 배열 칸에서 공통 처리와 선택 처리가 갈리는 지점 배열 칸 · 객체 publishAll() 출력 exportAll() 판정과 출력 summary() 합산 0 · TextPost publish=text:가입 인사 instanceof Exportable 은 false . 내보내기 출력 없음. viewCount() = 40 1 · CodePost publish=code:로그인 구현 instanceof Exportable 은 true . export=로그인 구현,50 viewCount() = 50 2 · null 순회하지 않음 검사하지 않음 합산하지 않음 0 · TextPost publishAll() 출력: publish=text:가입 인사 exportAll() 판정과 출력: instanceof Exportable 은 false . 내보내기 출력 없음. summary() 합산: viewCount() = 40 1 · CodePost publishAll() 출력: publish=code:로그인 구현 exportAll() 판정과 출력: instanceof Exportable 은 true . export=로그인 구현,50 summary() 합산: viewCount() = 50 2 · null publishAll() 출력: 순회하지 않음 exportAll() 판정과 출력: 검사하지 않음 summary() 합산: 합산하지 않음 세 반복문 모두 index < size 를 사용합니다. 비어 있는 2번 칸은 실제 검사나 메서드 호출 대상이 아닙니다. main 은 publishAll() , exportAll() , 요약 출력 순으로 실행합니다. 마지막 출력은 count=2, total=90 이며, 내보내기 가능 여부와 관계없이 두 게시글의 조회 수를 합합니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 공통 발행 publishAll이 Text와 Code 두 원소를 발행합니다. |
|
|
| 선택 역할 exportAll이 Text false를 건너뛰고 Code true를 내보냅니다. |
|
|
| 전체 집계 summary는 두 원소 조회 수를 모읍니다. |
|
|
그런데 다형성과 추상 클래스에서는 오버라이딩으로 게시글 종류마다 갈라지던 처리를 없앴습니다. 여기서 다시 instanceof로 분기한 것은 그 방향과 어긋나지 않을까요? 차이는 무엇을 묻느냐에 있습니다. 다음처럼 구체 클래스를 하나씩 묻는 분기는 게시글 종류가 늘 때마다 else if를 추가해야 합니다.
if (post instanceof CodePost codePost) {
System.out.println("export=" + codePost.exportText());
} else if (post instanceof VideoPost videoPost) {
System.out.println("export=" + videoPost.exportText());
}반면 exportAll은 '이 게시글이 내보내기 역할을 가졌는가'만 묻습니다. 내보낼 수 있는 VideoPost가 새로 생겨도 Exportable을 구현하기만 하면, exportAll은 한 줄도 고치지 않고 그 게시글을 내보냅니다. 모든 객체에 있는 공통 기능은 오버라이딩으로, 일부 객체에만 있는 선택 기능은 역할 검사로 처리하는 것입니다. 다만 이런 검사가 여러 곳으로 퍼지기 시작하면, 그 기능을 부모에 두는 편이 나은지 또는 내보낼 게시글을 처음부터 따로 모아 두는 편이 나은지 다시 따져 보아야 합니다.
생각해 보기: 내보낼 수 없는 세 번째 게시글
main의 두 번째 add 다음에 board.add(new TextPost("두 번째 인사", 30));을 추가하고 같은 순서로 실행한다고 합시다. 발행 줄과 내보내기 줄은 각각 몇 개 나오고, 마지막 줄은 무엇일까요? 내보낼 수 없는 게시글의 조회 수도 합계에 들어갈까요?
풀이: 발행 줄은 세 개, 내보내기 줄은 CodePost의 한 개이고, 마지막 줄은 count=3, total=120입니다. 세 번째 TextPost는 Exportable을 구현하지 않아 내보내기에서만 빠지고, Post의 공통 기능인 발행과 조회 수 합산에는 그대로 참여합니다. 역할을 가졌는지와 공통 부모의 기능은 서로 따로 판단된다는 뜻입니다. 용량이 3이므로 세 번째 add에서 full 예외는 나지 않습니다.
상속과 인터페이스를 고를 때 던질 질문
7장에서 상속, 추상 클래스, 인터페이스를 차례로 배웠습니다. 실제 설계에서 무엇을 쓸지 정할 때는 다음 질문이 도움이 됩니다.
- 모든 자식을 부모 타입으로 다뤄도 부모의 의미가 그대로 지켜지는가? 그렇지 않은 자식이 있다면 상속 관계가 잘못 잡혔을 수 있습니다.
- 부모의 공통 상태가 정말 모든 자식에게 필요한가? 일부 자식만 쓰는 필드라면 부모에 둘 이유가 없습니다.
- 인터페이스의 모든 메서드를 모든 구현이 자연스럽게 구현할 수 있는가? 예외만 던지는 구현이 생긴다면 인터페이스가 너무 크다는 신호입니다.
- 사용하는 쪽이 쓰지 않는 메서드까지 담긴 큰 인터페이스에 기대고 있지 않은가?
- 새 구현을 추가할 때마다 타입별
if나switch가 늘어나지 않는가? - 상속 대신 다른 객체를 필드로 가지는 구성(composition)이 관계를 더 정확히 나타내지 않는가?
마지막 질문의 구성은 상속과 메서드 오버라이딩에서 '가지고 있다(has-a)' 관계로 소개했습니다. 결국 상속, 추상 클래스, 인터페이스는 코드 중복을 줄이는 도구이기 전에 타입 사이의 관계를 나타내는 도구입니다. 그리고 그 관계에 따라 어떤 코드를 고쳤을 때 영향이 어디까지 퍼지는지가 정해집니다.
정리
- 자바 클래스는 부모를 하나만 상속합니다. 여러 부모의 구현과 필드가 겹칠 때 생기는 모호함을 피하기 위해서입니다.
- 인터페이스는 인스턴스 필드가 없고 추상 메서드에는 구현이 없으므로, 여러 개를 구현해도 그런 모호함이 생기지 않습니다.
class A extends B implements C, D처럼 부모 클래스 하나와 인터페이스 여러 개를 함께 쓸 수 있습니다. - 한 객체를 여러 인터페이스 타입으로 다룰 수 있고, 각 타입의 변수나 매개변수로는 그 인터페이스에 선언된 메서드만 부를 수 있습니다. 사용하는 쪽이 필요한 기능만 알도록 인터페이스를 작게 나눕니다.
- 관련 없는 두 인터페이스의
default메서드가 겹치면 클래스가 오버라이딩해서 직접 정해야 합니다. 이때인터페이스이름.super.메서드()로 직접 구현한 인터페이스의 기본 구현을 부를 수 있습니다. - 모든 객체에 있는 기능은 오버라이딩으로, 일부 객체에만 있는 선택 기능은
instanceof로 역할을 확인해서 처리합니다.
8장의 첫 글 인터페이스로 역할과 구현 분리하기에서는 이렇게 나눈 역할을 설계의 중심에 둡니다. 사용하는 쪽이 구현 클래스 대신 역할에 기대게 하면 그 코드를 고치지 않고도 구현을 바꿔 끼울 수 있다는 것을 게시글 알림 예제로 확인합니다.
연습 문제
문제 1: 두 역할을 함께 구현하기
Moderatable(int priority())과 Exportable(String exportText()) 인터페이스를 만들고, 제목 하나를 가진 PublishedPost가 둘 다 구현하게 하세요. 우선순위는 80이고, 내보내기 문자열은 제목 그대로입니다. moderate와 export 메서드는 각자 필요한 역할 타입만 매개변수로 받아 결과 앞에 priority=와 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=가입 인사같은 post 객체를 두 메서드에 넘겼지만 moderate는 그 객체를 Moderatable로만, export는 Exportable로만 봅니다. 그래서 moderate 안에서는 exportText()를, export 안에서는 priority()를 부를 수 없습니다. 지금의 PublishedPost에는 부모 클래스가 없지만, 나중에 Post를 상속하도록 바꿔도 두 인터페이스를 계속 구현하는 한 두 메서드는 고칠 필요가 없습니다.
문제 2: 어느 줄이 컴파일될까
앞의 MultipleInterfaceRoles에 있는 CodePost를 그대로 쓴다고 할 때, 다음 코드의 (1), (2), (3) 가운데 컴파일되지 않는 줄을 찾고 이유를 설명하세요. 그 줄을 지우면 무엇이 출력될까요?
CodePost codePost = new CodePost("로그인 구현", 50);
Exportable exported = codePost;
System.out.println(exported.exportText()); // (1)
System.out.println(exported.priority()); // (2)
System.out.println(((CodePost) exported).priority()); // (3)해설 보기
컴파일되지 않는 줄은 (2)입니다. exported의 타입인 Exportable에는 exportText()만 선언되어 있으므로, 실제 객체에 priority()가 있어도 컴파일러는 cannot find symbol 오류를 냅니다. 컴파일러는 변수의 타입만 보고 부를 수 있는 메서드를 판단하기 때문입니다.
(1)은 Exportable에 선언된 메서드이므로 문제없고, (3)은 CodePost로 다운캐스팅한 뒤 부르므로 컴파일됩니다. 실행할 때도 exported가 가리키는 객체가 실제로 CodePost이므로 캐스팅이 성공합니다. (2)를 지우고 실행하면 다음과 같이 출력됩니다.
로그인 구현,50
100다만 (3)처럼 다운캐스팅하면 Exportable만 알면 되던 코드가 다시 구체 클래스 CodePost를 알아야 합니다. 우선순위가 필요한 코드라면 처음부터 Moderatable 타입으로 받는 편이 낫습니다.
문제 3: default 메서드 충돌 해결하기
Exportable과 Previewable이 각각 default String label()을 가지고 있고, CodePost가 두 인터페이스를 모두 구현합니다. CodePost의 label()이 preview만 돌려주도록 만드세요. 그리고 CodePost에서 label()을 지우면 어떻게 되는지 설명하세요.
해설 보기
public final class PreviewLabelChoice {
public static void main(String[] args) {
System.out.println(new CodePost().label());
}
private interface Exportable {
default String label() { return "export"; }
}
private interface Previewable {
default String label() { return "preview"; }
}
private static final class CodePost implements Exportable, Previewable {
@Override public String label() {
return Previewable.super.label();
}
}
}previewPreviewable.super.label()은 Previewable의 default 구현을 불러 preview를 돌려줍니다. CodePost에서 label()을 지우면 관련 없는 두 인터페이스에서 같은 label()의 default 구현을 함께 물려받게 되어, 본문에서 본 inherits unrelated defaults 컴파일 오류가 납니다. 자바는 둘 중 하나를 임의로 고르지 않으므로 클래스가 오버라이딩해서 정해야 합니다.
return "preview";라고 직접 써도 출력은 같습니다. 차이는 나중에 드러납니다. Previewable.super.label()을 부르면 Previewable의 기본 구현이 바뀔 때 CodePost도 함께 따라가고, 문자열을 직접 쓰면 Previewable이 바뀌어도 CodePost는 그대로입니다.