JAVA/모던 자바 인 액션

모던 자바 인 액션 21. Default Method

kujaHn 2021. 10. 1. 00:14

디폴트 메스드의 등장

자동차 소프트웨어를 만든다고 해봅시다. 여러 자동차 타입마다 당연히 구동되는 방식도 다르기 때문에,

인터페이스로 정말 단순하게 구현해보겠습니다.

public interface Car {
    void on();

    void off();

    void drive();

    void stop();
}

 

구현체인 A, B

이게 기본적인 전략 패턴의 골자였습니다.

 

이 프로그램을 잘 쓰고 있다가, 나중에 하늘을 나는 기능을 을 추가한다고 해봅시다. 추가하는 건 좋은데 '어디에' 추가를 해야 할까요?

 

1. 각 클래스

각 클래스에서 구현을 한다 합시다.

만약 이 하늘을 나는 기능이 정말 소수의 자동차만 동작하는 (프리미엄 에디션) 기능이라면, 괜찮을지 몰라도 조금만 양이 늘어나면, 꽤나 많은 반복 작업을 수행해야 할지 모릅니다.

사실 이러한 작업을 하지 않기 위해서 인터페이스가 생겨났기 때문에, 좋은 선택은 아닌 것 같습니다

 

 

2. 인터페이스

인터페이스에서 추가하는 방법은 정적 메서드와, 인터페이스 내 메서드 생성으로 총 두가지입니다. 

public interface Car {
    void on();

    void off();

    void drive();

    void stop();
    
    // 정적 메서드로추가!!
    static void fly();
    
    // 또는 인터페이스의 메서드로 추가
    void fly();
}

 

정적 메서드

먼저 정적 메서드로 추가를 해봅시다. 

정적 메서드는 객체가 아닌, 클래스와 연결된 메서드입니다. 그렇기 때문에 인스턴스에서 실행이 불가능합니다. 반드시 클래스 단위에서 실행이 가능합니다.

public class Person {

    private final Car car;

    public Person(Car car) {
        this.car = car;
    }

    void use() {
        car.on();
        car.drive();
        car.stop();
        car.off();
        Car.fly();    <------ 인스턴스에서 실행 불가능. 인터페이스에서 직접 호출
    }
}

 

 

추가적으로 이 예제에서 메서드는 대충 상태를 가지지 않는 print로 구현을 했기 때문에 문제가 되지 않지만, 만약 상태를 가지는 메서드라면? 이 정적 메서드의 사용은 불가능합니다. (이렇게 상태를 가지지 않는 메서드들을 유틸리티 메서드라고 합니다.)

추가적으로 모든 클래스에서 공유하는 메서드이기 때문에 오버 라이딩이 불가능합니다. 날 수 있는 자동차의 차종이 유일하다면 가능한 방법이겠네요.

 

인터페이스에서 추가

다음으로는 인터페이스에서 추가하는 방법입니다.

하지만 문제가 생깁니다. 인터페이스를 구현하는 클래스는 인터페이스에서 정의하는 모든 메서드 구현을 제공하거나, 아니면 슈퍼클래스의 구현을 상속받아야 하기 때문입니다. (모든 Car 구현체에 fly를 추가해야 한다.)

모든 구현체들이 구현해야 한다.

 

심지어 BCar는 나는 기능을 제공하지 않는다고 합시다. 그래서 BCar는 빈 메서드로 정의만 해놓고 구현을 하게 됩니다.

바이너리 호환성은 유지되나, 언젠가 이를 호출하게 되면, 런타임 에러가 발생할 수 있습니다! (바이너리 호환성에 대해서는 다음 포스트를 확인하시기 바랍니다.)

눈앞이 깜깜해집니다.. 어떻게 해결해야 할까요?

바로 여기서 디폴트 메서드가 여러분을 구원해 줍니다!

public interface Car {
    void on();

    void off();

    void drive();

    void stop();
    
    // 디폴트 메서드
    default void fly(){
    	System.out.println("이 자동차는 나는 기능이 없습니다.");
    };
}

이렇게 되면 기본적인 베이스는 인터페이스에서 제공(상속)을 하고, 이 기능이 정말 클래스에서는 오버라이드를 통해서 재구현이 가능합니다!

 

이렇게 여러 디폴트 메서드들을 사용하면, 다중 상속 역시 문제가 되지 않습니다!

 

디폴트 메서드 주의점

1. 상속

