자바8의 default 메서드 등장

반응형
728x90
반응형

디폴트 메서드의 등장

자바 8에서는 기본 구현을 포함하는 인터페이스를 정의하는 2가지 방법을 제공한다. 만약 인터페이스를 바꾸게 되었을때, 해당 인터페이스를 구현한 모든 클래스의 구현도 고쳐져야하는 상황이 온다면 매우 당황스러울 것이다. 이 문제점을 자바 8에서 제공된 새로운 기능으로 해결할 수 있다.

 

1) 정적 메서드 : 인터페이스 내부
2) 디폴트 메서드 : 인터페이스의 기본 구현을 제공

 

자바 8에서는 메서드 구현을 포함하는 인터페이스를 정의할 수 있다. 결과적으로 기존 인터페이스를 구현하는 클래스는 자동으로 인터페이스에 추가된 새로운 메서드의 디폴트 메서드를 상속받게 된다. 이렇게 하면 기존의 코드 구현을 바꾸지 않으면서 인터페이스를 바꿀 수 있다. 이 말은 즉, 디폴트 메서드나 정적 메서드가 추가된다면 해당 인터페이스를 구현하던 모든 클래스가 이 메서드들을 구현할 필요 없이 즉시 접근이 가능하다는 사실이다.

 

List<Integer> numbers = Arrys.asList(3, 5, 1, 2, 6);
numbers.sort(Comparator.naturalOrder());

 

