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

DEINGVELOP·2022년 12월 13일
1

Effective Java

목록 보기
7/19

~ Java 7 vs Java 8

  • Java 8 이전 : '현재의 인터페이스에 새로운 메서드가 추가될 일은 영원히 없다'

    • 인터페이스에 메서드를 추가할 경우, 기존의 구현체들에는 해당 메서드가 없기 때문에 컴파일 오류 발생
  • Java 8 : 기존 인터페이스에 메서드를 새롭게 추가 가능

    • 기존 인터페이스에 메서드를 추가할 수 있도록 디폴트 메서드라는 것을 소개함

Default Method

  • 디폴트 메서드를 선언할 경우, 그 인터페이스를 구현한 후 디폴트 메서드를 재정의하지 않은 모든 클래스에서 디폴트 구현이 쓰임

  • 디폴트 메서드는 구현 클래스에 대해 아무것도 모른 채 합의 없이 무작정 삽입됨 → 모든 구현체들과 매끄럽게 연동된다는 보장은 없음

Java 8

  • 핵심 컬렉션 인터페이스들에 다수의 디폴트 메서드가 추가됨

  • 주로 Lamdba를 활용하기 위함

  • Java Library의 디폴트 메서드는 코드 품질이 높고 범용적이라 대부분의 상황에서 잘 작동한다.

  • 단, 새롭게 작성할 때에는 모든 상황에서 불변식을 해치지 않는 디폴트 메서드를 작성하기란 어려운 법이다.

Default Method의 예시

  • Collection 인터페이스 - removeIf

    • boolean 함수(predicate)가 true를 반환하는 모든 원소를 제거

    • 디폴트 구현 : 반복자를 이용해 순회하며 각 원소를 인수로 넣어 predicate를 호출함 → predicate가 true를 반환하면 반복자의 remove 메서드를 호출해 그 원소를 제거

      default boolean removeIf(Predicate<? super E> filter) {
      	Objects.requireNonNull(filter);
        boolean result = false;
        for (Iterator<E> it = iterator(); it.hasNext(); ) {
        	if (filter.test(it.next())) {
            	it.remove();
                result = true;
            }
        }
        
      }
    • 거의 최대한으로 범용적으로 구현되어있지만, 그럼에도 불구하고 현존하는 모든 Collection 구현체와 잘 어우러지는 것은 아니다. (ex: org.apache.commons.collections4.collection.SynchronizedCollection)

      📌 아파치 커먼즈 - SynchornizedCollection

      • java.util - Collections.synchronizedCollection정적 팩터리 메서드가 반환하는 클래스와 비슷함

      • 아파치 버전은 클라이언트가 제공한 객체로 락을 거는 능력을 추가로 제공.

      • 즉, Wrapper Class임
        (모든 메서드에서 주어진 락 객체로 동기화한 후, 내부 컬렉션 객체에 기능을 위임함)

      • 책을 쓰는 시점엔 removeIf 메서드를 재정의하고 있지 않다(현재는 재정의하고 있음)

      • 이 클래스를 Java 8과 사용한다면, rmoveIf 의 구현을 물려받음 → 자신이 한 약속을 지키지 못함. 모든 메서드 호출을 알아서 동기화해주지 못함 (removeIf는 동기화에 관해 아무것도 모르기 때문에 락 객체를 사용할 수 없음)
        따라서 SynchronizedCollection을 여러 스레드가 공유하는 환경에서 한 스레드가 removeIf를 호출 → ConcurrentModificationException 오류 등의 문제 발생

      💡 Predicate

      • argument를 받아 boolean 값을 반환하는 함수형 인터페이스

Java Platform Library의 예방책

구현한 인터페이스의 디폴트 메서드를 재정의하고, 다른 메서드에서는 디폴트 메서드를 호출하기 전에 필요한 작업을 수행하도록 함

  • Collections.synchronizedCollection이 반환하는 package-private 클래스들은 removeIf를 재정의하고, 이를 호출하는 다른 메서드들은 디폴트 구현을 호출하기 전에 동기화를 하도록 함

  • 하지만 Java Platform에 속하지 않은 제3의 기존 컬렉션 구현체들은 이런 언어 차원의 인터페이스 변화에 발맞춰 수정할 기회가 없었으며, 그 중 일부는 여전히 수정되지 않고 있음


한계

  • Default Method는 (컴파일에 성공하더라도) 기존 구현체에 런타임 오류를 일으킬 수 있음

    • 예) Java 8은 Collection 인터페이스에 꽤 많은 디폴트 메서드를 추가했으며, 그 결과 기존에 짜여진 많은 Java코드가 영향을 받음

정리

디폴트 메서드라는 도구가 생겼더라도 인터페이스를 설계할 때에는 여전히 세심한 주의를 기울여야 한다.

  • 기존 인터페이스에 디폴트 메서드로 새 메서드를 추가하는 일은 꼭 필요한 일이 아니면 피해야 한다.
    할 일이 있더라도, 추가하려는 디폴트 메서드가 기존 구현체들과 충돌하지는 않을지 꼭 고려해야 한다.

  • 새로운 인터페이스를 만드는 경우라면 디폴트 메서드는 표준적인 메서드 구현을 하는 데에 아주 유용한 수단이며, 그 인터페이스를 더 쉽게 구현하여 활용할 수 있게끔 해준다.

  • 단, 디폴트 메서드는 인터페이스로부터 메서드를 제거하거나 기존 메서드의 시그니처를 수정하는 용도가 아님! (이렇게 구현할 경우 반드시 기존 Client를 망가뜨림)

  • 디폴트 메서드로 기존 인터페이스에 새로운 메서드를 추가하면 커다란 위험도 딸려온다.

  • 새로운 인터페이스라면 릴리즈 전에 반드시 테스트를 거친다.

    • 서로 다른 방식으로 최소한 세 가지는 구현해보기
    • 각 인터페이스의 인스턴스를 다양한 작업에 사용하는 클라이언트도 여러 개 만들어보기
  • 인터페이스를 릴리스한 후라도 결함을 수정하는 것이 가능한 경우도 있지만, 절대 그 가능성에 기대서는 안된다.

0개의 댓글