본문 바로가기
Dev Log/Java

Modern Java In Action 정리 - 13 디폴트 메서드

by 삽질하는큐 2020. 7. 11.

자바8 이전에는 인터페이스에 새로운 메소드를 추가하게 된다면 해당 인터페이스를 implement하는 쪽에서 전부 compile error가 나곤 했다. 따라서 하나의 인터페이스를 정의하더라도 굉장히 고심해서 설계해야 했다. 하지만 요구사항은 쉽게 변하고 변경이 많아지기 때문에 이러한 문제는 어떻게든 계속 일어나게 된다. 자바8에서는 인터페이스에 static method 와 default method를 추가해서 이 문제를 해결하고자 했다.

 


인터페이스를 구현하는 클래스는 자동으로 추가된 디폴트 메서드를 상속받게 된다.

default void sort(Comparator<? super E> c) {
    Collections.sort(this, c);
}

 

자바 8에서 List 인터페이스에 이 코드가 추가되어 따로 구현 코드가 추가되지 않더라도 List 객체에서 이를 사용할 수 있다.

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

 

Collection의 stream 메서드도 default 메서드로 정의되어있다.

default Stream<E> stream() {
    return StreamSupport.stream(spliterator(), false);
}

 

 

변화하는 API

다음과 같은 코드가 있다면,

public interface Resizable extends Drawable{

    int getWidth();
    int getHeight();
    void setWidth(int width);
    void setHeight(int height);
    void setAbsoluteSize(int width, int height);
    
}

public interface Drawable {
    void draw();
}

 

사용자가 Ellipse라는 구현체를 만들어서

public class Ellipse implements Resizable {
    // implementations ..
}

public class Square implements Resizable {
    // implementations ..
}

public class Rectangle implements Resizable {
    // implementations ..
}

 

이와 같은 어플리케이션이 생길 수 있다.

public class Game {
    public static void main(String[] args) {
        List<Resizable> resizableShapes = Arrays.asList(new Ellipse(), new Square(), new Rectangle());
        GameUtils.paint(resizableShapes);
    }
}

public class GameUtils {
    public static void paint(List<Resizable> l) {
        l.forEach(r -> {
            r.setAbsoluteSize(42, 42);
            r.draw();
        });
    }
}

 

여기까지는 무난하게 우리가 자주 볼 수 있는 코드인데, 

 

아래처럼 Resizable에 메소드가 추가되었다면.. Ellipse, Square, Rectangle가 모두 setRelativeSize를 구현해주어야 한다.

public interface Resizable extends Drawable {
    ....
    
    void setRelativeSize(int wFactor, int hFactor);
}

 

여기에서 다른 버전 간의 호환성 문제가 생기게 된다.

 

  • 바이너리 호환성: 뭔가를 바꾼 이후에도 에러 없이 기존 바이너리가 실행될 수 있는 상황
  • 소스 호환성: 코드를 고쳐도 기존 프로그램을 성공적으로 재컴파일할 수 있는 상황
  • 동작 호환성: 코드를 바꾼 다음에도 같은 입력값이 주어지면 프로그램이 같은 동작을 실행하는 상황

 

디폴트 메서드

자바 8에서는 호환성을 유지하면서 API를 바꿀 수 있게끔 Default Method 디폴트 메서드를 제공한다.

public interface Resizable extends Drawable {
    ....
    
    default void setRelativeSize(int wFactor, int hFactor) {
        setAbsoluteSize(getWidth() / wFactor, getHeight() / hFactor);
    }
}

추상메서드가 아닌 실제로 구현된 메서드여야 하고, default로 시작하게 된다.

이렇게 하면 기존 코드와의 호환성을 유지한 채 기능 확장을 할 수 있다.

 

이쯤 읽다보면 자바 코어 개발자들이 자기네들 살려고 하는 것이라는 생각이 들게 된다...

 

default 메소드가 있는 interface와 추상 클래스와의 차이점이 이제 문제시 되는 시점이다. 면접 단골 질문이기도 하다!

  • 추상 클래스는 하나만 상속받을 수 있지만, 인터페이스는 다중 구현이 가능하다.
  • 추상 클래스는 인스턴스 변수로 상태를 가질 수 있지만, 인터페이스는 변수를 가질 수 없다.

선택형 메서드

인터페이스의 구현체가 어떠한 메소드를 필요로 할 때도 있고, 그렇지 않을 때도 있다면 default method가 유용할 수 있다. 그냥 구현되어야 할 메소드라면 빈 구현으로 남겨야 하기 때문이다. Iterator의 remove가 좋은 예가 될 수 있는데, 구현체에서 remove가 중요하게 쓰일 것도 있고, 아닐 것도 있을 수 있기 때문이다.

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

 

동작 다중 상속

책에서 틀린 부분이 있다.

