티스토리 뷰

728x90
반응형

서론


  • 자바에서는 인터페이스를 구현하는 구현 클래스에서는 해당 인터페이스의 메서드를 모두 구현해야 합니다. 하지만 자바 8이후 부터는 
    인터페이스의 static method, default method를 통해 구현체에서 메서드 구현을 생략할 수 있습니다.
  • 디폴트 메서드의 역할은 기존 인터페이스를 구현하는 구현 클래스는 기본적으로 디폴트 메서드를 상속받게 되며, 구현 클래스에서 따로 구현하지 않아도 됩니다.

 

변화하는 API


💡 릴리즈 1.0 버전의 FileUtils 인터페이스

  • 버전 1에서는 기본적으로 4가지의 기능을 제공하고 있고 각 라이브러리 사용자는 해당 인터페이스를 구현해 사용을 하고 있었습니다. 
    하지만 시간이 흐르자 4가지 기능만으로 해당 인터페이스를 사용하는 사용자들의 불만이 터져나왔습니다. 결국 라이브러리 제공자는 몇 가지 추가 기능을 제공하기로 마음을 먹었습니다. 
public interface FileUtils {

    void upload();
    void download();
    void resizing();
    String getFileName();
}

// FileUtils를 구현해 사용하는 사용자들
public class FileUtilsImpl implements FileUtils {
   ...   
}

 

💡 릴리즈 2.0 버전의 FileUtils 인터페이스

  • 릴리즈 2.0에서는 두가지의 기능이 추가되었습니다. 하지만 기존 릴리즈 1.0을 구현하고 있는 수많은 사용자들에게 문제가 발생합니다. 바로 해당 라이브러리를 사용하고 있는 사용자들은 두 메서드를 구현하고 있지 않기 때문에 재 컴파일시 에러가 발생하게 됩니다. 
    그럼 대체 어떻게 라이브러리 설계자는 이러한 걱정을 없애고 추가 기능을 삽입할 수 있을까요?
// 릴리즈 2.0에 추가된 기능들
public interface FileUtils {

    void upload();
    void download();
    void resizing();
    String getFileName();

    // 2.0 버전에 추가된 메서드들
    int setMaxUploadCount(int maxCount); // 최대 업로드 개수
    boolean accessExtensionSetting(List<String> extensions); // 허용된 확장자 설정
}

 

😱 사용자가 겪는 문제들

  • 기존 FileUtils 인터페이스를 수정하게 되면 해당 인터페이스를 구현한 구현 클래스에서는 메서드를 추가해야 합니다. 하지만 바이너리 호환성으로 인해 잠시나마 에러를 피해갈 수 있겠지만 그것은 정말 잠시일 뿐 이므로 결국 수정을 해야합니다.

바이너리 호환성이란?

  • 뭔가를 변경한 이후에도 에러 없이 기존 바이너리가 실행될 수 있는 상황을 바이너리 호환성이라 합니다.
  • 인터페이스에 새로운 메서드를 추가했을 경우 추가된 메서드를 호출하지 않는 이상 문제가 발생하지 않습니다.

소스 호환성이란?

  • 코드를 수정해도 기존 프로그램을 성공적으로 재컴파일할 수 있음을 의미합니다.
  • 인터페이스에 메서드를 추가하면(default, static 메서드 제외) 소스 호환성이 아닙니다. 추가한 메서드를 구현하도록 구현 클래스를 고처야 하기 때문입니다.

동작 호환성이란?

  • 코드를 변경한 후에 같은 입력값이 주어지면 프로그램이 같은 동작을 실행한다는 의미입니다.

 

디폴트 메서드란 무엇인가?


  • 위에서 FileUtils 인터페이스에 새로운 메서드를 추가하면 발생하는 문제점을 알아보았습니다. 그럼 어떻게 해결할 수 있을지 알아보겠습니다.
  • 자바 8에서는 호환성을 유지하면서 API를 변경할 수 있도록 새로운 기능인 디폴트 메서드를 제공하고 있습니다.
  • default 메서드를 추가하면 구현 클래스에서는 아무런 변경없이 해당 메서드를 상속받게 됩니다. 또한 인터페이스에 디폴트 메서드를 추가하면 소스 호환성이 유지됩니다.
public interface FileUtils {

    void upload();
    void download();
    void resizing();
    String getFileName();

