이펙티브 자바 완벽 공략 2부
아이템 21. 인터페이스는 구현하는 쪽을 생각해 설계하라.
아이템 21. 인터페이스는 구현하는 쪽을 생각해 설계하라.
- 아이템 21. 인터페이스는 구현하는 쪽을 생각해 설계하라.
- 아이템 21. 핵심 정리
- 아이템 21. 인터페이스는 구현하는 쪽을 생각해 설계하라
- default 메서드는 기존 구현체를 깨뜨리지 않는 것처럼 보인다
- 대표적인 예가 Collection.removeIf()
- 문제는 구현체마다 특별한 규칙이 있을 수 있다는 것이다
- default 메서드는 구현 클래스의 특별한 규칙을 모른다
- 인터페이스 관점에서는 정상인데 구현체 관점에서는 위험할 수 있다
- 구현 클래스가 default 메서드를 재정의해야 할 수도 있다
- 내가 관리하는 코드라면 대응할 수 있다
- 공개된 인터페이스라면 상황이 다르다
- 컴파일 오류 없이 동작이 바뀐다는 것이 더 위험하다
- default 메서드는 문제를 숨길 수 있다
- binary compatibility와 behavioral compatibility는 다르다
- default 메서드는 런타임 오류를 유발할 수도 있다
- 기존 상위 클래스
- 마커 성격의 인터페이스
- 하위 클래스
- 인터페이스에 default hello()가 추가된다면?
- default 메서드가 없었다면 더 일찍 발견할 수도 있다
- 컴파일 오류와 런타임 오류
- 개발자는 가능하면 컴파일 타임 실패를 선호한다
- default 메서드의 위험은 “조용히 들어온다”는 데 있다
- 새로운 default 메서드는 구현체의 불변식을 깨뜨릴 수 있다
- 인터페이스 설계자는 구현체를 예상해야 한다
- default 메서드를 추가하기 전 확인해야 할 것
- 새 인터페이스를 설계할 때도 default 메서드를 과신하면 안 된다
- default 메서드는 인터페이스 진화를 위한 도구다
- 인터페이스 변경의 영향 범위
- 컴파일 호환성이 전부가 아니다
- 실무에서 public 인터페이스를 설계한다면
- 테스트를 통해 기존 구현체와의 호환성을 검증해야 한다
- 아이템 20과 아이템 21을 함께 이해하자
- 핵심 정리
- 한 줄 요약
- 아이템 21. 핵심 정리
아이템 21. 핵심 정리
아이템 21. 인터페이스는 구현하는 쪽을 생각해 설계하라
Java 8부터 인터페이스에 default 메서드를 정의할 수 있게 되면서 기존 인터페이스를 훨씬 유연하게 확장할 수 있게 되었다.
예를 들어 이미 널리 사용되고 있는 인터페이스가 있다고 하자.
public interface MyService {
void execute();
}
시간이 지나 새로운 기능이 필요해졌다고 가정해보자.
기존 방식대로 추상 메서드를 추가하면
public interface MyService {
void execute();
void validate();
}
기존의 모든 구현 클래스가 validate()를 구현해야 한다.
하지만 default 메서드를 사용하면 기존 구현체를 깨뜨리지 않고 기능을 추가할 수 있다.
public interface MyService {
void execute();
default void validate() {
// 기본 검증
}
}
겉으로 보면 굉장히 편리하다.
하지만 바로 이 편리함 때문에 새로운 문제가 생길 수도 있다.
default 메서드를 기존 인터페이스에 추가한다는 것은 사실상
그 인터페이스를 구현하고 있는 모든 클래스에 새로운 동작을 강제로 주입하는 것
과 비슷하기 때문이다.
그래서 인터페이스를 설계하거나 이미 공개된 인터페이스에 default 메서드를 추가할 때는 인터페이스 자체만 볼 것이 아니라 그 인터페이스를 구현하고 있는 클래스들의 특성까지 고려해야 한다.
default 메서드는 기존 구현체를 깨뜨리지 않는 것처럼 보인다
default 메서드의 가장 큰 장점은 기존 구현 클래스가 새로운 메서드를 직접 구현하지 않아도 된다는 것이다.
public interface Worker {
void work();
default void stop() {
System.out.println("stop");
}
}
기존 구현체는 그대로 유지된다.
public class Developer
implements Worker {
@Override
public void work() {
System.out.println("develop");
}
}
다음 코드도 바로 사용할 수 있다.
Worker worker =
new Developer();
worker.stop();
컴파일 오류가 발생하지 않는다.
이 점만 보면 매우 안전한 변경처럼 느껴진다.
하지만 중요한 문제는 다음이다.
"컴파일된다."
≠
"기존 구현체의 의미와도 잘 맞는다."
기존 구현체가 특별한 불변식이나 동기화 정책을 가지고 있다면 새롭게 추가된 default 메서드가 그 규칙을 알지 못한 채 동작할 수 있다.
대표적인 예가 Collection.removeIf()
Collection에는 다음과 같은 default 메서드가 있다.
default boolean removeIf(
Predicate<? super E> filter
) {
Objects.requireNonNull(filter);
boolean removed = false;
final Iterator<E> each =
iterator();
while (each.hasNext()) {
if (filter.test(each.next())) {
each.remove();
removed = true;
}
}
return removed;
}
개념적으로는 컬렉션의 요소를 순회하면서 조건에 맞는 요소를 삭제한다.
사용 방법도 매우 편리하다.
List<Integer> numbers =
new ArrayList<>(
List.of(1, 2, 3, 4, 5)
);
numbers.removeIf(
number -> number % 2 == 0
);
결과는 다음과 같다.
1
3
5
Collection이라는 인터페이스의 관점에서 보면 굉장히 자연스럽고 유용한 기능이다.
문제는 구현체마다 특별한 규칙이 있을 수 있다는 것이다
인터페이스는 자신을 구현하는 모든 클래스의 내부 정책을 알 수 없다.
예를 들어 어떤 Collection 구현체가 모든 메서드 호출을 동기화한다고 생각해보자.
public class SynchronizedCollection<E>
implements Collection<E> {
private final Collection<E> collection;
private final Object lock =
new Object();
@Override
public boolean add(E e) {
synchronized (lock) {
return collection.add(e);
}
}
@Override
public boolean remove(Object o) {
synchronized (lock) {
return collection.remove(o);
}
}
// ...
}
이 클래스의 중요한 정책은 다음과 같다.
모든 Collection 연산
↓
동일한 Lock 획득
↓
한 번에 하나의 스레드만 실행
즉 단순히 Collection을 구현했다는 것이 전부가 아니다.
이 클래스에는
"모든 접근은 동기화되어야 한다."
라는 추가적인 불변식이 존재한다.
default 메서드는 구현 클래스의 특별한 규칙을 모른다
만약 Collection에 새로운 default 메서드가 추가된다면 기존 SynchronizedCollection은 별도의 코드를 작성하지 않아도 해당 메서드를 자동으로 가지게 된다.
collection.removeIf(
value -> ...
);
문제는 인터페이스의 기본 구현이 SynchronizedCollection의 락 정책을 알지 못할 수 있다는 점이다.
기존 클래스의 메서드들은 다음처럼 작성되어 있을 수 있다.
synchronized (lock) {
// 실제 작업
}
하지만 새로 들어온 default 메서드에는 그러한 동기화가 없다.
구조적으로 보면 다음과 같다.
기존 메서드
add()
remove()
clear()
...
↓
동기화 적용
반면 새 default 메서드는
removeIf()
↓
동기화 정책을 모름
이 될 수 있다.
이 경우 클래스가 원래 보장하려던 멀티스레드 안정성이 훼손될 수 있다.
인터페이스 관점에서는 정상인데 구현체 관점에서는 위험할 수 있다
이것이 아이템 21의 핵심 중 하나다.
인터페이스를 작성하는 입장에서는 다음과 같이 생각하기 쉽다.
Collection이면 요소를 순회할 수 있다.
Iterator가 있으니 삭제할 수 있다.
따라서 removeIf() 기본 구현을 제공할 수 있다.
인터페이스 계약만 생각하면 완벽하게 합리적이다.
하지만 특정 구현 클래스에서는 다음과 같은 추가 조건이 있을 수 있다.
모든 연산은 동기화되어야 한다.
모든 변경은 특정 검증을 거쳐야 한다.
모든 변경에는 로그가 기록되어야 한다.
모든 접근에는 권한 검사가 필요하다.
default 메서드는 이러한 구현체별 규칙을 자동으로 알 수 없다.
구현 클래스가 default 메서드를 재정의해야 할 수도 있다
이 문제를 해결하려면 해당 구현체가 새로운 default 메서드를 직접 재정의하여 자신의 정책에 맞춰야 한다.
예를 들어 동기화가 필요하다면 다음과 같이 만들 수 있다.
@Override
public boolean removeIf(
Predicate<? super E> filter
) {
synchronized (lock) {
return collection.removeIf(filter);
}
}
이렇게 하면 기존 클래스의 동기화 정책을 유지할 수 있다.
문제는
"새로운 default 메서드가 추가되었다."
는 사실을 구현체 개발자가 알아야 한다는 것이다.
내가 관리하는 코드라면 대응할 수 있다
인터페이스와 구현체를 모두 직접 관리하고 있다면 상대적으로 쉽다.
예를 들어
my-library
├─ MyCollection
├─ MyArrayCollection
├─ MySynchronizedCollection
└─ MyReadOnlyCollection
을 모두 같은 팀에서 관리하고 있다고 하자.
MyCollection에 새로운 default 메서드를 추가하면서 관련 구현체를 모두 검사할 수 있다.
default method 추가
↓
모든 구현체 점검
↓
필요한 클래스에서 override
이렇게 대응할 수 있다.
공개된 인터페이스라면 상황이 다르다
하지만 public 인터페이스는 누가 구현하고 있는지 모두 알 수 없다.
public interface MyLibraryInterface {
}
이 인터페이스를 다음 사람들이 사용할 수 있다.
우리 서비스
다른 사내 프로젝트
외부 라이브러리
오픈소스 프로젝트
서드파티 애플리케이션
라이브러리 제공자는 모든 구현체를 확인할 수 없다.
그런 상황에서 다음 코드를 추가한다.
default void newOperation() {
}
컴파일 오류도 발생하지 않는다.
기존 구현 클래스에 자동으로 새로운 메서드가 추가된다.
바로 이 특성이 위험하다.
컴파일 오류 없이 동작이 바뀐다는 것이 더 위험하다
일반적인 인터페이스 메서드를 추가하면 기존 구현체가 컴파일되지 않는다.
public interface Worker {
void work();
void stop();
}
기존 클래스가 stop()을 구현하지 않았다면 컴파일러가 알려준다.
컴파일 실패
↓
인터페이스 변경 사실 발견
↓
구현체 수정
개발자 입장에서는 불편해 보이지만 오히려 안전한 측면이 있다.
문제를 빠르게 발견하기 때문이다.
default 메서드는 문제를 숨길 수 있다
반면 다음처럼 추가하면
default void stop() {
}
기존 구현 클래스가 컴파일된다.
인터페이스 변경
↓
default method 추가
↓
기존 구현체 정상 컴파일
↓
문제 없음처럼 보임
그러나 실제 실행 시점에는 클래스의 기존 규칙과 충돌할 수 있다.
이런 종류의 변화는 발견하기 더 어렵다.
따라서
컴파일 호환성을 유지한다고 해서 의미적 호환성까지 자동으로 유지되는 것은 아니다.
라는 사실을 기억해야 한다.
binary compatibility와 behavioral compatibility는 다르다
이 문제를 조금 더 일반적인 관점에서 보면 다음 두 가지를 구분할 수 있다.
코드가 계속 컴파일되는가?
vs
코드가 기존과 동일한 의미로 동작하는가?
default 메서드는 첫 번째 문제를 상당 부분 해결해준다.
하지만 두 번째 문제까지 자동으로 해결해주는 것은 아니다.
예를 들어
SynchronizedCollection
이 계속 컴파일된다고 하더라도
"모든 연산이 동기화된다."
라는 기존 클래스의 의미가 유지된다는 보장은 별개의 문제다.
default 메서드는 런타임 오류를 유발할 수도 있다
더 까다로운 경우도 있다.
기존에는 컴파일 오류가 발생했을 코드가 default 메서드 추가 이후에는 컴파일되지만, 실행 시점에 오류가 발생할 가능성이 생길 수 있다.
개념적인 구조를 살펴보자.
기존 상위 클래스
public class SuperClass {
private void hello() {
System.out.println(
"SuperClass hello"
);
}
}
hello()는 private이므로 하위 클래스에서는 사용할 수 없다.
마커 성격의 인터페이스
처음에는 아무 메서드도 없다고 가정한다.
public interface MarkerInterface {
}
하위 클래스
public class SubClass
extends SuperClass
implements MarkerInterface {
}
이 상태에서 다음과 같이 호출한다면
SubClass subClass =
new SubClass();
subClass.hello();
SuperClass.hello()는 private이므로 하위 클래스에서 보이지 않는다.
따라서 정상적인 접근 가능한 메서드로 사용할 수 없다.
인터페이스에 default hello()가 추가된다면?
시간이 지나 인터페이스가 다음처럼 변경되었다고 생각해보자.
public interface MarkerInterface {
default void hello() {
System.out.println(
"Interface hello"
);
}
}
이제 SubClass에는 인터페이스를 통해 접근 가능한 hello()가 생긴 것처럼 보인다.
subClass.hello();
그런데 Java의 메서드 선택 규칙에는 앞에서 살펴본 중요한 원칙이 있다.
클래스가 인터페이스보다 우선한다.
즉 메서드를 찾는 과정에서 클래스 계층이 인터페이스 기본 구현보다 우선적으로 고려된다.
제시된 예제와 같은 특정 상황에서는 이러한 메서드 탐색과 접근성 사이의 충돌 때문에 컴파일 단계가 아니라 실행 과정에서 문제가 드러나는 경우가 있을 수 있다.
핵심은 구체적인 예외 종류 자체보다 다음 사실이다.
인터페이스에 default method 하나 추가
↓
기존 클래스의 메서드 탐색 구조 변화
↓
기존에는 존재하지 않았던 실행 경로 형성 가능
↓
런타임 문제 발생 가능
즉 default 메서드 추가가 항상 무해한 API 확장은 아니다.
default 메서드가 없었다면 더 일찍 발견할 수도 있다
기존 인터페이스에 hello()가 없었다면
subClass.hello();
와 같은 코드는 애초에 컴파일 단계에서 문제가 드러난다.
Compile Time
↓
문제 발견
그런데 인터페이스에 default hello()가 들어오면서 컴파일러가 호출 가능한 메서드가 존재한다고 판단하게 되는 구조가 만들어질 수 있다.
그 결과 문제가
Compile Time
이 아니라
Runtime
까지 밀려날 수 있다.
이것은 개발자에게 더 좋지 않은 상황이다.
컴파일 오류와 런타임 오류
두 오류의 차이를 명확하게 이해할 필요가 있다.
컴파일 오류
코드를 실행하기 전에 발견된다.
int number = "hello";
컴파일러가 즉시 잘못된 코드를 알려준다.
코드 작성
↓
Compile
↓
Error
↓
실행되지 않음
문제를 상대적으로 빠르게 발견할 수 있다.
런타임 오류
컴파일은 성공하지만 프로그램을 실제로 실행하면서 문제가 발생한다.
코드 작성
↓
Compile 성공
↓
배포 또는 실행
↓
특정 경로 실행
↓
Runtime Error
어떤 코드 경로가 드물게 실행된다면 문제가 오랫동안 발견되지 않을 수도 있다.
개발자는 가능하면 컴파일 타임 실패를 선호한다
개발자의 입장에서 보통 다음이 더 좋다.
컴파일 단계에서 실패
왜냐하면 문제를 가장 빠르게 발견할 수 있기 때문이다.
반면 런타임 문제는
테스트 중
운영 중
특정 데이터 처리 중
특정 사용자의 요청 중
에야 발견될 수 있다.
따라서 같은 문제라면 가능한 한 컴파일러가 잡아주는 구조가 더 안전하다.
default 메서드의 위험은 “조용히 들어온다”는 데 있다
일반적인 인터페이스 변경은 매우 시끄럽다.
새 추상 메서드 추가
↓
수많은 컴파일 에러
개발자가 즉시 변경 사실을 알 수 있다.
반면 default 메서드는 조용하다.
default method 추가
↓
기존 클래스 컴파일 성공
↓
아무 경고 없음
바로 이 점 때문에 더 조심해야 한다.
새로운 default 메서드는 구현체의 불변식을 깨뜨릴 수 있다
인터페이스의 구현 클래스들은 단순히 메서드만 구현하고 있는 것이 아니다.
각자 자신만의 불변식을 가질 수 있다.
예를 들어 캐시 구현체가 있다고 하자.
모든 put 연산
↓
용량 검사
↓
통계 기록
↓
데이터 저장
그런데 인터페이스에 새로운 데이터 변경 default 메서드가 추가되면서 이 과정을 우회한다면 문제가 된다.
또 다른 예로 보안 Wrapper가 있을 수 있다.
모든 변경
↓
권한 검사
↓
실제 변경
새로운 default 메서드가 권한 검사 없이 내부 상태를 바꿀 수 있다면 보안 정책이 깨질 수 있다.
즉 문제의 본질은 removeIf() 하나가 아니다.
인터페이스가 모르는
구현 클래스 고유의 규칙
이 존재할 수 있다는 것이 핵심이다.
인터페이스 설계자는 구현체를 예상해야 한다
새로운 인터페이스를 설계할 때는 다음 질문을 생각해야 한다.
이 인터페이스를 어떤 클래스들이 구현할까?
읽기 전용 구현체가 존재할 수 있는가?
동기화가 필요한 구현체가 존재할 수 있는가?
불변 구현체가 존재할 수 있는가?
특정 검증을 반드시 수행해야 하는 구현체가 있는가?
Wrapper 또는 Decorator 구현체가 존재할 수 있는가?
인터페이스가 단순하다고 해서 구현체까지 단순한 것은 아니다.
default 메서드를 추가하기 전 확인해야 할 것
기존 인터페이스에 default 메서드를 추가해야 한다면 최소한 다음을 검토하는 것이 좋다.
모든 구현체에서 의미적으로 자연스러운가?
특정 구현체에서만 의미가 있는 기능은 아닌가?
구현체의 상태 불변식을 깨뜨리지 않는가?
동기화
검증
권한
로깅
트랜잭션
같은 정책을 우회할 가능성이 없는지 살펴봐야 한다.
기존 구현체와 충돌하는 메서드가 없는가?
인터페이스 계층과 클래스 계층 전체에서 동일한 시그니처의 메서드가 존재할 가능성도 고려해야 한다.
공개된 인터페이스라면 외부 구현체도 생각했는가?
내 코드베이스에서 검색했는데 구현 클래스가 두 개밖에 없다고 해서 실제 구현체가 두 개뿐이라는 보장은 없다.
public API라면 외부에서 얼마든지 새로운 구현체를 작성했을 수 있다.
새 인터페이스를 설계할 때도 default 메서드를 과신하면 안 된다
이 이야기는 기존 인터페이스 변경에만 적용되는 것은 아니다.
처음부터 새로운 인터페이스를 설계할 때도 default 메서드가 많아지면 구현 클래스의 자유도가 예상보다 줄어들 수 있다.
public interface Processor {
default void before() {
}
default void process() {
}
default void after() {
}
default void retry() {
}
default void validate() {
}
}
처음에는 편리해 보여도 구현체 입장에서는 인터페이스의 기본 동작과 자신의 정책 사이의 관계를 계속 검토해야 한다.
따라서 default 메서드는 단순히
"코드를 재사용하기 위해"
추가하는 기능으로 생각하면 안 된다.
default 메서드는 인터페이스 진화를 위한 도구다
앞선 아이템에서도 살펴봤듯 default 메서드의 중요한 목적은 이미 존재하는 인터페이스를 진화시키는 것이다.
기존 인터페이스
↓
새로운 기능 필요
↓
기본 구현이 합리적으로 가능
↓
default method
하지만 이것은 안전성을 자동으로 보장해주는 기능이 아니다.
오히려 구현 클래스의 입장에서는 예상하지 못한 새로운 동작이 들어올 수 있다.
따라서
기존 코드를 컴파일되게 유지하는 것
과
기존 코드의 의미를 유지하는 것
을 반드시 구분해야 한다.
인터페이스 변경의 영향 범위
일반 클래스의 private 메서드를 변경한다고 생각해보자.
변경 영향
↓
해당 클래스 내부
반면 public 인터페이스는 다르다.
인터페이스 변경
↓
우리 구현체
↓
다른 모듈 구현체
↓
외부 라이브러리 구현체
↓
사용자가 직접 만든 구현체
영향 범위가 매우 넓을 수 있다.
특히 default 메서드는 이 모든 구현체에 자동으로 새로운 동작을 추가할 수 있다.
따라서 public 인터페이스는 클래스보다 훨씬 보수적으로 변경할 필요가 있다.
컴파일 호환성이 전부가 아니다
default 메서드가 추가되었는데 기존 코드가 모두 정상적으로 컴파일된다고 해보자.
그렇다고 해서 바로 안전하다고 판단해서는 안 된다.
확인해야 할 것은 다음과 같다.
컴파일되는가?
+
기존 의미가 유지되는가?
+
Thread-Safe한가?
+
불변식이 유지되는가?
+
메서드 충돌은 없는가?
+
기존 테스트가 여전히 통과하는가?
인터페이스의 진화에서는 컴파일 호환성보다 행동 호환성(Behavioral Compatibility) 이 더 어려운 문제가 될 수 있다.
실무에서 public 인터페이스를 설계한다면
예를 들어 결제 수단 인터페이스가 있다고 생각해보자.
public interface PaymentMethod {
PaymentResult pay(
PaymentRequest request
);
}
이미 여러 구현체가 존재한다.
CardPayment
PayPalPayment
BankTransferPayment
PointPayment
어느 날 인터페이스에 다음 default 메서드를 추가한다고 해보자.
default void cancel(
Payment payment
) {
// 기본 취소
}
문법적으로는 편리하다.
하지만 실제로는 결제 수단별 취소 정책이 완전히 다를 수 있다.
카드
→ PG 승인 취소 필요
PayPal
→ 외부 API 호출 필요
계좌이체
→ 환불 프로세스 필요
포인트
→ 내부 잔액 복원
이런 기능에 무리하게 기본 구현을 제공하면 기존 구현체에서 잘못된 동작이 발생할 수 있다.
따라서 모든 구현체에 적용 가능한 기본 의미가 존재하는지를 먼저 고민해야 한다.
테스트를 통해 기존 구현체와의 호환성을 검증해야 한다
인터페이스에 새로운 default 메서드를 추가했다면 기존 구현체에 대해 테스트하는 것이 중요하다.
예를 들어
기본 구현체
읽기 전용 구현체
동기화 구현체
Wrapper 구현체
특수한 상태를 가진 구현체
를 각각 테스트해볼 수 있다.
특히 동시성이나 보안, 트랜잭션 같은 특수한 정책이 들어간 구현체는 새로운 기본 구현과 충돌하지 않는지 확인해야 한다.
아이템 20과 아이템 21을 함께 이해하자
아이템 20에서는 인터페이스가 매우 유연한 설계 도구라는 사실을 살펴봤다.
다중 구현 가능
Mixin 가능
Default Method 가능
Wrapper와 조합 가능
하지만 아이템 21에서는 그 강력함에 대한 주의사항을 이야기한다.
default method를 추가할 수 있다.
↓
그렇다고 마음대로 추가하면 안 된다.
즉 두 아이템을 함께 이해해야 한다.
아이템 20
인터페이스를 적극적으로 활용하라.
아이템 21
하지만 구현하는 쪽까지 생각해서
신중하게 설계하라.
핵심 정리
default메서드는 기존 인터페이스를 진화시키는 강력한 기능이다.- 기존 구현체가 새로운 메서드를 직접 구현하지 않아도 되기 때문에 컴파일 호환성을 유지하기 쉽다.
- 하지만
default메서드를 기존 인터페이스에 추가한다는 것은 모든 기존 구현체에 새로운 동작을 자동으로 추가하는 것과 비슷하다. - 인터페이스 입장에서는 합리적인 기본 구현이라도 특정 구현 클래스의 규칙과 충돌할 수 있다.
- 대표적인 예가
Collection.removeIf()와 동기화를 요구하는 컬렉션 구현체의 관계다. - 구현체가 모든 연산에 별도의 동기화 정책을 가지고 있다면 인터페이스 기본 구현이 이를 우회할 수 있다.
- 이런 경우 구현 클래스가
default메서드를 직접 재정의하여 자신의 정책을 유지해야 한다. - 자신이 관리하는 구현체라면 대응할 수 있지만 공개 인터페이스의 외부 구현체까지 모두 확인하는 것은 어렵다.
- 일반 추상 메서드를 추가하면 컴파일 오류를 통해 변경 사실을 즉시 발견할 수 있지만
default메서드는 아무 경고 없이 기존 구현체에 들어갈 수 있다. - 따라서 컴파일 호환성과 행동 호환성을 구분해야 한다.
default메서드 추가로 기존 클래스의 메서드 탐색 구조가 달라지면서 특정 상황에서는 실행 과정에서 문제가 드러날 수도 있다.- 같은 문제라면 런타임보다 컴파일 시점에 발견되는 것이 개발자에게 더 유리하다.
- 인터페이스 구현체는 동기화, 검증, 보안, 로깅, 트랜잭션 등 인터페이스가 알지 못하는 추가적인 불변식을 가질 수 있다.
default메서드를 추가할 때는 이러한 구현체별 정책을 깨뜨릴 가능성이 없는지 검토해야 한다.- 특히 public 인터페이스는 외부에서 어떤 구현체가 만들어졌는지 알 수 없으므로 훨씬 보수적으로 변경해야 한다.
default메서드는 단순한 코드 재사용 수단이 아니라 인터페이스 진화를 위한 기능으로 이해하는 것이 좋다.- 인터페이스를 설계할 때는 사용하는 쪽뿐 아니라 직접 구현하는 개발자가 어떤 제약과 문제를 만나게 될지까지 함께 고려해야 한다.
한 줄 요약
default메서드는 기존 인터페이스를 깨뜨리지 않고 진화시키는 강력한 기능이지만 모든 구현체에 새로운 동작을 조용히 삽입하기 때문에, 구현 클래스의 동기화·검증·불변식·메서드 탐색 규칙을 훼손하지 않는지까지 고려해서 매우 신중하게 추가해야 한다.