본문 바로가기
자바

[모던 자바 인 액션] 디폴트 메서드

by __Minnie_ 2025. 5. 8.
변화하는 API

 

우리가 많은 사용자들에게 인터페이스를 제공한다고 생각해보자. 그러다가 갑자기 인터페이스에 새로운 메서드의 추가가 필요해졌다. 만약 추상 메서드로 추가하게 된다면, 이 인터페이스를 사용하고 있는 기존의 모든 코드들을 변경해야 할 것이다. 이럴 때 사용할 수 있는 것이 디폴트 메서드이다. 디폴트 메서드를 사용하면 기본 구현을 제공하기 때문에 인터페이스에 새로운 메서들을 추가해도 기존에 사용하던 코드에 사이트 이펙트를 주지 않는다.

 

바이너리 호환성 / 소스 호환성 / 동작 호환성

 

1. 바이너리 호환성

뭔가를 바꾼 이후에도 에러 없이 기존 바이너리가 실행될 수 있는 상황을 의미한다.

 

2. 소스 호환성

고드를 고쳐도 기존 프로그램을성공적으로 재컴파일 할 수 있는 것을 의미한다. 

 

3. 동작 호환성

코드를 바꾼 후에도 같은 입력값이 주어지면 프로그램이 같은 동작을 실행한다는 것을 의미한다. 

 

 

디폴트 메서드란?

 

디폴트 메서드는 호환성을 유지하면서 API를 바꿀 수 있도록 돕는 기능이다. 디폴트 메서드는 인터페이스에서 선언하며, default 키워드를 사용하고, 정적 메서드처럼 메서드 바디를 포함한다. 

public interface Sized {

	int size();
    
    default boolean isEmpty() {
    	return size() == 0;
    }
}

 

디폴트 메서드에 대한 설명을 듣다보면, 디폴트 메서드를 사용할 수 있는 인터페이스와 추상 클래스의 차이점은 뭘까? 라는 의문이 들 수 있다. 둘 다 추상 메서드와 바디를 포함하는 메서드를 만들 수 있다는 것은 동일하지만, 차이점도 있다. 추상 클래스는 클래스이기 때문에 하나만 상속받을 수 있지만, 인터페이스는 다중 구현이 가능하다. 또한, 추상 클래스는 인스턴스 변수를 가질 수 있지만, 인터페이스는 인스턴스 변수를 가질 수 없다. 

 

선택형 메서드와 동작 다중 상속

 

1. 선택형 메서드

선택형 메서드란 구현 클래스가 꼭 구현하지 않아도 되는 메서드를 의미한다. 디폴트 메서드가 제공되기 전에는 사용하지 않는 메서드의 경우 빈 구현을 제공하는 경우가 많았다. 디폴트 메서드를 사용하면 기본 구현이 제공되기 때문에 필요한 경우에만 오버라이딩해서 선언할 수 있다. 

 

2.  동작 다중 상속

자바에서는 여러개의 인터페이스를 구현할 수 있기 때문에, 여러 인터페이스의 동작을 상속받을 수 있다. 예를 들어서 Moveable, Resizable, Rotatable이라는 인터페이스가 있으면 Moveable, Rotatable이라는 인터페이스를 조합해서 움직일 수 있고, 회전할 수 있는 사람 객체를 만들 수 있고, Moveable, Resizable, Rotatable를 조합해서 움직일 수 있고, 사이즈를 조절할 수 있고, 회전할 수 있는 괴물이라는 객체를 만들 수 있다. 

 

옳지 못한 상속과 델리게이트

 

상속을 사용하면 부모 클래스의 기능을 활용할 수 있다는 장점이 있지만, 항상 좋은 것은 아니다. 만약 부모 클래스에서 100개의 기능을 제공하고 있는데, 1개의 기능만 필요하다면 상속을 하는 것은 오히려 안좋은 결과를 낳게 된다. 이런 경우에는 델리게이트를 사용하는 것이 좋다.

 

델리게이트란 무엇일까? 델리게이트는 클래스 안에 멤버 변수로 다른 객체를 포함하는 것을 의미한다. 상속보다 결합도가 낮고 유연하다는 장점이 있다. 

 

// 기능 제공 클래스
class Printer {
    void print(String msg) {
        System.out.println("Print: " + msg);
    }
}

// 위임 클래스
class MyPrinter {
    private Printer printer = new Printer(); // 델리게이트 대상

    void customPrint(String msg) {
        System.out.println("MyPrinter custom:");
        printer.print(msg);  // 위임
    }
}

 

 

해석 규칙

 

동일한 시그니처를 갖는 디폴트 메서드가 여러 개라면 어떤 디폴트 메서드를 사용하게 될까?

 

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

 

2. 1번 규칙 이외의 상황에서는 서브 클래스가 이긴다. 상속 관계를 갖는 인터페시으가 같은 시그니처를 갖는 메서드를 정의할 때는 서브인 터페이스가 이긴다. 예를 들어 B가 A를 상속받는다면 B가 이긴다. 

 

3. 1, 2번 규칙을 적용해도 우선순위가 결정되지 않는다면, 명시적으로 디폴트 메서드를 오버라이드하고 호출해야 한다. 

 

예제1. 

아래의 경우에는 1, 2번 모두에 해당하지 않기 때문에 C에서 명시적으로 디폴트 메서드를 오버라이드해야 한다. 

static class C implements B, A {
    C() {
    }

    public void hello() {
        Ambiguous.A.super.hello();
    }
}

interface B {
    default void hello() {
        System.out.println("Hello from B");
    }
}

interface A {
    default void hello() {
        System.out.println("Hello from A");
    }
}

 

예제 2.

아래 코드의 경우 2번 룰이 적용되어 서브 클래스인 B가 이긴다. 

static class C implements B, A {
    C() {
    }
    
    public static void main(String ...args) {
    	new C().hello();
    }
}

interface B extends A{
    default void hello() {
        System.out.println("Hello from B");
    }
}

interface A {
    default void hello() {
        System.out.println("Hello from A");
    }
}