    // 2.0 버전에 추가된 메서드들
    default int setMaxUploadCount(int maxCount) {
        // 로직    
    }
    
    default boolean accessExtensionSetting(List<String> extensions) {
        // 로직    
    }
}

 

💡 추상 클래스와 인터페이스의 차이

  • 클래스는 하나의 추상 클래스만을 상속 받을 수 있지만 인터페이스는 여러 개를 구현할 수 있습니다.
  • 추상 클래스는 인스턴스 변수를 가질 수 있지만 인터페이스는 인스턴스 변수를 가질 수 없습니다.

 

해석 규칙


  • 여러 인터페이스로부터 같은 시그니처를 갖는 디폴트 메서드를 상속받는 상황이 발생할 수도 있습니다. 자바 8에서는 이러한 문제에 대한 해결 규칙을 제공하고 있습니다.

💡 3가지 규칙

  • 클래스가 항상 이깁니다. 클래스나 슈퍼 클래스에서 정의한 메서드가 디폴트 메서드보다 우선권을 갖습니다.
  • 1번 규칙 이외의 상황에서는 서브 인터페이스가 이깁니다. 상속 관계를 갖는 인터페이스에서 같은 시그니처를 갖는 메서드를 정의할
    때는 서브 인터페이스가 이깁니다. 즉, B가 A를 상속 받는다면 B가 이깁니다.
  • 여전히 디폴트 메서드의 우선 순위가 결정되지 않았다면 여러 인터페이스를 상속받는 클래스가 명시적으로 디폴트 메서드를 재정의해야 합니다.

 

예제 1

  • C에서는 B, A를 implements하고 있고 B는 A를 extends하고 있는 상황에서는 어떤게 출력될까요?
    바로 B 인터페이스의 디폴트 메서드가 출력됩니다. 그 이유는 3가지 규칙 중 두 번째인 서브 인터페이스가 우선권을 가지게 되기 때문입니다.
public interface A {

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

public interface B extends A {

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

public class C implements B, A {

    public static void main(String[] args) {
        C c = new C();
        c.hello(); // 어떤게 호출될까? -> Hello B
    }
}

 

예제 2

  • D는 C를 extends하고 있는 동시에  B, A를 implements하고 있는 상황에서는 어떤게 출력될까요?
    바로 C 클래스에 있는 메서드가 호출됩니다. 그 이유는 3가지 규칙 중 첫 번째인 클래스가 항상 이기기 때문입니다.
public interface A {

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

public interface B extends A {

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

public class C implements A {

    public void hello() {
        System.out.println("Hello C");
    }
}

public class D extends C implements B, A {

    public static void main(String[] args) {
        D d = new D();
        d.hello(); // 어떤게 호출될까? -> Hello C
    }
}

 

예제 3

  • 그렇다면 D 클래스는 C를 상속받고 있지만 C에는 아무 메서드가 없고 단순히 A 인터페이스를 implements하고 있는 상황에서는 어떤 메서드가 호출될까요? 
    제 생각에는 클래스가 우선권을 가지니 그래도 C 클래스에게 먼저 도달하고 C 클래스는 A 인터페이스를 implements 하고 있으니 
    A 인터페이스의 메서드가 호출되지 않을까? 생각을 했지만 결과는 B 인터페이스의 메서드가 호출되었습니다.
public interface A {

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

public interface B extends A {

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

public class C implements A {

}

public class D extends C implements B, A {

    public static void main(String[] args) {
        D d = new D();
        d.hello(); // 어떤게 호출될까? -> Hello B
    }
}

 

예제 4

  • A, B 인터페이스가 상속 관계가 아니고 C 클래스에서 A, B를 둘다 implements를 하고 있는 경우 C 클래스에서는 어떤 메서드를 호출해야 하는지 모르기 때문에 컴파일 에러가 발생하게 됩니다. 그렇기 때문에 명시적으로 선언해야 합니다. 아래와 같은 경우는 
    세가지 규칙 중 마지막 규칙에 해당됩니다.
public interface A {

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

public interface B {

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

// 컴파일 에러 발생, A, B 인터페이스의 시그니처 메서드가 동일하여 명시적으로 선언해야 함
public class C implements A, B { 
    
    // 문제 발생
}

// 명시적으로 선언하여 문제 해결
public class C implements A, B {

    @Override
    public void hello() {
        A.super.hello(); // 명시적으로 선언
    }
}

 

 

 

 

 

 

728x90
반응형