Book/Effective Java

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

마닐라 2022. 7. 5. 23:03

자바 8 이전에는 기존 구현체를 깨뜨리지 않고는 인터페이스에 메서드를 추가할 방법이 없었다.

자바 8에서 디폴트 메서드를 추가할 수 있도록 변경되었지만 이를 믿고 그대로 사용하는 것은 위험하다.

 

만약 인터페이스에 디폴트 메서드를 선언한다면 디폴트 메서드를 재정의하지 않은 해당 인터페이스를 구현한 모든 클래스에서 디폴트 구현이 쓰이게 된다.

이 부분이 주의해야 할 점이라고 한다.

 

그 이유는 매끄럽게 연동되리라는 보장이 없기 때문이다.

자바 7까지는 "현재의 인터페이스에 새로운 메서드가 추가될 일은 영원히 없다"고 가정하고 작성되었다.

이러한 디폴트 메서드는 구현 클래스를 고려하지 않고 무작정 '삽입'만 될 뿐이다.

 

자바 8에서는 핵심 컬렉션 인터페이스들에 다수의 디폴트 메서드가 추가되었다.

주로 람다를 참조하기 위해서라고 한다.

자바 라이브러리의 디폴트 메서드는 물론 코드 품질이 높고 범용적이라 대부분 상황에서 잘 작동한다.

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

 

하나의 예를 보자.

자바 8에서 Collection 인터페이스에 추가된 removeIf가 있다.

이 메서드는 반복자를 이용해 순회하면서 remove 메서드를 호출해 그 원소를 제거한다.

해당 디폴트 메서드와 매끄럽게 연동되지 않는 클래스가 있다.

바로 org.apache.commons.collections4.collection.SynchronizedCollection이다.

단순하게 Collections.synchronizedCollection 클래스와 비슷한데 아파치 버전은 (컬렉션 대신) 클라이언트가 제공한 객체로 락을 거는 능력을 추가로 제공한다.

 

아파치의 SynchronizedCollection 클래스는 이 책이 쓰여진 시점엔 removeIf 메서드를 재정의하지 않고 있다.

따라서 자바 8과 함께 사용한다면 removeIf의 디폴트 구현을 물려받게 되고 그로 인해 동기화 기능이 정상적으로 동작하지 않는다.

removeIf는 동기화에 관해 아무것도 모르기 때문에 락 객체를 사용할 수 없는 것이다.

때문에 SynchronizedCollection 인스턴스를 여러 스레드가 공유하는 환경에서 한 스레드가 removeIf를 호출하면 ConcurrentModificationException이 발생하거나 다른 예기치 못한 결과로 이어질 수 있다.

 

자바 플랫폼 라이브러리에서 문제 예방을 위해 일련의 조치를 취했다.

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

하지만 자바 플랫폼에 속하지 않은 제 3의 기존 컬렉션 구현체들 중 일부는 수정될 기회가 없었고 여전히 수정되지 않고 있다.

 

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

자바 8은 컬렉션 인터페이스에 꽤 많은 디폴트 메서드를 추가했고, 그 결과 기존에 짜여진 많은 자바 코드가 영향을 받은 것으로 알려졌다.

 

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

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

 

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