모던 자바 인 액션 13장

디폴트 메소드

모던 자바에서 추가된, 인터페이스에 메소드 내용을 디폴트로 구현할 수 있는 것을 디폴트 메소드라고 한다.

보통, API 개발자와 인터페이스를 받아다 쓰는 서비스 개발자들이 조직이 분리되어서 일할 경우에,

API개발자는 인터페이스를 구현하고, 서비스 개발자들은 그 인터페이스를 implement하여 개발을 하게 된다.

디폴트 메소드가 없는 레거시 자바에서의 문제는

  • API개발자가 인터페이스에 특정 메소드를 추가하고자 할 때

문제가 생긴다.

인터페이스에 특정 메소드 A를 추가 –> 인터페이스를 implement한 모든 클래스들에 컴파일에러 빨간 불 들어옴 –> implement한 모든 클래스들에 A 구현을 해야 함

조금 더 복잡하고 세밀하게 보면, ‘바이너리 호환성’에 의해 새로 추가된 메소드를 호출하지 않는 한 문제가 일어나지 않는다는 내용도 있으나…..

장담하건데 99%의 프로젝트가 ‘인터페이스 하나만 바꿨으니, 그 인터페이스 하나만 재컴파일 해서 실 서비스에 배포’ 하는 그런 위험한 행위를 하진 않는다.

아무튼, 이런 인터페이스 버저닝 및 호환성 문제 등의 이슈가 존재해서, ‘디폴트 메소드’ 라는 것이 존재한다


디폴트 메소드가 무엇인가

말 그대로 메소드인데, 디폴트로 구현한 내용이 포함되어있는 메소드이다.

인터페이스를 구현하는 하위 클래스에서, 인터페이스가 디폴트 메소드를 구현하였다면,

  • 디폴트 메소드를 하위 클래스가 구현한다
    • 구현한 메소드의 로직이 수행됨
  • 디폴트 메소드를 하위 클래스가 구현하지 않는다
    • 인터페이스에서 정의한 디폴트 메소드의 로직이 수행됨

의 동작을 하게 된다.

앞서 말했던, ‘인터페이스의 메소드 추가/수정이 용이함’ 의 장점을 갖고 갈 수 있다.

학생때 자바를 처음 배울때부터 이 문제는 인지할 수 있었는데… 수년이나 지난 후에야 쉽게 해결할 수 있는 spec이 나오다니….


디폴트 메소드 활용 패턴

선택형 메소드

Iterator 인터페이스에서 제공하는 remove메소드가 존재하는데, 레거시 자바에서는 remove를 그냥 멍텅구리 구현으로 강제(?)상속받아 사용하였었다.

모던 자바(8) 로 넘어오면서, Iterator인터페이스는 다음과 같이 remove메소드를 인터페이스 레벨에서 빈 구현으로 제공해 준다.

interface Iterator<T> {
  boolean hasNext();
  T next();
  default void remove() {
    throw new UnsupportedOperationException();
  }
}

동작 다중상속

자바는 기본적으로 클래스 다중상속이 불가능하다.

하위 클래스는 상위 클래스를 ‘단 하나만’ extends 할 수 있기 때문…(정책)

하위 클래스는 상위 인터페이스를 ‘여러개’ implement 할 수 있다.

그런데 (레거시 자바 기준으로는) 인터페이스는 메소드의 body를 가질 수 없기에, ‘동작을’ 다중상속 하는것은 불가능했다.

하지만 자바8 이후에 디폴트 메소드가 생기면서, 동작도 다중상속을 받을 수 있게 되면서 조금 더 개발 및 구현의 편의성도 증대되었다.

예제

게임의 오브젝트가 움직이는 것을 인터페이스로 구현한다고 하자

‘움직임’이라는 것을 다음과 같이 세개의 종류로 나눌 수 있다.

  • 회전 (Rotatable)
  • 좌표이동 (Moveable)
  • 크기변경 (Resizable)

그리고 이 세개의 종류를 각각의 인터페이스로 구현한다

이렇게 세개의 움직임 인터페이스를 적절히 implement(상속)한다면, 별 수고로움 없이 코드를 작성할 수 있다

괴물을 하나 만든다고 하자. 이동도 가능하고,스스로 회전도 하고, 몸집도 불릴 수 있는 무시무시한 괴물 말이다…..

public class Monster implements Rotatable, Moveable, Resizable {
  //구현부는 생략
}

이렇게 구현하면,

  • Rotatable.rotateBy()
  • Moveable.moveHorizontally(), Moveable.moveVertically()
  • Resizable.setRelativeSize()

이 네개의 동작을 괴물 코드의 구현 없이도 상속받아 그대로 사용할 수 있다.

그럼 이번엔 태양을 만든다고 하자. 회전도 하고 움직일수는 있지만, 몸집을 키울 수는 없는 커다란 태양….

public class Sun implements Rotatable, Moveable {
  //구현부는 생략
}

