새 기능의 뼈대를 설계할 때 두 가지 선택지가 생긴다. interface와 abstract class다. 겉보기엔 둘 다 "완성되지 않은 것을 강제하는 구조"처럼 보이지만, 목적이 다르다.
interface는 "이 기능을 할 수 있다"는 약속이다. 구현 방식은 각 클래스가 결정하고, interface는 어떤 메서드를 반드시 가져야 하는지만 정의한다.
interface Runnable {
void run(); // 구현 방식은 각 클래스에서 결정
}
여러 개를 동시에 구현할 수 있다는 게 핵심 특성이다. Dog가 달릴 수도 있고 비교도 가능하게 만들려면 두 interface를 함께 implements하면 된다.
class Dog implements Runnable, Comparable<Dog> { ... }
단, interface에서 변수를 선언하면 자동으로 public static final이 붙는다. interface는 인스턴스를 직접 만들지 않기 때문에, 변수가 어느 시점에 초기화될지 보장할 수 없다. 그래서 컴파일러가 강제로 상수로 만든다.
interface Counter {
int count = 0; // 실제로는 public static final int count = 0;
}
이 말은 Counter를 구현한 클래스 10개가 있어도 모두 같은 count를 공유한다는 뜻이다. 인스턴스마다 독립적인 상태를 가질 수 없다.
abstract class는 "이 계열이다"라는 공통 구조다. 자식 클래스들이 공유하는 상태와 동작을 하나의 부모로 묶어두는 방식이다.
abstract class Animal {
private String name; // 인스턴스마다 독립적으로 존재
public Animal(String name) { this.name = name; }
public String getName() { return name; }
abstract void sound(); // 각 자식이 구현
}
인스턴스 변수를 가질 수 있고, 생성자를 통해 초기화할 수 있다. 자식 클래스들이 각자의 name을 가지면서 조회 로직은 부모에서 공통으로 처리된다.
단 extends로 하나만 상속할 수 있다. Dog extends Animal, Pet은 불가능하다.
Java 8에서 interface에 default method가 추가됐다. 이전까지는 abstract class만 기본 구현을 제공할 수 있었는데, interface도 가능해진 것이다.
interface Greeter {
void greet(String name);
default void greetAll(List<String> names) { // Java 8~
names.forEach(this::greet);
}
}
Java 9에서는 private method까지 추가됐다. interface 내부에서만 쓰는 공통 로직을 분리할 수 있다.
이 변화로 "그냥 다 interface로 하면 안 되나?"는 생각이 들 수 있다. 메서드 구현까지 가능해졌으니 abstract class 대신 쓸 수 있을 것 같지만, 인스턴스 상태 문제는 해결되지 않았다.
공통으로 관리해야 할 인스턴스 상태가 있는지 없는지가 기준이다.
Comparable, Iterable, Runnable은 계열이나 상태와 무관하게 어떤 클래스든 구현할 수 있는 역할이다. interface가 맞다.
HttpServlet, AbstractList처럼 내부 상태와 공통 동작을 함께 묶어야 하는 경우는 abstract class가 맞다. 상태를 공유해야 하는 구조라면 interface로는 애초에 해결이 안 된다.
다중 구현이 가능한 interface가 유리한 경우가 더 많다. abstract class는 상태 공유가 필요할 때만 선택한다.