위 코드에서 Comparator.naturalOrder`이라는 새로운 메서드가 등장했다. 이 메서드는 Comparator 인터페이스에 새로 추가된 정적메서드이다. 자연 순서(표준 알파벳 순서)로 요소를 정렬할 수 있도록 Comparator 객체를 반환한다. 이렇게 추가된 정적 메서드를 코드 수정 없이 바로 사용이 가능하다.

 

결국 인터페이스가 아니라 추상 클래스가 아닌가?

 

정적 메서드와 디폴트 메서드가 없던 시절에는 인터페이스에 메서드를 추가하면서 여러 문제가 발생했다. 인터페이스에 새로 추가된 메서드를 구현하도록 인터페이스를 구현하는 기존의 모든 클래스에 수정이 일어났기 때문이다. 만약 구현하는 기존의 모든 클래스를 관리할 수 있는 상황이라면 쉽게 해결할 수 있겠지만, 인터페이스가 오픈되어 대중에 공개되었을 때는 애매한 상황에 놓여진다. 이는 디폴트 메서드가 탄생한 이유이다. 디폴트 메서드를 이용하면 인터페이스의 기본 구현을 그대로 상속하므로 인터페이스에 자유롭게 새로운 메서드를 추가할 수 있게된다. 이로써 인터페이스가 추상클래스와 비슷해보일 수도 있지만, 여전히 다른 점이 있으므로 인터페이스와 추상 클래스를 동일하게 생각하면 안된다.

 

 

예시

하나의 인터페이스 A가 있다. 이 인터페이스를 구현하는 클래스 B가 있다. 인터페이스 A에 setUsername() 메서드가 새로 추가되었다. 이렇게 인터페이스 A에 변경이 일어났으므로 우리는 인터페이스 A를 구현한 모든 클래스들을 수정해야한다. 클래스 B에 setUsername() 메서드를 구현하지 않는다. 인터페이스에 새로운 메서드를 추가하면 바이너리 호환성은 유지된다. 바이너리 호환성이란, 새로 추가된 메서드를 호출하지만 않으면 새로운 메서드 구현이 없이도 기존 클래스 파일 구현이 잘 동작한다는 의미이다. 만약 어디선가 클래스 B에 구현되지않은 setUsername() 메서드를 호출한다면 런타임 에러가 발생한다.

 

Exception int thread "main" java.lang.AbstractMethodError : ...

 

그리고 만약, 전체 애플리케이션을 재빌드한다면 클래스 B에 setUsername() 메서드가 구현되지 않았으므로 에러가 발생한다. 공개된 API를 고치면 기존 버전과의 호환성 문제가 발생하게된다. 이런 이유 때문에 공식 자바 컬렉션 API 같은 기존의 API를 변경하기는 어렵다.

 

디폴트 메서드로 이 모든 문제를 해결할 수 있다. 디폴트 메서드를 이용해서 API를 바꾸면 새롭게 바뀐 인터페이스에서 자동으로 기본 구현을 제공하므로 기존 코드를 고치지 않아도 된다.

 

 

디폴트 메서드의 정의

디폴트 메서드의 등장으로 인터페이스는 자신을 구현하는 클래스에서 매소드를 구현할 필요가 없게 되었다. 디폴트 메서드를 누가 구현할까? 인터페이스를 구현하는 클래스에서 구현하지 않은 메서드는 인터페이스 자체에서 기본으로 제공한다. 이름이 디폴트 메서드인 이유이다. 디폴트 메서드는 default 키워드로 시작하며 다른 클래스에 선언된 메서드처럼 메서드 바디를 포함한다.

 

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

 

위 isEmpty() 메서드가 바로 default 메서드이다. 위 Sized 인터페이스를 구현하는 모든 클래스들은 isEmpty의 구현도 상속받게 된다. 이렇게 인터페이스에 디폴트 메서드를 추가하면 소스 호환성이 유지된다. 여기서 잠시 한가지 짚고가자면 함수형 인터페이스는 오직 하나의 추상 메서드만을 포함하는데, 그 외에 default 메서드는 몇개를 선언하여도 상관이 없다. 추상 메서드에 default 메서드는 포함하지 않기 때문이다.

 

 

 

디폴트 메서드의 활용, 선택형 메서드

인터페이스를 구현하는 클래스에서 인터페이스의 메서드를 구현하였지만 내용(body)이 비어있는 파일의 모습을 본 적이 있을 것이다. default 메서드를 사용하면 인터페이스 default 메서드 내에서 기본 구현을 제공할 수 있으므로 구현하는 클래스에서 빈 구현을 할 필요가 없다. 이는 불 필요한 코드를 줄일 수 있다.

 

 

 

디폴트 메서드의 활용, 동작 다중 상속

디폴트 메서드를 사용하면 기존에는 불가능했던 동작 다중 상속 기능도 구현할 수 있다. 클래스는 다중 상속을 이용해서 기존 코드를 재사용할 수 있다. 자바에서 클래스는 1개의 다른 클래스만 상속할 수 있지만 인터페이스는 여러 개를 구현할 수 있다. 하나의 클래스에서 1개의 파일을 상속하고 여러개의 인터페이스를 구현할수 있는데, 이는 디폴트 메서드를 사용하지 않아도 다중 상속을 활용할 수 있다는 사실을 뜻한다. 하지만 이제 자바8부터는 인터페이스가 구현을 포함할 수 있으므로 클래스는 여러 인터페이스에서 동작을 상속받을 수 있게 되었다. 중복되지않는 최소한의 인터페이스를 유지한다면 우리 코드에서 동작을 쉽게 재사용하고 조합할 수 있다.

 

하나의 또다른 장점이 존재한다. 인터페이스 A가 있고 A를 구현한 클래스 B가 있다. 인터페이스 A의 default 메서드가 변경사항이 발생하였다. 이는 인터페이스 A만 수정하면 된다. 이 default 메서드를 호출하는 클래스 B에는 수정이 필요가 없다. 만약 클래스 B가 default 메서드를 정의하지 않은 상황에서는 이러한 장점도 존재한다는 사실을 알아두자.

 

 

 

default 메서드의 세가지 규칙

자바의 클래스는 하나의 부모 클래스만 상속받을 수 있지만 여러 인터페이스를 동시에 구현할 수 있다. 자바 8에서는 디폴트 메서득 추가되었으므로 같은 시그니처를 갖는 디폴트 메서드를 상속받는 상황이 생길 수 있다. 이 상황에서 어떤 인터페이스의 디폴트 메서드를 사용하게 할지는 규칙이 필요하다.

 

1) 클래스가 항상 이긴다. 클래스나 슈퍼 클래스에서 정의한 메서드가 디폴트 메서드보다 항상 우선권을 갖는다.
2) 1번 규칙 이외의 상황에서는 서브 인터페이스가 이긴다. 상속관계를 갖는 인터페이스에서 같은 시그니처를 갖는 메서드를 정의할때는 서브 이너페이스가 이긴다. (B가 A를 상속 받는다면 B가 A를 이긴다.)
3) 여전히 디폴트 메서드의 우선순위가 결정되지 않았다면 여러 인터페이스를 상속받는 클래스가 명시적으로 디폴트 메서드를 오버라이드하고 호출해야한다.

 

public class D implements A {}

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

 

1) 1번 규칙은 클래스의 메서드 구현이 이긴다.라고 설명하고 있다. 클래스 D는 인터페이스 A를 구현했다. 따라서 D는 인터페이스 A의 default 메서드 구현을 상속받는다.
2) 2번 규칙에서는 클래스나 슈퍼클래스에 메서드 정의가 없을때 dafult 메서드를 정의하는 서브 인터페이스가 선택된다. 라고 했다. 컴파일러는 인터페이스 A의 hello나 인터페이스 B의 hello 둘 중 하나를 선택해야한다. 여기서 B가 A를 상속받는 관계이므로 이번에도 B의 hello() 메서드가 실행된다.

 

 

충돌 해결

인터페이스 A가있고, 인터페이스 B가 있다. 인터페이스 A, B는 둘이서 아무런 연관이 없고 클래스 C는 인터페이스 A와 B를 구현하고있다. 만약 인터페이스 A, B가 같은 hello() 라는 디폴트 메서드를 가지고 있을때, 클래스 C에서 hello() 메서드를 호출한다면 A, B 중에 어떤 메서드를 실행해야할까?

 

직접 메서드를 명시해줘야한다.

public class C implements B, A {
  void hello() {
    B.super.hello();
  }
}

 

반응형

'Coding > Java' 카테고리의 다른 글

자바 List의 null 체크 (with isEmpty())  (0) 2020.11.07
java8에서의 날짜/시간 API (LcalDate/LocalTime)  (0) 2020.11.06
의존 객체 주입의 사용 이유  (0) 2020.11.06
제네릭 메서드  (0) 2020.11.06
숫자형 스트림  (0) 2020.11.06

Designed by JB FACTORY