상속은 좋다고 다 사용하는 것이 아닙니다. 특히 극히 일부분의 메서드를 (1개, 2개 등) 재활용하겠다고 백개가 넘는 메서드를 가지는 클래스를 상속받는 것은 좋은 생각이 아닙니다.

이런 상황에서는 차라리 멤버 변수를 이용해서 클래스에서 필요한 메서드를 직접 호출하는 메서드를 작성하는 것을 추천합니다. (이러한 기능을 델리게이션(위임)이라고 합니다.)

 

또한 원래 동작을 변화시키고 싶지 않은 경우 클래스를 final로 선언해 상속을 막기도 합니다.

 

2. 해석 규칙

기본적으로 같은 시그니처를 가지는 메서드를 상속받을 때는 다음과 같습니다.

 

1. 클래스가 항상 우선! 클래스나 슈퍼클래스에서 정의한 메서드가 디폴트 메서드보다 우선권을 가짐.

클래스 상속과, 인터페이스 구현이 둘 다 되어 있다면, 반드시 클래스 상속이 우선입니다.

'클래스 정의 메서드 >>> 디폴트 메서드'

 

2. 1번 규칙 이외의 상황에서는 서브 인터페이스가 우선!

public interface Mammal {
    default void run(){
        System.out.println("달립니다.");
    };

    default void birth(){
        System.out.println("새끼를 낳습니다");
    }

    default void eat(){
        System.out.println("밥을 먹습니다.");
    };
}

public interface Dog extends Mammal{
    default void birth() {
        System.out.println("강아지를 낳습니다.");
    }
}

public class AnimalTest implements Mammal, Dog{
    public static void main(String[] args) {
        new AnimalTest().birth();	// 강아지를 낳습니다.
    }
}

포유류(Mammal) 인터페이스를 상속받는 서브 인터페이스 Dog가 있습니다. 여기서 birth( )를 실행하면,

서브 인터페이스인 Dog.birth( )가 실행됩니다!

 

 

 

 

3. 이래도 우선순위가 결정되지 않았다면, 명시적으로 디폴트 메서드를 오버라이드하고 호출

그럼 다음과 같은 예시를 봅시다.

 

어류, 조류, 포유류의 특징을 모두 가지고 있는 동물을 만들어 봅시다.

(예외가 많이 있지만, 아주 보편적인 형태로 메서드를 만듭시다.)

public interface Mammal {
    default void run(){
        System.out.println("달립니다.");
    };

    default void birth(){
        System.out.println("새끼를 낳습니다");
    }

    default void eat(){
        System.out.println("밥을 먹습니다.");
    };
}

public interface Pisces {
    default void swim(){
        System.out.println("수영을 합니다.");
    };

    default void birth(){
        System.out.println("알을 낳습니다");
    }

    default void eat(){
        System.out.println("밥을 먹습니다.");
    };
}

public interface Birds {
    default void fly(){
        System.out.println("하늘을 납니다.");
    };

    default void birth(){
        System.out.println("알을 낳습니다");
    }

    default void eat(){
        System.out.println("밥을 먹습니다.");
    };
}

여기서 공통적인 메서드는 birth( )와 eat( )가 있습니다. 두 메서드를 구별할 기준이 없기 때문에 어느 메서드를 선택할지 혼동이 옵니다. 이러한 경우를 충돌이라고 합니다. 이 상황에서는, 어떻게 해결해야 할까요?

 

이러한 경우에는 직접 결정을 해주어도 되고, 완전 다른 경우라면 직접 오버라이드를 해주면 됩니다.

public class NewAnimal implements Birds, Mammal, Pisces {
    public static void main(String[] args) {
        new NewAnimal().birth();
        new NewAnimal().eat();
    }

    // 상속 결정
    public void birth() {
        Birds.super.birth();
    }

    // 직접 오버라이드
    @Override
    public void eat() {
        System.out.println("밥을 먹습니다.");
    }
}

 

 

 

참고자료

Reference : 

Default Methods (The Java™ Tutorials > Learning the Java Language > Interfaces and Inheritance) (oracle.com)

 

Default Methods (The Java™ Tutorials > Learning the Java Language > Interfaces and Inheritance)

The Java Tutorials have been written for JDK 8. Examples and practices described in this page don't take advantage of improvements introduced in later releases and might use technology no longer available. See Java Language Changes for a summary of updated

docs.oracle.com

도서 : 모던 자바 인 액션