JAVA/Effective Java

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

꾸준함. 2024. 2. 7. 09:00

아이템 20에서 Java 8 이후 도입된 default 메서드는 인터페이스에 새로운 기능을 추가할 수 있으면서도 컴파일 에러를 발생시키지 않기 때문에 장점이라고 소개헀지만 이러한 특성은 단점으로 작용할 수도 있습니다.

  • 추가된 default 메서드를 해당 인터페이스 구현체와 합의 없이 무작정 삽입된 형태
  • 추가된 디폴트 메서드는 경우에 따라 기존 구현체에 런타임 오류를 야기할 수 있음

 

1. 추가된 default 메서드를 해당 인터페이스 구현체와 합의 없이 무작정 삽입된 형태

 

디폴트 메서드를 선언하면 해당 인터페이스를 구현한 클래스 중 디폴트 메서드를 재정의하지 않은 모든 클래스에서는 디폴트 구현이 적용됩니다.

자바 8에서는 람다를 활용하기 위해 핵심 컬렉션 인터페이스들에 다수의 default 멤서드가 추가되었고 자바 라이브러리의 디폴트 메서드는 코드 품질이 높고 범용적이기 때문에 "대부분" 상황에서 잘 작동합니다.

하지만 모든 상황에서 불변식을 해치지 않는 디폴트 메서드를 설계하고 작성하는 것은 어렵기 때문에 간혹 추가된 default 메서드에 의해 문제가 발생하는 케이스도 있습니다.

자바 8의 Collection 인터페이스에 추가된 removeIf 메서드가 대표적인 예이며 이보다 범용적으로 구현하기도 어렵겠지만 SynchornizedCollection과 같이 동기화가 필요한 경우 문제가 생길 수 있습니다.

 

 

SynchronizedCollection 클래스는 모든 메서드에 주어진 락 객체로 동기화한 후 내부 컬렉션 객체에 기능을 위임한 Wrapper 클래스인데 Java 8 기준 해당 클래스에서 removeIf 메서드를 재정의하지 않았기 때문에 앞서 언급한 원칙이 깨지게 되었습니다.

따라서 자바 8 기준 멀티 쓰레드 환경에서 SynchronizedCollection 인스턴스에 한 쓰레드가 removeIf 메서드를 호출할 경우 ConcurrentModificationException 예외가 발생하거나 예상치 못한 문제가 발생할 수 있습니다.

다행히도 자바 11 버전에서는 SynchronizedColleciton 클래스에서 removeIf 메서드를 재정의하여 원칙을 지킬 수 있게 되었지만 현실적으로는 모든 인터페이스 구현체가 인터페이스에 default 메서드가 추가되는 것을 알아차리고 대응하는 것은 간단하지 않습니다.

 

자바 11버전

 

2. 추가된 디폴트 메서드는 경우에 따라 기존 구현체에 런타임 오류를 야기할 수 있음

 

앞서 아이템 20에서 클래스와 인터페이스 간 우선순위에 대해 간단하게 언급했습니다.

  • 클래스가 인터페이스보다 우선순위 높음
  • 보다 구체적인 인터페이스가 우선순위 높음 

 

하위 클래스가 상위 클래스를 상속하면서 인터페이스도 구현하고 있었고 인터페이스에 상위 클래스의 private 메서드와 동일한 이름으로 default 메서드가 추가되었다고 가정하겠습니다.

하위 클래스에서 인터페이스의 default 메서드를 호출하려고 했는데 클래스가 인터페이스보다 우선순위가 높기 때문에 컴파일러는 상위 클래스의 private으로 선언한 메서드를 호출하려고 합니다.

이 때문에 IllegalAccessError가 발생하며 위 케이스는 디폴트 메서드가 경우에 따라 오류를 발생시킬 수 있다는 것을 보여줍니다.

 

 

정리

디폴트 메서드를 통해 편리해진 것은 사실이지만 위험도 뒤따라오기 때문에 인터페이스를 설계할 때는 여전히 세심한 주의를 기울여야 합니다.

따라서 새로운 인터페이스를 정의할 때는 릴리즈 전 충분한 테스트를 거쳐야 하며 서로 다른 방식으로 최소 세 가지를 구현해 보는 것을 권장합니다.

반응형