ArrayList<String> names = new ArrayList<>(); // 구현체 타입
CardGateway gateway = new CardGateway(); // 구현체 타입나중에 LinkedList나 BankTransferGateway로 바꾸려면 변수 타입까지 고쳐야 하고, 그 변수를 받는 메서드 시그니처도 연쇄적으로 바뀐다. 다형성의 이점을 스스로 버리는 것이다.
List<String> names = new ArrayList<>(); // 인터페이스 타입
PaymentGateway gateway = new CardGateway(); // 계약 타입interface AppConstants {
String DB_URL = "jdbc:...";
int TIMEOUT = 30;
}
class UserService implements AppConstants { /* DB_URL 쓰려고 구현 */ }UserService는 AppConstants라는 "능력"을 갖는 게 아니다. 상수를 쓰려고 타입 계층을 오염시키는 안티패턴(상수 인터페이스 안티패턴)이다.
final class AppConstants {
private AppConstants() {}
static final String DB_URL = "jdbc:...";
static final int TIMEOUT = 30;
}
// 사용: AppConstants.DB_URL 또는 import staticabstract class Exporter {
public void export() { validate(); write(); notifyDone(); } // final 아님
}
class LazyExporter extends Exporter {
@Override
public void export() { write(); } // validate 건너뜀 — 골격이 무너짐
}템플릿 메서드의 존재 이유는 "순서를 보장"하는 것이다. 자식이 오버라이딩할 수 있으면 보장이 사라진다.
abstract class Exporter {
public final void export() { validate(); write(); notifyDone(); }
protected abstract void write();
protected void validate() {} // 훅으로 세부 조정만 허용
protected void notifyDone() {}
}abstract class BaseEntity {
Long id;
abstract void validate();
abstract String toCsv();
abstract void sendNotification(); // 엔티티가 알림까지?
}하나의 부모에 여러 책임을 넣으면 모든 자식이 필요 없는 메서드까지 구현해야 한다(빈 구현이 늘어난다). 능력은 인터페이스로 잘게 나눈다(인터페이스 분리 원칙).
abstract class BaseEntity { Long id; }
interface Validatable { void validate(); }
interface CsvExportable { String toCsv(); }
class Order extends BaseEntity implements Validatable, CsvExportable { /* 필요한 것만 */ }interface Cache {
default void clearAll() {
// 구현체에 어떤 저장소가 있는지 모르는데 "다 지운다"는 건 할 수 없음
throw new UnsupportedOperationException(); // 이런 default는 함정
}
}인터페이스는 상태가 없다. default 메서드는 다른 추상 메서드를 조합해서 구현할 수 있는 것만 제공해야 한다.
interface Cache {
Set<String> keys();
void remove(String key);
default void clearAll() { // 계약된 메서드만으로 구현
for (String k : Set.copyOf(keys())) remove(k);
}
}