2022/05/13
자바 8 이전에는 기존 구현체를 깨뜨리지 않고는 인터페이스에 메서드를 추가할 방법이 없었다.
- 인터페이스에 메서드를 추가하면 보통은 컴파일 오류가 나는데, 추가된 메서드가 기존 구현체에 존재할 가능성이 아주 낮기 때문이다.
자바 8 이후부터 기존 인터페이스에 메서드를 추가할 수 있도록 디폴트 메서드가 추가되었다.
- 디폴트 메서드를 선언하면, 그 인터페이스를 구현한 후 디폴트 메서드를 재정의하지 않은 모든 클래스에서 디폴트 구현이 쓰이게 된다.
- 디폴트 메서드는 구현 클래스에 대해 아무것도 모른 채 합의 없이 무작정 '삽입'될 뿐이므로 주의해야 한다.
자바 8에서는 핵심 컬렉션 인터페이스들에 다수의 디폴트 메서드가 추가되었다. 이는 주로 람다를 활용하기 위해서다.
- 자바 라이브러리의 디폴트 메서드는 코드 품질이 높고 범용적이라 대부분의 상황에서 잘 작동하지만
- 생각할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴트 메서드를 작성하는 것은 어렵다.
불변식 : 한번 만들어진 식(객체)이 변하지 않다는 것을 의미
자바 8의 Collection 인터페이스에 추가된 디폴트 메서드
public interface Collection<E> extends Iterable<E> {
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;
}
}
- 이 메서드는 주어진 boolean 함수(predecate)가 true를 반환하는 모든 원소를 제거한다.
- 위 코드는 범용적으로 구현되었지만 현존하는 모든 Collection 구현체와 잘 어우러지는 것은 아니다.
- SynchronizedCollection이 대표적인 예다.
- 아파치 버전은 클라이언트가 제공한 객체로 락을 거는 기능을 추가로 제공한다.
- 즉, 모든 메서드에서 주어진 락 객체로 동기화한 후 내부 컬렉션 객체에 기능을 위임하는 래퍼 클래스다.
- 따라서 SynchronizedCollection 인스턴스를 여러 스레드가 공유하는 환경에서 한 스레드가 removeIf를 호출하면 concurrentModificationException이 발생하거나 다른 예기치 못한 결과로 이어질 수 있다.
디폴트 메서드가 추가된것을 인지 하지 못하고 removeIf 메서드를 재정의 하지 않는다면 오류가 발생하거나 예기치 못한 결과로 이어질 수 있다.
- 구현한 인터페이스의 디폴트 메서드를 재정의
- 다른 메서드 에서는 디폴트 메서드를 호출하기 전에 필요한 작업을 수행하도록 했다.
Collections.synchronizedCollection이 반환하는 package-private 클래스 들은 removeIf를 재정의하고, 이를 호출하는 다른 메서드들은 디폴트 구현을 호출하기 전에 동기화를 하도록 했다.
4.4 버전 이후부터는 override 즉 재정의 되어있다.
public class SynchronizedCollection<E> implements Collection<E>, Serializable {
...
/**
* @since 4.4
*/
@Override
public boolean removeIf(final Predicate<? super E> filter) {
synchronized (lock) {
return decorated().removeIf(filter);
}
}
}
- 디폴트 메서드는 (컴파일에 성공하더라도) 기존 구현체에 런타임 오류를 일으킬 수 있다.
기존 메서드를 제거하거나 수정하는 용도가 아니다.
디폴트 메서드로 인해 기존 클라이언트를 망가뜨릴 수 있다.
따라서, 인터페이스를 설계 할 때는 여전히 세심한 주의를 기울여야 한다.
이를 검증하기 위해 서로 다른 방식으로 최소 세 가지의 구현체를 만들어 보자.
인터페이스를 릴리즈한 후라도 결함을 수정하는 게 가능한 경우도 있지만, 이를 보험삼아서는 안된다.
- 디폴트 메서드 사용은 불변식을 보장하지 못한다.
- 디폴트 메서드가 추가된것을 인지 하지 못하고 메서드를 재정의 하지 않는다면 오류가 발생하거나 예기치 못한 결과로 이어질 수 있다.
- 따라서 디폴트 메서드는 (컴파일에 성공하더라도) 기존 구현체에 런타임 오류를 일으킬 수 있다.
- 결론적으로 꼭 필요한 경우가 아니면 디폴트 메서드를 추가하지 말자
- 디폴트 메서드로 인해 기존 클라이언트를 망가뜨릴 수 있다.
- 또한 인터페이스 설계에 세심한 주의를 기울이고, 항상 여러개의 구현체를 통해 테스트를 진행하자.
[오브젝트 600p]
디폴트 메서드가 추가된 이유 :
- 기존에 널리 사용되고 있는 인터페이스에 새로운 오퍼레이션을 추가할 경우 하위 호환성 문제를 해결하기 위해서이다.
오해1. 추상클래스의 역할을 대체하기 위해 디폴트 메서드를 사용하는것인가? No
public interface DiscountPolicy {
default int calculateDiscountAmount(Object movie){
for(String each : getConditions()){
if(each.equals((String) movie)){
return getDiscountAmount(movie);
}
}
return 0;
}
List<String> getConditions(); // 디폴트 메서드 내부 구현
int getDiscountAmount(Object movie); // 디폴트 메서드 내부 구현
String publicInterface(); // 퍼블릭 인터페이스
}
public class AmountDiscountPolicy implements DiscountPolicy{
@Override
public List<String> getConditions() { // 내부 구현에 사용되는 메서드 접근자가 public 으로 열린다.
return null;
}
@Override
public int getDiscountAmount(Object movie) { // 내부 구현에 사용되는 메서드 접근자가 public 으로 열린다.
return 0;
}
@Override
public String publicInterface() {
return null;
}
}
- 캡슐화를 약화시킨다.
- 인터페이스가 불필요하게 비대해진다.
- 코드 중복을 환벽하게 제거하지 못한다.