마찬가지로, 디폴트 메소드로 생성된 메소드들은 굳이 클래스에서 구현하지 않아도 동작을 상속받아 사용할 수 있다(아,물론 오버라이딩해서 사용할 수는 있다.)

동작 상속을 보면서 생각할 수 있는 또 다른 장점으로는, 디폴트 메소드가 생김으로써, 인터페이스에서 기존 디폴트 메소드를 고도화하여 로직을 개선시키면, (오버라이딩 하지 않은) 하위 클래스에 자동으로 적용이 된다는 것이다. 뭐…물론 단점이 될 수도 있겠으나 일단 잘 활용한다면 얻을 수 있는게 클 것 같긴 함…


어?

다중 상속이 가능하다…… C++공부할때 많이 봤던 패턴이다….

‘같은 메소드 시그니쳐(메소드명 & argument list)를 갖는 서로 다른 두개의 메소드를 하나의 클래스가 상속받는다면?’ 에 대한 문제

(첨 듣는데 ‘다이아몬드 문제’라고 한다. 다이어그램으로 그리면 다이아몬드 모양으로 나와서 그러는듯)

이에 대해 자바8은 다음과 같은 규칙을 제공한다고 한다.

  1. 클래스가 항상 이긴다. 같은 메소드 시그니쳐를 가진 인터페이스 A와, 상위클래스 B를 상속받는 하위클래스 x가 있다고 하면, x는 B의 메소드를 우선으로 상속받아 사용한다.
  2. 1번규칙 이외의 상황에서는 서브인터페이스가 이긴다. 특정 인터페이스 A를 extends한 B가 있고, 메소드 n을 A와 B가 둘 다 디폴트 메소드로 구현하였다면, A와 B를 implement하는 하위 클래스 x는, B의 메소드를 상속받아 사용한다.(B가 A의 서브 인터페이스이기 때문에)
  3. 여전히 우선순위가 결정되지 않는다면, 상속받는 하위클래스가 명시적으로 오버라이드하여 호출해야 함.

사실 1,2번은 그리 흔한 케이스는 아닐 것 같고…… 3번이 중요하다.

결국 ‘구현/사용하는 현장에서 명시적으로 정해주면 된다’ 가 결론.

시그니쳐가 충돌나는 메소드 상속의 명시적인 문제 해결

이 경우에는 상기 1,2번 조건을 들이대도 적용할 수 없는 규칙이다. 이떄 함부로 컴파일을 때리면

‘Error: class C inherits unrelated defaults for hello() from types B and A.” 라는 에러가 나온다고 한다.

이때 사용하는 키워드는 ‘A.super.method()’ 형식의 문법이다…..

public class C implements A,B {
  void hello() {
    B.super.hello() // B인터페이스의 메소드를 상속받겠다!!!!!
  }
}
네?

자, 그럼 이 상황은 어떻게 될까?


오….문제은행인가 천잰데 ㅋㅋㅋㅋㅋ

바로 이 상황을 ‘다이아몬드 문제’ 라고 한다.

Diamond!!!

얼핏 보기엔 ‘디폴트 메소드가 A에서만 정의되어있으니 그냥 A메소드의 hello()가 실행되는거 아냐?’ 라고 생각하기 쉽다.

하지만 실전은 다르다.

위에 명시해 두었던 2번 규칙을 상기시켜보자. 무조건 하위 인터페이스의 디폴트 메소드를 상속받는다고 하였다.

따라서 저 코드를 걍 쌩으로 컴파일 하면 컴파일 에러가 난다.

저럴땐위에서 배웠듯 D에서 명시적으로 허느 인터페이스의 디폴트 메소드를 사용할 지 선택해 주어야 한다.

아무튼 위의 3가지 규칙을 꼭꼭 명심하자.

마치며

  • 공개된 인터페이스에 추상메소드를 추가하면 호환성이 깨진다
  • 자바 8부터 인터페이스에서 메소드를 구현할 수 있는 디폴트 메소드가 존재한다
  • 디폴트 메소드는 키워드가 default 로 시작하며, 메소드 body를 가질 수 있다
  • 디폴트 메소드 덕분에 깨지는 호환성을 어느정도 커버할 수 있다.
  • 디폴트 메소드가 생김으로써 동작 다중 상속이 가능하다
  • 규칙 3개
    • 클래스가 항상 이긴다. 같은 메소드 시그니쳐를 가진 인터페이스 A와, 상위클래스 B를 상속받는 하위클래스 x가 있다고 하면, x는 B의 메소드를 우선으로 상속받아 사용한다.
    • 위 규칙 이외의 상황에서는 서브인터페이스가 이긴다. 특정 인터페이스 A를 extends한 B가 있고, 메소드 n을 A와 B가 둘 다 디폴트 메소드로 구현하였다면, A와 B를 implement하는 하위 클래스 x는, B의 메소드를 상속받아 사용한다.(B가 A의 서브 인터페이스이기 때문에)
    • 여전히 우선순위가 결정되지 않는다면, 상속받는 하위클래스가 명시적으로 오버라이드하여 호출해야 함.

Leave a Comment