본문 바로가기

Reading/Effective Java

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

모든 상황에서 불변식을 해치지 않는 디폴트 메소드 작성은 어렵다.


인터페이스에 메소드를 추가하면 우연히 기존 구현체에 추가된 메소드가 상황이 아니라면 컴파일 오류가 발생한다.

원래는 기존 구현체를 깨뜨리지 않고는 인터페이스에 메소드를 추가할 방법이 없었는데, 자바8 이후에는 디폴트 메소드를 소개됐다. 

 

디폴트 메소드를 선언하면, 재정의 하지 않았던 모든 클래스에서 디폴트 메소드가 쓰이게 된다.

자바7까지만 하더라도 현재의 인터페이스에 새로운 메소드가 추가될 일 없다라는 가정으로 작성되었으니,

디폴트 메소드는 구현 클래스에 대해 아무것도 모른 채 무작정 삽입될 뿐이다. 모든 기존 구현체들과 매끄럽게 연동되리라는 보장은 없다.

 

주로 람다를 활용할 목적으로 자바 8에서는 핵심 컬렉션 인터페이스에 다수의 디폴트 메소드가 추가되었다.

Colection 인터페이스엔 removeIf라는 default method가 추가됐다. 이 메소드는 주어진 불리언 함수(predicate)가 true를 반환하는 모든 원소를 제거한다. 디폴트 구현은 반복자를 순회하며 각 원소를 인수로 넣어 프레디키트를 호출하고 true를 반환하면 반복자의 remove 메소드를 호출해 원소를 제거한다.

 

 

이 코드보다 범용적으로 구현되긴 어렵겠지만, 모든 Collection 구현체와 어울린다는 보장은 없다.

org.apache.commons.collections4.collection.synchronizedCollection 이 대표적인 예다.

이 클래스는 java.util의 Collection.synchronizedCollection 정적 팩토리 메소드가 반환하는 클래스와 비슷하다.

아파치 버전은 클라이언트가 제공한 객체로 락을 거는 능력을 추가로 제공하는데, 모든 메소드에서 주어진 락 객체로 동기화 후

내부 컬렉션 객체에 기능을 위임하는 래퍼클래스다.

 

이 책이 쓰인 시점에서는 removeIf가 재정의되어 있지 않았었다. 그래서 모든 호출을 알아서 동기화 해주지 않는다.

removeIf의 구현은 동기화에 관해 아무것도 모르니까 락 객체를 사용할 수 없고, 이 인스턴스를 멀티 스레드 환경에서

한 스레드가 removeIf를 호출하면 ConcurrentModificationException이 발생하거나 다른 예기치 못한 결과로 이어질 수 있다.

 

자바 플랫폼 라이브러리에선 구현한 인터페이스의 디폴트 메소드를 재정의하고 다른 메소드에서는 디폴트 메소드 호출 전 필요 작업을 수행했고, java.util의 Collection.synchronizedCollection이 반환하는 package-private 클래스들은 removeIf를 재정의하고 이를 호출하는 다른 메소드들은 디폴트 구현을 호출하기 전 동기화 하도록 했다. 하지만 위의 apache를 포함한 자바 플랫폼에 속하지 않은 제3의 기존 컬렉션 구현채들은 언어 차원의 인터페이스 변화에 맞춰 수정될 기회가 없었으며 여전히 수정되지 않는 경우도 있다.

 

apache의 경우에는 synchronizedCollection이 4.4 버전 기준으로 removeIf를 재정의 했음을 확인할 수 있었다.

 

 

더 자세한건 아래 문서에서 살펴보자

 

SynchronizedCollection (Apache Commons Collections 4.4 API)

Decorates another Collection to synchronize its behaviour for a multi-threaded environment. Iterators must be manually synchronized: synchronized (coll) { Iterator it = coll.iterator(); // do stuff with iterator } This class is Serializable from Commons Co

commons.apache.org

 

기존 인터페이스에 디폴트 메소드로 새 메소드를 추가하는 일은 꼭 필요한 게 아니라면 피하자.


디폴트 메소드는 컴파일에 성공하더라도 기존 구현체에 런타임 오류를 일으킬 수 있다.

추가하려는 디폴트 메소드가 기존 구현체들과 충돌하지 않을지도 당연히 고려를 해야하고, 위 apache 예처럼 기존 인터페이스에 디폴트 메소드를 추가하는 행위는 인터페이스를 사용하는 클라이언트와 이 인터페이스를 구현하는 모든 API에 큰 재앙을 가져올 수 있다.

그리고 디폴트 메소드는 인터페이스로부터 메소드를 제거하거나, 기존 메소드의 시그니처를 수정하는 용도가 아님을 명심하자.

 

반면 새 인터페이스를 만드는 경우에 표준적인 메소드 구현을 제공하여 아주 유용하며 더 쉽게 구현할 수 있도록 돕는다.

새 인터페이스를 만들 때 다른 방식으로 최소 세 가지는 구현해보고, 인터페이스의 구현체를  클라이언트 입장에서도 만들어보자.

인터페이스를 릴리즈한 후라도 결함을 수정하는 게 가능한 경우도 있지만 그 가능성에 기대지 말자.

 

 

* 위 글은 EffectiveJava 3/E 책 내용을 정리한 글로, 저작권 관련 문제가 있다면 댓글로 남겨주시면 즉각 삭제조치 하겠습니다.