추상 클래스와 인터페이스
공통 상태를 가진 추상 클래스와 구현 계약만 제공하는 인터페이스를 비교하고 게시글 발행 역할을 여러 구현에 적용합니다.
추상 클래스는 공통 상태와 구현을 물려주는 클래스 계층입니다.
인터페이스는 구현 클래스가 따라야 할 역할 계약을 정의하며 클래스 상속 관계와 별도로 적용할 수 있습니다.
둘 다 직접 인스턴스화할 수 없는 타입으로 다형적 참조에 사용되지만 목적과 제약이 다릅니다.
인터페이스 구현 의무
public final class IncompletePublishablePost {
private interface Publishable {
String title();
void publish();
}
private static final class TextPost implements Publishable {
@Override
public void publish() {
System.out.println("publish text post");
}
}
}error: TextPost is not abstract and does not override abstract method title()TextPost를 구체 클래스로 선언했으므로 Publishable의 모든 추상 메서드를 구현해야 합니다.
구현을 다음 자식에 맡기려면 TextPost도 abstract로 선언해야 합니다.
인터페이스는 구현 누락을 컴파일 경계에서 찾습니다.
인터페이스 메서드는 기본적으로 public abstract입니다.
구현 메서드의 접근 범위를 package-private으로 줄이면 public 계약을 만족하지 못합니다.
@Override public을 명시하면 외부 역할 구현이라는 사실이 드러납니다.
추상 클래스의 공통 구현
public final class AbstractPostTemplate {
public static void main(String[] args) {
Post post = new CodePost("로그인 구현", 50);
post.publish();
}
private abstract static class Post {
private final String title;
private final int viewCount;
Post(String title, int viewCount) {
this.title = title;
this.viewCount = viewCount;
}
final void publish() {
System.out.println("validate=" + title);
renderBody();
System.out.println("saved=" + viewCount);
}
protected abstract void renderBody();
}
private static final class CodePost extends Post {
CodePost(String title, int viewCount) {
super(title, viewCount);
}
@Override
protected void renderBody() {
System.out.println("body=code");
}
}
}validate=로그인 구현
body=code
saved=50부모 publish는 공통 발행 순서를 완성하고 중간 본문 렌더링만 추상 메서드로 자식에 맡깁니다.
publish를 final로 두어 자식이 전체 순서를 깨뜨리지 못하게 했습니다.
추상 클래스는 생성자와 인스턴스 필드를 가질 수 있어 공통 상태 초기화에 적합합니다.
인터페이스의 역할 부여
public final class PublishableInterface {
public static void main(String[] args) {
Publishable[] posts = {
new TextPost("가입 인사"),
new CodePost("로그인 구현")
};
for (Publishable post : posts) {
System.out.println("title=" + post.title());
post.publish();
}
}
private interface Publishable {
String title();
void publish();
}
private static final class TextPost implements Publishable {
private final String title;
TextPost(String title) { this.title = title; }
@Override public String title() { return title; }
@Override public void publish() { System.out.println("text=" + title); }
}
private static final class CodePost implements Publishable {
private final String title;
CodePost(String title) { this.title = title; }
@Override public String title() { return title; }
@Override public void publish() { System.out.println("code=" + title); }
}
}title=가입 인사
text=가입 인사
title=로그인 구현
code=로그인 구현두 클래스는 필드 구조와 상속 부모가 달라도 Publishable 역할을 구현할 수 있습니다.
클라이언트는 구체 클래스가 아니라 title과 publish에 정의된 기능만 사용합니다.
클래스가 인터페이스를 구현할 때는 implements를 사용합니다.
인터페이스의 필드는 모두 public static final 상수입니다.
인스턴스 상태를 저장하지 않습니다.
메서드는 추상 메서드 외에도 default, static, private 메서드를 가질 수 있지만 인터페이스의 중심은 역할 계약입니다.
추상 클래스와 인터페이스
| 질문 | 추상 클래스 | 인터페이스 |
|---|---|---|
| 공통 인스턴스 상태가 필요한가 | 적합 | 상태 필드 없음 |
| 공통 생성 과정이 필요한가 | 생성자 제공 | 생성자 없음 |
| 단일 계층인가 | 클래스 상속 하나 | 여러 역할 구현 가능 |
| 관련 없는 타입도 계약을 가져야 하는가 | 부자연스러울 수 있음 | 적합 |
| 공통 구현이 필요한가 | 일반 메서드 | default 제한적 사용 |
하나를 무조건 고르는 대신 함께 사용할 수 있습니다.
AbstractPost가 공통 상태를 제공하고 Exportable 인터페이스가 내보내기 역할을 추가하는 식입니다.
클래스 상속은 하나만 가능하지만 인터페이스는 여러 개 구현할 수 있습니다.
인터페이스 이름은 구현 기술보다 역할을 표현합니다.
JsonPost처럼 특정 형식을 고정하기보다 Exportable, Moderatable, Publishable처럼 클라이언트가 기대하는 능력을 나타냅니다.
발행기 인터페이스
public final class PublicationRunner {
public static void main(String[] args) {
Publisher publisher = new Publisher();
publisher.publish(new TextPost());
publisher.publish(new CodePost());
}
private static final class Publisher {
void publish(Publishable post) {
System.out.println("publisher:validate");
PublishResult result = post.publish();
System.out.println("publisher:saved=" + result.viewCount());
}
}
private interface Publishable {
PublishResult publish();
}
private record PublishResult(String title, int viewCount) {}
private static final class TextPost implements Publishable {
@Override
public PublishResult publish() {
System.out.println("publish text");
return new PublishResult("가입 인사", 40);
}
}
private static final class CodePost implements Publishable {
@Override
public PublishResult publish() {
System.out.println("publish code");
return new PublishResult("로그인 구현", 50);
}
}
}publisher:validate
publish text
publisher:saved=40
publisher:validate
publish code
publisher:saved=50Publisher는 게시글 종류를 묻지 않고 publish 결과만 받습니다.
새 게시글은 Publishable을 구현해 기존 Publisher에 전달합니다.
구체 구현 생성은 main 같은 조립점이 담당합니다.
연습 문제
PostNotifier 인터페이스에 notify(String message)를 선언하고 ConsoleNotifier와 SilentNotifier를 구현하세요.
같은 send 메서드에 두 구현을 전달해 동작 차이를 확인합니다.
해설 보기
public final class PostNotifierInterface {
public static void main(String[] args) {
send(new ConsoleNotifier(), "post published");
send(new SilentNotifier(), "post published");
}
private static void send(PostNotifier notifier, String message) {
notifier.notify(message);
}
private interface PostNotifier {
void notify(String message);
}
private static final class ConsoleNotifier implements PostNotifier {
@Override public void notify(String message) {
System.out.println("console=" + message);
}
}
private static final class SilentNotifier implements PostNotifier {
@Override public void notify(String message) {
System.out.println("silent");
}
}
}console=post published
silentsend는 알림 구현을 모르고 인터페이스 역할만 호출합니다.
SilentNotifier도 계약을 구현하므로 조건 분기 없이 교체됩니다.