Post

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

디폴트 메서드의 도입

  • 자바 8 이전에는 기존 구현체를 깨뜨리지 않고는 인터페이스에 메서드를 추가할 방법이 없었다.
  • 자바 8부터는 디폴트 메서드가 소개되면서 기존 인터페이스에 메서드를 추가할 수 있게 되었다.

그러나 이렇게 추가된 디폴트 메서드가 모든 기존 구현체들과 매끄럽게 연동되리라는 보장은 없다.

디폴트 메서드 추가 시 주의사항

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

예시 코드

초기 인터페이스와 클래스

1
2
3
4
5
6
7
8
9
10
interface Animal {
    void sound(); // Animal 인터페이스는 sound() 메서드를 가진다.
}

class Dog implements Animal {
    @Override
    public void sound() {
        System.out.println("Woof"); // Dog 클래스는 sound() 메서드를 구현하고 있다.
    }
}

디폴트 메서드 추가

  • Animal 인터페이스에 새로운 디폴트 메서드를 추가한다고 가정해보자.
  • Animal 인터페이스를 구현한 모든 클래스는 sleep() 메서드를 가지게 된다.
1
2
3
4
5
6
7
8
interface Animal {
    void sound();

    // 새로 추가된 디폴트 메서드
    default void sleep() {
        System.out.println("The animal is sleeping");
    }
}

런타임 오류가 발생할 수 있는 상황

  • Dog 클래스에 sleep()이라는 메서드를 추가하면서 의도적으로 잘못된 코드를 넣어 보자.
    • 예를 들어, sleep() 메서드가 null인 객체를 참조하도록 하자.
1
2
3
4
5
6
7
8
9
10
11
12
13
class Dog implements Animal {
    @Override
    public void sound() {
        System.out.println("Woof");
    }

    // Dog 클래스에 새로운 sleep() 메서드 추가 (오버라이딩)
    public void sleep() {
        // 실수로 발생한 NullPointerException
        String message = null;
        System.out.println(message.toLowerCase());
    }
}

코드 실행

1
2
3
4
5
6
7
public class Main {
    public static void main(String[] args) {
        Animal dog = new Dog();
        dog.sound(); // "Woof" 출력
        dog.sleep(); // NullPointerException 발생
    }
}
  • Dog 클래스는 sleep() 메서드를 재정의했지만, 해당 메서드에 버그가 있어서 런타임 오류가 발생한다.
  • 기존 인터페이스에 디폴트 메서드 구현을 추가하는 일은 꼭 필요한 경우가 아니면 피해야한다.
    • 디폴트 메서드는 구현 클래스에 대해 아무것도 모른 채 합의 없이 무작정 “삽입”될 뿐이다.
  • 인터페이스를 설계할 때는 세심한 주의를 기울여야 한다.
    • 새로운 인터페이스라면 릴리즈 전에 반드시 테스트를 거쳐야 한다.
    • 서로 다른 방식으로 최소한 세 가지는 구현을 해보자.

참고 🕶️

ConcurrentModificationException - 현재 바뀌면 안되는 것을 수정할 때 발생하는 예외

  • 멀티 스레드가 아니라 싱글 스레드 상황에서도 발생할 수 있다.
    • 가령, fail-fast 이터레이터를 사용해 컬렉션을 순회하는 중에 컬렉션을 변경하는 경우
This post is licensed under CC BY 4.0 by the author.