디폴트 메소드는 인터페이스에 선언된 인스턴스 메소드이기 때문에 구현 객체가 있어야 사용할 수 있다. 선언은 인터페이스에서 하고, 사용은 구현 객체를 통해 한다는 것이 어색해 보인다. 디폴트 메소드는 모든 구현 객체에서 공유하는 기본 메소드처럼 보이지만, 사실은 인터페이스에서 디폴트 메소드를 허용한 다른 이유가 있다.
디폴트 메소드의 필요성
인터페이스에서 디폴트 메소드를 허용한 이유는 기존 인터페이스를 확장해서 새로운 기능을 추가하기 위해서이다. 기존 인터페이스의 이름과 추상 메소드의 변경 없이 디폴트 메소드만 추가할 수 있기 때문에 이전에 개발한 구현 클래스를 그대로 사용할 수 있으면서 새롭게 개발하는 클래스는 디폴트 메소드를 활용할 수 있다.
기존에 MyInterface라는 인터페이스와 이를 구현한 MyClassA 라는 클래스가 있었다. 시간이 흘러 MyInterface에 기능을 추가해야할 필요성이 생겼다. 그래서 MyInterface에 추상 메소드를 추가했는데, 엉뚱하게도 MyClassA에서 문제가 발생한다. 그 이유는 추가된 추상 메소드에 대한 실체 메소드가 MyClassA에 없기 때문이다. MyClassA를 수정할 여건이 안된다면 결국 MyInterface에 추상 메소드를 추가할 수 없다. 그래서 MyInterface를 수정할 여건이 안된다면 결국 MyInterface에 추상 메소드를 추가할 수 없다. 그래서 MyInterface에 디폴트 메소드를 선언한다. 디폴트 메소드는 추상 메소드가 아니기 때문에 구현 클래스에서 실체 메소드를 작성할 필요가 없다. 따라서 MyClassA는 아무런 문제 없이 계속 사용이 가능하다.
수정된 MyInterface를 구현한 새로운 클래스인 MyClassB는 method1()의 내용은 반드시 채워야 하지만, 디폴트 메소드인 method2()는 MyInterface에 정의된 것을 사용해도 되고, 필요에 따라 오버라이딩해서 사용할 수도 있다. 다음 예제는 지금까지 설명한 내용을 코드로 표현한 것이다. 기존에 다음과 같이 MyInterface와 MyClassA가 있었다.
// MyInterface.java -- 기존 인터페이스
public interface MyInterface {
public void method1();
}
// MyClassA.java -- 기존 구현 클래스
public class MyClassA implements MyInterface {
@Override
public void method1() {
System.out.println("MyClassA-method1() 실행");
}
}
다음과 같이 MyInterface에 디폴트 메소드인 method2()를 추가해서 수정된 MyInterface를 만들었다.
// MyInterface.java -- 수정 인터페이스
public interface MyInterface {
public void method1();
public default void method2() {
System.out.println("MyInterface-method2() 실행");
}
}
인터페이스를 수정하더라도 MyClassA는 컴파일 에러가 발생하지 않는다. 이번에는 수정된 인터페이스를 구현한 새로운 구현 클래스인 MyClassB를 다음과 같이 정의했다. MyClassB는 method2()를 재정의해서 내용을 변경했다.
// MyClassB.java
public class MyClassB implements MyInterface {
@Override
public void method1() {
System.out.println("MyClassB-method1() 실행");
}
@Override
public void method2() {
System.out.println("MyClassB-method2() 실행");
}
}
다음은 MyClassA와 MyClassB를 객체로 생성해서 인터페이스 변수로 method1()과 method2()를 호출해 보았다. 실행 결과를 보면 MyClassA의 method2()는 MyInterface에 정의된 디폴트 메소드가 실행되었고, MyClassB의 method2()는 오버라이딩한 MyClassB의 method2()가 실행되었다.
// DefaultMethodExample.java
public class DefaultMethodExample {
public static void main(String[] args) {
MyInterface mi1 = new MyClassA();
mi1.method1();
mi1.method2();
MyInterface mi2 = new MyClassB();
mi2.method1();
mi2.method2();
}
}
디폴트 메소드가 있는 인터페이스 상속
인터페이스 간에도 상속이 있다는 것을 이전에 알려주었다. 부모 인터페이스에 디폴트 메소드가 정의되어 있을 경우, 자식 인터페이스에서 디폴트 메소드를 활용하는 방법은 다음 세 가지가 있다.
- 디폴트 메소드를 단순히 상속만 받는다.
- 디폴트 메소드를 오버라이딩해서 실행 내용을 변경한다.
- 디폴트 메소드를 추상 메소드로 재선언한다.
다음과 같이 추상 메소드와 디폴트 메소드가 선언된 ParentInterface가 있다고 가정해보자.
// ParentInterface.java -- 부모인터페이스
public interface ParentInterface {
public void method1();
public default void method2() { /*실행문*/}
}
다음 ChildInterface1은 ParentInterface를 상속하고 자신이 추상 메소드인 method3()을 선언한다.
// ChildInterface1.java -- 자식 인터페이스
public interface ChildInterface1 extends ParentInterface {
public void method3();
}
이 경우 ChildInterface1 인터페이스를 구현하는 클래스는 method1()과 method3()의 실체 메소드를 가지고 있어야 하며 ParentInterface의 method2()를 호출할 수 있다.
ChildInterface1 ci1 = new ChildInterface1() {
@Override
public void method1() {/*실행문*/}
@Override
public void method3() {/*실행문*/}
};
ci1.method1();
ci1.method2(); // ParentInterface의 method2() 호출
ci1.method3();
다음 ChildInterface2는 ParentInterface를 상속하고 ParentInterface의 디폴트 메소드인 method2()를 오버라이딩한다. 그리고 자신의 추상 메소드인 method3()을 선언한다.
// ChildInterface2.java -- 자식 인터페이스
public interface ChildInterface2 extends ParentInterface {
@Override
public default void method2() {/*실행문*/} // 오버라이딩
public void method3();
}
이 경우도 ChildInterface2 인터페이스를 구현하는 클래스를 method1()과 method3()의 실체 메소드를 가지고 있어야 하며, ChildInterface2에서 오버라이딩한 method2()를 호출할 수 있다.
ChildInterface2 ci2 = new ChildInterface2() {
@Override
public void method1() {/*실행문*/}
@Override
public void method3() {/*실행문*/}
};
ci2.method1();
ci2.method2(); // ChildInterface2의 method2() 호출
ci2.method3();
다음 ChildInterface3는 ParentInterface를 상속하고 ParentInterface의 디폴트 메소드인 method2()를 추상 메소드로 재선언한다. 그리고 자신의 추상 메소드인 method3()을 선언한다.
// ChildInterface3.java -- 자식 인터페이스
public interface ChildInterface3 extends ParentInterface {
@Override
public void method2(); // 추상 메소드로 재선언
public void method3();
}
이 경우 ChildInterface3 인터페이스를 구현하는 클래스는 method1()과 method2(), method3()의 실체 메소드를 모두 가지고 있어야 한다.
ChildInterface3 ci3 = new ChildInterface3() {
@Override
public void method1() {/*실행문*/}
@Override
public void method2() {/*실행문*/}
@Override
public void method3() {/*실행문*/}
};
ci3.method1();
ci3.method2(); // ChildInterface3 구현 객체의 method2() 호출
ci3.method3();
'Java 길찾기 > 이것이 자바다' 카테고리의 다른 글
[Java] 중첩 클래스 (0) | 2022.02.08 |
---|---|
[Java] 인터페이스 - 타입 변환과 다형성 (2) (0) | 2022.02.07 |
[Java] 인터페이스 상속 (0) | 2022.02.03 |
[Java] 인터페이스 - 타입 변환과 다형성 (1) (0) | 2022.01.28 |
[Java] 인터페이스 (2) (0) | 2022.01.27 |