디폴트 메서드를 이용하면 기존에는 불가능했던 동작 다중 상속 기능도 구현할 수 있다. (p.420)

하면서 이와 같은 그림을 보여준다.

다중 상속 구현되는 메소드가 같은 시그니처라면, 기존에 불가능했던 동작의 상속이 가능하지만, 책에서는 이를 다루고 있지는 않다. 실제로 책에서 다루는 동작 다중 상속은 여러 인터페이스에서 각자 다른 추상 메소드를 정의하고 이를 조합해서 클래스가 구현할 수 있음을 다루고 있다. 이는 자바 7에서도 가능했었다.

 

public interface Moveable {
    int getX();
    int getY();
    void setX(int x);
    void setY(int y);

    default void moveHorizontally(int distance) {
        setX(getY() + distance);
    }

    default void moveVertically(int distance) {
        setY(getY() + distance);
    }
}

public interface Rotatable {

    void setRotationAngle(int angleInDegrees);
    int getRotationAngle();

    default void rotateBy(int angleInDegrees) {
        setRotationAngle((getRotationAngle() + angleInDegrees) % 360);
    }

}

public interface Resizable extends Drawable {

    int getWidth();
    int getHeight();
    void setWidth(int width);
    void setHeight(int height);
    void setAbsoluteSize(int width, int height);

    default void setRelativeSize(int wFactor, int hFactor) {
        setAbsoluteSize(getWidth() / wFactor, getHeight() / hFactor);
    }

}

 

위와 같이 세 가지의 인터페이스가 있다고 했을 때

public class Monster implements Rotatable, Moveable, Resizable {
    ....
}

 

Monster처럼 세 가지 인터페이스를 구현하는 클래스를 만들 수 있다. 이때, default 메소드는 필수로 오버라이딩해야되는 대상이 아니다.

참고로 이때 Rotatable과 Moveable이 같은 시그니처의 추상 메소드를 선언했다고 하더라도 구현하는 클래스 입장에서는 그게 어디에서 정의되었는 별로 상관이 없이 하나의 메소드로 취급된다.

 

만약에 같은 default 메소드가 여러 인터페이스에 걸쳐 있다면?

디폴트 메서드가 같은 시그니처를 갖는 인터페이스를 여러개 상속할 때 어느 디폴트 메서드를 취할지가 문제시된다.

다중 상속을 지원하는 C++에서는 이를 다이아몬드 문제라고 한다.

 

 

자바에서는 이러한 상황에서 세 가치 규칙을 따른다.

 

1. 클래스가 항상 이긴다. 클래스나 슈퍼 클래스에서 정의한 메서드가 디폴트 메서드보다 우선권을 갖는다.

public class ClassA {
    public void hello() {
        System.out.println("Hello from ClassA");
    }
}

public interface InterfaceA {
    default void hello() {
        System.out.println("Hello from InterfaceA");
    }
}

public class Impl extends ClassA implements InterfaceA {
    public static void main(String[] args) {
        new Impl().hello(); // Hello from ClassA
    }
}

 

2. 둘 다 인터페이스라면 서브 인터페이스가 이긴다. 상속 관계를 갖는 인터페이스에서 같은 시그니처를 갖는 메서드를 정의할 때는 서브인터페이스가 이긴다. 즉 B가 A를 상속받는다면 B가 A를 이긴다.

public interface InterfaceA {
    default void hello() {
        System.out.println("Hello from A");
    }
}

public interface InterfaceB extends InterfaceA{
    default void hello() {
        System.out.println("Hello from B");
    }
}

public class Impl implements InterfaceA, InterfaceB {
    public static void main(String[] args) {
        new Impl().hello(); // Hello from B
    }
}

 

3. 상속 관계도 아닌 전혀 관계 없는 인터페이스라면 명시적으로 디폴트 메서드를 오버라이드하고 호출해야 한다.

public interface InterfaceA {
    default void hello() {
        System.out.println("Hello from A");
    }
}

public interface InterfaceB {
    default void hello() {
        System.out.println("Hello from B");
    }
}

public class Impl implements InterfaceA, InterfaceB {
    public static void main(String[] args) {
        new Impl().hello(); // inherits unrelated defaults for hello() from types InterfaceA and InterfaceB
    }
}

 

B, C가 A를 상속하는데 B, C를 구현하는 경우이더라도 같은 에러가 나게 된다.

public interface InterfaceA {
    default void hello() {
        System.out.println("Hello from A");
    }
}

public interface InterfaceB extends InterfaceA{
    default void hello() {
        System.out.println("Hello from B");
    }
}

public interface InterfaceC extends InterfaceA{
    default void hello() {
        System.out.println("Hello from C");
    }
}

public class Impl implements InterfaceB, InterfaceC {
    public static void main(String[] args) {
        new Impl().hello(); // inherits unrelated defaults for hello() from types InterfaceB and InterfaceC
    }
}