인터페이스는 구현하는 쪽을 생각해 설계하라

@023· January 16, 2025 · 4 min read

자바 8 이전에는 기존 인터페이스에 메서드르 추하려면 기존 구현체를 수정해야 했고, 이를 안할 시 컴파일 오률를 일으켰다. 자바 8에서는 디폴트 메서드라는 기능을 도입하면서 인터페이스에 새로운 메서드를 추가할 수 있게 되었지만, 모든 상황에 대비하여 안전하게 동작하리라는 보장은 없다.

디폴트 메서드와 위험성

디폴트 메서드는 인터페이스 구현체에서 재정의하지 않으면 기본 구현을 사용한다. 이런 면에서 기존 클래스들은 새로운 메서드의 동작과 충돌할 수 있다. 예를 들어, removeIf 디폴트 메서드는 대부분의 상황에서 잘 동작하지만, 기존 구현체가 가진 고유 불변식이나 동기화 정책을 깨뜨릴 위험이 있다.

실제로 아파치 토미캣 8.5 버전에서는 removeIf 메서드를 사용하면 ConcurrentModificationException이 발생하는 문제가 있었다. 이는 아파치 커먼즈의 Collectoins.synchronizedCollection 클래스는 동기화를 위해 메서드 호출하다 락을 걸지만, removeIf 디폴트 구현은 동기화와 관련된 지식이 없기 때문이다.

디폴트 메서드와 기존 구현체의 충돌

이렇듯 디폴트 메서드는 기존 구현체와 충돌할 수 있다. 그럼 자바 표준 라이브러리에서는 어떻게 이 문제를 해결했을까? 자바에서는 기존 인터페이스를 구현하는 클래스에서 디폴트 메서드를 재정의하거나, 디폴트 메서드를 호출하기 전 동기화같은 작업을 수행하도록 했다.

@Override
public synchronized boolean removeIf(Predicate<? super E> filter) {
    return c.removeIf(filter); // c는 내부 컬렉션 객체
}

하지만 자바 플랫폼에 속하지 않은 제 3자의 라이브러리의 구현체들은 이런 수정이 어려우며, 이로인해 런타임 오류가 발생할 수도 있다

디폴트 메서드의 설계 원칙

위와 같이 디폴트 메서드는 컴파일에 성공하더라도 기존 구현체에 대한 런타임 오류를 일으킬 수 있다. 책은 이를 방지하기 위해서 다음과 같은 원칙을 제시한다.

  1. 기존 인터페이스에 디폴트 메서드로 새 메서드를 추가하는 일은 꼭 필요한 경우가 아니면 피해야 한다.
  2. 추가하려는 디폴트 메서드가 기존 구현체들과 충돌하지는 않을지 테스트해야 한다.
  3. 인터페이스로부터 메서드를 제거하거나 기존 메서드의 시그니처를 수정하는 것은 기존 클라이언트를 망가뜨리게 되므로 피해야 한다.

정리

디폴트 메서드는 유용한 도구지만, 기존 인터페이스를 수정할 때는 매우 신중해야 한다. 사용을 한다면, 다양한 테스트를 통해 완성도를 높이고, 릴리스 전에 결함을 수정해야 한다.

@023
focus and hustle