Study/Effective-Java

[item#21] 인터페이스는 구현하는 쪽을 생각해 설계하라

hongeeii 2023. 11. 30.
728x90
반응형
  • 기존 인터페이스에 디폴트 메서드 구현을 추가하는 것은 위험한 일
    • 디폴트 메서드는 구현 클래스에 대해 아무것도 모른 채 합의 없이 무작정 삽입될 뿐
    • 디폴트 메서드는 기존 구현체에 런타임 오류를 일으킬 수 있다.
  • 인터페이스를 설계할 때는 세심한 주의를 기울여야 한다.
    • 서로 다른 방식으로 최소한 세가지는 구현 해보자.

구현체들은 default 메서드 추가 사실을 모른다

default 메서드를 추가하면 그 인터페이스를 구현한 모든 구현체들은 해당 default 메서드를 사용할 수 있게 된다.
(오버라이딩을 하던, 안하던) 단, 구현체들이 default 메서드가 추가됐는지 알아차리지 못하는 채로 무작정 추가된다.
자바 7까지는 모든 클래스가 “현재의 인터페이스에 새로운 메서드가 추가될 일은 영원히 없다” 라고 가정한 뒤 작성 되었기 때문이다.

모든 상황에서 불변식을 해치지 않기란 어렵다

  Collection.java
  
   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;
    }

Collection을 구현한 대부분의 클래스에서 잘 돌아가는 메서드지만, 모든 구현체에서 removeIf 메서드를 잘 소화하는 것은 아니다.
대표적인 예가 SynchronizedCollection이다.
클라이언트가 제공한 객체에 lock을 거는 능력을 추가하여 스레드간 동기화를 보장하는 클래스이다.
그러나, 4.3이하 버전의 SynchronizedCollection에서는 removeIf가 오버라이딩 되지않아 default메서드의 형태로 사용되어져 동기화가 지켜지지 않았다.
따라서, SynchronizedCollection인스턴스를 여러 스레드가 공유하는 환경에서 removeIf메서드가 호출되면 ConcurrentModificationException이 발생하거나, 다른 잘못된 결과를 반환한다.

4.4이상 버전 부터는 이러한 사태를 인지하고 다음과 같이 오버라이딩 하였다.

// SynchronizedCollection in Collections

// Override default methods in Collection
@Override
public boolean removeIf(Predicate<? super E> filter) {
    synchronized (mutex) {return c.removeIf(filter);}
}

문제점이 런타임에서야 발견 될 수도 있다

default 메서드의 문제점은 컴파일 타임을 지나 런타임에 들어서야 발견 될 수도 있다. (특히나 동기화 관련된 문제에서)
자바 8에서 Collection 인터페이스에 상당히 많은 default 메서드를 추가했고, 그 결과 기존에 짜여진 수 많은 자바코드들이 영향을 받게 되었다.
언제 어디서 문제가 터질지 예측이 불가능하다.

ex2 ::

public interface MarkerInterface {
    default void hello(){
        System.out.println("hello interface");
    }
}


public class SuperClass {
    private void hello(){
        System.out.println("hello super");
    }
}


public class SubClass extends SuperClass implements MarkerInterface {
    public static void main(String[] args) {
        SubClass sub = new SubClass();
        sub.hello();
    }
}

///////////
Exception in thread "main" java.lang.IllegalAccessError: class com.example.effective.item21.SubClass tried to access private method com.example.effective.item21.SuperClass.hello()V (com.example.effective.item21.SubClass and com.example.effective.item21.SuperClass are in unnamed module of loader 'app')
    at com.example.effective.item21.SubClass.main(SubClass.java:7)

Class는 Interface를 이긴다는 원칙때문에 SuperClass의 hello가 private이라서 sub에서 접근조차 불가하지만, interface의 hello가 있기 때문에 컴파일 에러는 발생하지 않는다.
하지만 Runtime시, SuperClass의 hello에 접근하려고 하면서 private method는 접근 불가하다는 RuntimeException 발생.

default 메서드를 신중하게 사용해라

default 메서드는 새로운 인터페이스를 설계할 때 표준적인 메서드 구현을 제공하는데 아주 유용한 수단이며,
인터페이스를 더 쉽게 구현해 활용할 수 있게끔 해준다.

그러나! default 메서드는 인터페이스로부터 메서드를 제거하거나 기존 메서드의 시그니처를 수정하는 용도가 아님을 명심하자.
또한 기존 인터페이스에 default 메서드로 새 메서드를 추가하는 일은 꼭 필요한 경우가 아니라면 피해야 한다.

https://worthpreading.tistory.com/90

image

현재 바뀌면 안되는 것을 수정할 때 발생하는 예외

  • 멀티 스레드가 아니라 싱글 스레드 상황에서도 발생할 수 있다.
  • 가령, fail-fast 이터레이터를 사용해 콜렉션을 순회하는 중에 콜렉션을 변경하는 경우

image
immutable Collection을 수정할 땐 UnsupportedOperationException 발생.

ConcurrentModificationException

image

우회 하는 방법

      // 우회 하는 방법
        // iterator를 사용
        for (Iterator<Integer> iterator = numbers.iterator(); iterator.hasNext(); ) {
            Integer integer = iterator.next();
            if (integer == 3) {
                iterator.remove(); // iterator를 사용하여 빼내면 ConcurrentModificationException 안남
            }
        }

        // index를 사용하는 방법
        for (int i = 0; i < numbers.size(); i++) {
            if (numbers.get(i) == 3) {
                numbers.remove(numbers.get(i));
            }
        }

        // removeIf 사용하기
        numbers.removeIf(number -> number == 3);
728x90
반응형

추천 글