Modern Java Ch13. Default Method
원래 기존의 인터페이스는 추상 메서드만 존재할 수 있었고 이를 상속받는 구현체에서 직접 해당 추상 메서드를 구현했다.
객체지향 설계 5대 원칙 중 하나인 개방 폐쇄 원칙(OCP : Open Close Principle)가 있다.
개방 폐쇄 원칙이란?
- "소프트웨어 요소는 확장에는 열려있으나 변경에는 닫혀있어야 한다." 소프트웨어 구성 요소(컴포넌트, 클래스, 모듈, 함수)는 확장에 대해서 개방(OPEN)되어야 하지만 변경에 대해서는 폐쇄(CLOSE)되어야 한다는 의미라고 볼 수 있다.
자바 8에서는 기본 구현을 포함하는 인터페이스를 정의하는 두 가지 방법을 제공한다.
첫 번째는 인터페이스 내부에 정적 메서드를 사용하는 것이다.
두 번째는 인터페이스의 기본 구현을 제공할 수 있도록 디폴트 메서드기능을 사용하는 것이다.
자바 8에서는 메서드 구현을 포함하는 인터페이스를 정의할 수 있다.
바이너리 호환성
뭔가를 바꾼 이후에도 에러 없이 기존 바이너리가 실행될 수 있는 상황.
바이너리 실행에는 인증, 준비, 해석등의 과정이 포함된다.
예를 들어 인터페이스에 메서드를 추가했을 때 추가된 메서드를 호출하지 않는 한 문제가 일어나지 않는데 이를 바이너리 호환성이라고 한다.
소스 호환성
코드를 고쳐도 기존 프로그램을 성공적으로 재컴파일할 수 있음을 의미한다. 예를 들어 인터페이스에 메서드를 추가하면 소스 호환성이 아니다. 추가한 메서드를 구현하도록 클래스를 고쳐야 하기 때문이다.
동작 호환성
코드를 바꾼 다음에도 같은 입력 값이 주어진면 프로그램이 같은 동작을 실행한다는 의미다.
예를 들어 인터페이스에 메서드를 추가하더라도 프로그램에서 추가된 메서드를 호출할 일은 없으므로 동작 호환성은 유지된다.
디폴트 메서드란?
개념
디폴트 메서드는 인터페이스에 있는 구현 메서드를 의미한다.
기존의 추상 메서드와 다른 점은
- 메서드 앞에 default 예약어를 붙인다.
- 구현부 {}가 있어야 한다.
public interface Interface {
// 추상 메서드
void abstractMethodA();
void abstractMethodB();
void abstractMethodC();
// default 메서드
default int defaultMethodA(){
...
}
}
장점
기존에는 인터페이스에 추상메서드를 추가하게 되면 모든 구현체에 구현을 해야했다.
디폴트 메서드가 등장하면서 인터페이스에 default를 사용하면 추가 변경을 막을 수 있다.
이로써 OCP에서 확장에 개방되어 있고, 변경에 닫혀있는 코드를 설계할 수 있게 된다.
디폴트 메서드 간 충돌
- 여러 인터페이스의 디폴트 메서드 간의 충돌
- 디폴트 메서드와 상위 클래스의 메서드 간의 충돌
디폴트 메서드는 인터페이스를 구현한 클래스에서 코드를 구현할 필요가 없을 뿐이지, 구현을 할 수 없는 것이 아니다.
즉, 인터페이스를 구현하는 클래스에서 디폴트 메서드를 재정의할 수 있다.
따라서, 위와 같은 충돌 상황이 일어나는 클래스에서 디폴트 메서드를 재정의하면 충돌 상황을 해결할 수 있다.
디폴트 메서드 활용 패턴
디폴트 메서드를 이용하는데엔 두 가지 방식이 있다.
선택형 메서드
인터페이스에는 간혹 구현 클래스에서 크게 중요하지 않은 메서드를 정의할 때가 있다.
그로인해, 구현 클래스들은 메서드 오버라이드만 할 뿐 실제 바디에는 내용이 없게 하는 코드들이 생기게 된다.
하지만 디폴트 메서드를 사용하면 기본 메서드 구현을 인터페이스에 상주할 수 있어 이러한 문제를 해결할 수 있다.
public interface Iterator<E> {
...
default void remove() {
throw new UnsupportedOperationException("remove");
}
...
}
동작 다중 상속
자바에서 클래스는 한 개의 다른 클래스만 상속할 수 있지만 인터페이스는 여러 개 구현할 수 있다.
인터페이스를 조합하면 다양한 클래스를 구현할 수 있다.
public interface A {
default void a() {
}
}
public interface B {
default void b() {
}
}
public interface C {
default void c() {
}
}
public class Modern implements A, B, C {
}
옳지 못한 상속
상속으로 코드 재사용 문제를 모두 해결할 수 있는 것은 아니다. 예를 들어 한 개의 메서드를 재사용하려고 100개의 메서드와 필드가 정의되어 있는 클래스를 상속받는 것은 좋은 생각이 아니다. 이럴 때는 델리게이션(delegation), 즉 멤버 변수를 이용해서 클래스에서 필요한 메서드를 직접 호출하는 메서드를 작성하는 것이 좋다.
종종 final로 선언된 클래스를 볼 수 있는데 다른 클래스가 이 클래스를 상속받지 못하게 함으로써 원래 동작이 바뀌지 않길 원하기 때문이다. 예를 들어 String 클래스로 final로 선언되어 있다.
디폴트 메서드에도 이 규칙을 적용할 수 있다. 필요한 기능만 포함하도록 인터페이스를 최소한으로 유지한다면 필요한 기능만 선택할 수 있으므로 쉽게 기능을 조립할 수 있다.
메서드 구현 규칙
public interface A {
default void run() {
System.out.println("A : run!");
}
}
public interface B {
default void run() {
System.out.println("B : run!");
}
}
public class C implements B, A {
@Override
public void run() {
B.super.run();
}
}
만약 상속받은 두 디폴트 메서드의 시그니처가 같다면 반드시 재정의하여 구현해야 한다.
만약 같은 시그니처를 갖는 인터페이스들이 상속관계라면
public interface A {
default void run() {
System.out.println("A : run!");
}
}
public interface B extends A {
default void run() {
System.out.println("B : run!");
}
}
public class C implements B, A {
}
new C().run(); // B : run!
이러한 상황에서는 서브 인터페이스(B)가 우선권을 갖는다.
그리고 클래스에서 정의한 메서드와 인터페이스 디폴트 메서드의 시그니처가 같은 상황도 있을 수 있다.
public interface A {
default void run() {
System.out.println("A : run!");
}
}
public interface B extends A {
default void run() {
System.out.println("B : run!");
}
}
public class C implements A, B {
public void run() {
System.out.println("C : run!");
}
}
new C().run(); // C : run!
이런 경우는 클래스나 슈퍼클래스에서 정의한 메서드가 디폴트 메서드보다 우선권을 갖는다.
'Study > Modern-Java-In-Action' 카테고리의 다른 글
Modern Java Ch16. 안정적 비동기 프로그래밍 (0) | 2023.01.29 |
---|---|
Modern Java Ch15. completableFuture와 Reactive 프로그래밍 (0) | 2023.01.25 |
Modern Java Ch12. 날짜와 시간 API (0) | 2023.01.24 |
Modern Java Ch11. Optional Class (0) | 2023.01.24 |
Modern Java Ch09. 리팩터링_테스팅_디버깅 (0) | 2023.01.24 |
댓글