추상 클래스와 인터페이스
공통 상태를 가진 추상 클래스와 구현 계약만 제공하는 인터페이스를 비교하고 Study Task 역할을 여러 구현에 적용합니다.
추상 클래스는 공통 상태와 구현을 물려주는 클래스 계층입니다. 인터페이스는 구현 클래스가 따라야 할 역할 계약을 정의하며 클래스 상속 관계와 별도로 적용할 수 있습니다. 둘 다 직접 인스턴스화할 수 없는 타입으로 다형적 참조에 사용되지만 목적과 제약이 다릅니다.
인터페이스 구현 의무
public final class IncompleteStudyTask {
private interface StudyTask {
void start();
int estimatedMinutes();
}
private static final class ReadingTask implements StudyTask {
@Override
public void start() {
System.out.println("read");
}
}
}error: ReadingTask is not abstract and does not override abstract method estimatedMinutes()ReadingTask를 구체 클래스로 선언했으므로 StudyTask의 모든 추상 메서드를 구현해야 합니다.
구현을 다음 자식에 맡기려면 ReadingTask도 abstract로 선언해야 합니다.
인터페이스는 구현 누락을 컴파일 경계에서 찾습니다.
인터페이스 메서드는 기본적으로 public abstract입니다.
구현 메서드의 접근 범위를 package-private으로 줄이면 public 계약을 만족하지 못합니다.
@Override public을 명시하면 외부 역할 구현이라는 사실이 드러납니다.
추상 클래스의 공통 구현
public final class AbstractActivityTemplate {
public static void main(String[] args) {
StudyActivity activity = new CodingActivity("Spring", 50);
activity.run();
}
private abstract static class StudyActivity {
private final String topic;
private final int minutes;
StudyActivity(String topic, int minutes) {
this.topic = topic;
this.minutes = minutes;
}
final void run() {
System.out.println("start=" + topic);
perform();
System.out.println("saved=" + minutes);
}
protected abstract void perform();
}
private static final class CodingActivity extends StudyActivity {
CodingActivity(String topic, int minutes) {
super(topic, minutes);
}
@Override
protected void perform() {
System.out.println("write code");
}
}
}start=Spring
write code
saved=50부모 run은 공통 실행 순서를 완성하고 중간 perform만 추상 메서드로 자식에 맡깁니다.
run을 final로 두어 자식이 전체 순서를 깨뜨리지 못하게 했습니다.
추상 클래스는 생성자와 인스턴스 필드를 가질 수 있어 공통 상태 초기화에 적합합니다.
인터페이스의 역할 부여
public final class StudyTaskInterface {
public static void main(String[] args) {
StudyTask[] tasks = {
new ReadingTask("Java"),
new CodingTask("Spring")
};
for (StudyTask task : tasks) {
task.start();
System.out.println("estimate=" + task.estimatedMinutes());
}
}
private interface StudyTask {
void start();
int estimatedMinutes();
}
private static final class ReadingTask implements StudyTask {
private final String book;
ReadingTask(String book) { this.book = book; }
@Override public void start() { System.out.println("read=" + book); }
@Override public int estimatedMinutes() { return 40; }
}
private static final class CodingTask implements StudyTask {
private final String project;
CodingTask(String project) { this.project = project; }
@Override public void start() { System.out.println("code=" + project); }
@Override public int estimatedMinutes() { return 50; }
}
}read=Java
estimate=40
code=Spring
estimate=50두 클래스는 필드 구조와 상속 부모가 달라도 StudyTask 역할을 구현할 수 있습니다.
클라이언트는 구체 클래스가 아니라 start와 estimatedMinutes에 정의된 기능만 사용합니다.
클래스가 인터페이스를 구현할 때는 implements를 사용합니다.
인터페이스의 필드는 모두 public static final 상수입니다.
인스턴스 상태를 저장하지 않습니다.
메서드는 추상 메서드 외에도 default, static, private 메서드를 가질 수 있지만 인터페이스의 중심은 역할 계약입니다.
추상 클래스와 인터페이스
| 질문 | 추상 클래스 | 인터페이스 |
|---|---|---|
| 공통 인스턴스 상태가 필요한가 | 적합 | 상태 필드 없음 |
| 공통 생성 과정이 필요한가 | 생성자 제공 | 생성자 없음 |
| 단일 계층인가 | 클래스 상속 하나 | 여러 역할 구현 가능 |
| 관련 없는 타입도 계약을 가져야 하는가 | 부자연스러울 수 있음 | 적합 |
| 공통 구현이 필요한가 | 일반 메서드 | default 제한적 사용 |
하나를 무조건 고르는 대신 함께 사용할 수 있습니다.
AbstractStudyActivity가 공통 상태를 제공하고 Exportable 인터페이스가 내보내기 역할을 추가하는 식입니다.
클래스 상속은 하나만 가능하지만 인터페이스는 여러 개 구현할 수 있습니다.
인터페이스 이름은 구현 기술보다 역할을 표현합니다.
JsonStudyTask처럼 특정 형식을 고정하기보다 Exportable, Trackable, StudyTask처럼 클라이언트가 기대하는 능력을 나타냅니다.
실행기 인터페이스
public final class TaskRunner {
public static void main(String[] args) {
Runner runner = new Runner();
runner.run(new ReadingTask());
runner.run(new CodingTask());
}
private static final class Runner {
void run(StudyTask task) {
System.out.println("runner:start");
TaskResult result = task.execute();
System.out.println("runner:saved=" + result.minutes());
}
}
private interface StudyTask {
TaskResult execute();
}
private record TaskResult(String topic, int minutes) {}
private static final class ReadingTask implements StudyTask {
@Override
public TaskResult execute() {
System.out.println("reading");
return new TaskResult("Java", 40);
}
}
private static final class CodingTask implements StudyTask {
@Override
public TaskResult execute() {
System.out.println("coding");
return new TaskResult("Spring", 50);
}
}
}runner:start
reading
runner:saved=40
runner:start
coding
runner:saved=50Runner는 작업 종류를 묻지 않고 execute 결과만 받습니다.
새 작업은 StudyTask를 구현해 기존 Runner에 전달합니다.
구체 구현 생성은 main 같은 조립점이 담당합니다.
연습 문제
StudyNotifier 인터페이스에 notify(String message)를 선언하고 ConsoleNotifier와 SilentNotifier를 구현하세요.
같은 send 메서드에 두 구현을 전달해 동작 차이를 확인합니다.
해설 보기
public final class StudyNotifierInterface {
public static void main(String[] args) {
send(new ConsoleNotifier(), "session complete");
send(new SilentNotifier(), "session complete");
}
private static void send(StudyNotifier notifier, String message) {
notifier.notify(message);
}
private interface StudyNotifier {
void notify(String message);
}
private static final class ConsoleNotifier implements StudyNotifier {
@Override public void notify(String message) {
System.out.println("console=" + message);
}
}
private static final class SilentNotifier implements StudyNotifier {
@Override public void notify(String message) {
System.out.println("silent");
}
}
}console=session complete
silentsend는 알림 구현을 모르고 인터페이스 역할만 호출합니다.
SilentNotifier도 계약을 구현하므로 조건 분기 없이 교체됩니다.