FeGwan-Training / FeGwan

0 stars 0 forks source link

Chapter13. 디폴트 메서드 #27

Closed MyeoungDev closed 1 year ago

MyeoungDev commented 1 year ago

Discussed in https://github.com/FeGwan-Training/FeGwan/discussions/26

Originally posted by **MyeoungDev** June 19, 2023 # 📕 Modern Java In Action Chapter_13 디폴트 메서드 # 13.0.0 디폴트 메서드 **인터페이스를 구현하는 클래스는** **인터페이스에서 정의하는** **모든 메서드 구현을 제공하거나** 아니면 **슈퍼클래스의 구현을 상속받아야 한다.** 이미 설계가 끝난 **인터페이스**에 새로운 메서드를 추가하거나 **변경**하고 싶을 경우, **구현하고 있는 모든 클래스의 구현도 수정**해야 한다는 문제점이 존재한다. Java 8에서는 기본 구현을 포함하는 인터페이스를 정의하는 두 가지 방법을 제공한다. - **정적 메서드(static method)** - **디폴트 메서드(default method)** 즉, Java 8 부터는 **메서드 구현을 포함하는 인터페이스를 정의할 수 있다.** 인터페이스에 디폴트 메서드를 추가하게 되면, 구현 클래스는 자동으로 해당 디폴트 메서드를 상속받아 사용할 수 있게 된다. 이렇게 하면 기존의 코드 구현을 바꾸도록 강요하지 않으면서도 인터페이스를 바꿀 수 있다. ![image](https://github.com/MyeoungDev/MyeoungDev/assets/73057935/18f8caac-940b-446a-9630-13c74c96f957) 디폴트 메서드는 주로 라이브러리 설계자들이 사용한다. 디폴트 메서드를 이용하면 자바 API의 호환성을 유지하면서 라이브러리를 바꿀 수 있다. 디폴트 메서드를 이용하면 인터페이스의 기본 구현을 그대로 상속하므로 인터페이스에 자유롭게 새로운 메서드를 추가할 수 있다. ```java /* List 인터페이스에 정의되어 있는 default method sort */ default void sort(Comparator c) { Collections.sort(this. c); } ``` 여기서 `default` 키워드는 해당 메서드가 디폴트 메서드임을 가리킨다. 여기서 `sort` 메서드는 `Collections.sort` 메서드를 호출한다. 이 메서드 덕분에 `List` 에서 직접 `sort` 를 호출할 수 있게 되었다. ```java /* Comparator 인터페이스에 정의되어 있는 static method */ public static > Comparator naturalOrder() { return (Comparator) Comparators.NaturalOrderComparator.INSTANCE; } /* Collection 인터페이스에 정의되어 있는 default method */ default Stream stream() { // StreamSupoort.stream() 이라는 메서드에 다른 default method 인 spliterator() 호출 return StremaSupport.stream(spliterator(), false); } ``` > 정적 메서드와 인터페이스 > 보통 자바에서는 인터페이스 그리고 인터페이스의 인스턴스를 활용할 수 있는 다양한 정적 메서드를 정의하는 유틸리티 클래스를 활용한다. > 예를 들어 `Collections` 는 `Collection` 객체를 활용할 수 있는 유틸리티 클래스다. > **Java 8에서는 인터페이스에 직접 정적 메서들르 선언**할 수 있으므로 **유틸리티 클래스를 없애고 직접 인터페이스 내부에 정적 메서드를 구현할 수 > 있다.** > 그럼에도 불구하고 **과거 버전과의 화환성을 유지할 수 있도록** 자바 API에는 유틸리티 클래스가 남아 있다. # 13.1 변화하는 API 만약, 자바 라이브러리 설계자가 되었다고 가정을 한다. 나는 사용자가 많은 라이브러리 설계자가 되었고, 이미 많은 사람들이 사용하고 있다. 그러나, 버전을 업데이트 하는 도중 새로운 메서드가 필요하다가 생각하여 인터페이스에 추가하였고, 해당 구현 클래스들에 메서드를 구현하여 새로운 버전을 릴리즈 하였다. 그럼에도 불구하고, 이전에 이미 사용하고 있던 사용자는 문제에 봉착하게 된다. ## 13.1.1 API 버전 1 ```java /* 라이브러리 초기 코드 */ public interface Resizable extends Drawable { int getWitdh(); int getHeight(); void setWidth(int width); void setHeight(int height); void setAbsoluteSize(int width, int height); } ``` ```java /* 사용자가 Resizable 라이브러리를 사용하여 작성한 코드 */ public class Ellips implements Resizable { ... } public class Game { public static void main(String[] args) { List resizableShapes = Arrays.asList( new Square(), new Rectangle(), new Ellipse() ); Utils.paint(resizableShapes); } } public class Utils { public static void paint(List list) { list.forEach(r -> { r.setAbsoluteSize(42, 42); r.draw(); }); } } ``` ## 13.1.2 API 버전2 시간이 지나 `Resizable` 을 구현하는 `Square` 와 `Rectangle` 구현을 개선해달라는 많은 요청을 받아 수정을 진행했다. ```java /* 수정한 API ver2 Resizable */ public interface Resizable extends Drawable { int getWitdh(); int getHeight(); void setWidth(int width); void setHeight(int height); void setAbsoluteSize(int width, int height); void setRelativeSize(int wFactor, int hFactor); } ``` ### 사용자가 겪는 문제 - `Reizable` 을 구현하는 모든 클래스는 `setRelativeSize` 메서드를 구현해야 한다. - 하지만, 사용자가 직접구현한 `Ellipse` 는 `setRelativeSize` 메서드를 구현하지 않고 있다. - `Ellipse` 는 `setRelativeSize` 메서드를 정의하지 않았으므로 런타임 에러가 난다. - `java.lang.AbstractMethodError` , `is not abstract and does not override abstract mehod` 이렇듯 공개된 API를 고치면 기존 버전과의 호환성 문제가 발생한다. 예전 버전과 새로운 버전을 직접 관리하는 방법도 있지만 관리하기가 복잡하고, 사용자는 두 가지 라이브러리를 모두 사용해야하는 상황이 생기기도 한다. 이는 결국 프로젝트에서 로딩해야 할 클래스 파일이 많아지면서 메모리 사용과 로딩 시간 문제가 발생한다. 따라서, 디폴트 메서드를 사용하면 이러한 문제를 해결할 수 있다. > 바이너리 호환성, 소스 호환성, 동작 호환성 > 자바 프로그램을 바꾸는 것과 관련된 호환성 문제는 크게 바이너리 호환성, 소스 호환성, 동작 호환성 세 가지로 분류할 수 있다. > 바이러니 호환성: 뭔가를 바꾼 이후에도 에러 없이 기존 바이너리가 실행될 수 있는 상황 > 소스 호환성: 코드를 고쳐도 기존 프로그램을 성공적으로 재 컴파일할 수 있음 > 동작 호환성: 코드를 바꾼 다음에도 같은 입력값이 주어지면 프로그램이 같은 동작을 실행한다는 의미. > # 13.2 디폴트 메서드란 무엇인가? 디폴트 메서드는 `default` 란 키워드로 시작하며 다른 메서드처럼 메서드 바디를 포함한다. ```java public interface Sized { int size(); /* default method */ default boolean isEmpty() { return size() == 0; } } ``` `Sized` 인터페이스를 구현하는 모든 클래스는 `isEmpty` 의 구현도 상속받는다. 즉, 인터페이스에 디폴트 메서드를 추가하면 소스 호환성이 유지된다. 이전 `Resizable` 예제에서 `setRelativeSize` 메서드를 디폴트 메서드로 선언하게 된다면 호환성을 유지하면서 라이브러리를 고칠 수 있다. ```java default void setRelativeSize(int wFactor, int hFactor) { setAbsoluteSiz(getWidth() / wFactor, getHeight() / hFactor); } ``` ### 그렇다면 추상 클래스와 자바 8의 디폴트 메서드를 사용한 인터페이스와 무슨 차이? 이제 공통점으로 **둘 다 추상 메서드와 바디를 포함한 메서드를 정의할 수 있다.** 그러나 클래스는 하나의 추상 클래스만 상속받을 수 있지만, 인터페이스는 여러 개를 구현할 수 있다. 또한, 추상클래스는 인스턴스 변수로 공통 상태를 가질 수 있다. 하지만 인터페이스는 인스턴스 변수를 가질 수 없다. # 13.3 디폴트 메서드 활용 패턴 디폴트 메서드를 이용하는 두 가지 방식이 존재한다. - 선택형 메서드(Optional Method) - 다중 상속(Multiple Inheritance Of Behavior) ## 13.3.1 선택형 메서드 `Iterator` 인터페이스에 `remove` 메서드가 존재 했다는 것을 알고있었나? 사용자들이 `remove` 메서드는 잘 이용하지 않는다. 따라서, 해당 인터페이스의 많은 구현 클래스에 내부 구조가 비어있는 메서드가 존재했다. 디폴트 메서드를 이용함으로써 해당 문제를 해결하여 빈 메서드 구현을 할 필요가 없어졌고, 불필요한 코드를 줄일 수 있다. ```java /* 실제 Iterator 인터페이스에 구현된 remove 메서드 */ default void remove() { throw new UnsupportedOperationException("remove"); } ``` ## 13.3.2 동작 다중 상속 디폴트 메서드를 이용하면 기존에는 불가능했던 동작 다중 상속 기능도 구현할 수 있다. ![image](https://github.com/MyeoungDev/MyeoungDev/assets/73057935/562181c6-b1aa-4613-a9bf-6519427f3aaf) 자바에서 클래스는 한 개의 다른 클래스만 상속할 수 있지만 인터페이스는 여러 개 구현할 수 있다. ```java /* 총 한 개의 클래스를 상속받고, 4개의 인터페이스를 구현하고 있다. */ public class ArrayList extends AbstractList implements List, RandomAccess, Cloneable, Serializable { ... } ``` ### 다중 상속 형식 결과적으로 해당 `ArrayList` 는 `AbstractList`, `List` , `RandomAccess`, `Clonable`, `Serializable`, `Iterable`, `Collection` 의 서브형식(SubType) 이 된다. 따라서, 디폴트 메서를 사용하지 않아도 다중 상속을 활용할 수 있다. Java 8 에서는 인터페이스가 구현을 포함할 수 있으므로 클래스는 여러 인터페이스에서 동작(구현 코드)을 상속 받을 수 있다. 중복되지 않는 최소한의 인터페이스를 유지한다면 우리 코드에서 동작을 쉽게 재사용하고 조합할 수 있다. ### 기능이 중복되지 않는 최소의 인터페이스 다양한 특성을 나타내기 위해 인터페이스를 정의한다. 예를 들어 어떤 모양은 회전할 수 있으며, 어떤 모양은 회전할 수 없는 모양일 경우 최대힌 기존 코드를 재사용해서 아래와 같이 구현할 수 있다. ```java /* 회전이 가능한 모양은 해당 인터페이스를 implements 하기만 하면 * setRotaionAngle, getRotationAngel 구현해야 하지만, rotateBy 는 기본구현으로 제공됨으로 * 따로 구현해야 할 필요가 없다. */ public interface Rotatable { void setRotationaAngle(int angleInDegress); int getRotationAngle(); default void rotateBy(int angleInDegress) { setRotationAngle((getRotationAngle() + angleIndegress) % 360); } } ``` ### 인터페이스 조합 인터페이스를 조합해서 필요한 다양한 클래스를 구현할 수 있다. ```java public class Monster implements Rotatable, Moveable, Resizable { ... } public class Sun implements Moveable, Rotatable { ... } ``` ![image](https://github.com/MyeoungDev/MyeoungDev/assets/73057935/60440c97-5ef2-4b0f-af87-39bacf3fa775) 위와 같이 디폴트메서드를 제공할 경우, 디폴트 메서드를 수정할 경우 구현 클래스들은 수정할 필요가 없어진다. (단, 해당 메서드를 Override 하지 않았다는 가정) 또한, 구현의 클래스의 수가 많아지더라도 해당 메서드를 구현 클래스에 다시 작성해야 하는 문제가 사라지게 된다. > **옳지 못한 상속** > **상속으로 코드 재사용 문제를 모두 해결할 수 있는 것은 아니다.** > 예를 들어, 한 개의 메서드를 재사용하려고 100개의 메서드와 필드가 정의되어 있는 클래스를 상속받는 것은 좋은 생각이 아니다. > 이럴 때는 **델리게이션(Delegation) 즉, 멤버 변수를 이용해서 클래스에서 필요한 메서드를 직접 호출하는 메서드를 작성하는 것이 좋다. > 종종** `final` **로 선언된 클래스가 있다.** 이는 **다른 클래스가 이 클래스를 상속받지 못하게 함으로써 원래 동작이 바뀌지 않길 원하기 때문이다. 대> 표적으로** `String` **클래스가 존재한다. > > 우리의 디폴트 메서드에도 이 규칙을 적용할 수 있다. > 필요한 기능만 포함하도록 인터페이스를 최소한으로 유지한다면 필요한 기능만 선택할 수 있으므로 쉽게 기능을 조립할 수 있다.** > ## 13.4.2 디폴트 메서드를 제공하는 서브인터페이스가 이긴다 ![image](https://github.com/MyeoungDev/MyeoungDev/assets/73057935/b9199104-fe99-4661-afc0-9abaef2f9cdd) 위의 경우 컴파일러는 어떤 메서드 정의를 사용할까? 결과적으로, B가 A를 상속받았으므로 컴파일러는 B의 `hello` 를 선택한다. ![image](https://github.com/MyeoungDev/MyeoungDev/assets/73057935/7c21962d-bb21-45f3-a818-f8bb6bf166c5) ```java public class D implements A{} public class C extends D implements B, A { public static void main(String[] args) { new C().hello(); } } ``` D 클래스의 경우 A의 디폴트 메서드를 상속받고 있다. C의 클래스의 경우에는 B, A 두 인터페이스를 구현하고있고, 디폴트 메서드를 재정의 하지 않았다. 결국 위의 케이스와 동일하게 A의 디폴트 메서드를 재정의한 B의 메서드를 상속받아 `Hello from B` 가 출력되게 된다. ## 13.4.3 충돌 그리고 명시적인 문제 해결 ![image](https://github.com/MyeoungDev/MyeoungDev/assets/73057935/d0555f22-22e2-4982-9f31-cdd28229bd24) ```java public interface A { default void hello() { System.out.println("Hello from A"); } } public interface B { default void hello() { System.out.println("Hello from B"); } } public classs C implements B, A { } ``` 위의 경우는 자바 컴ㅍ파리어는 어떤 메서드를 호출해야 할지 알 수 없으므로 `Error:class C inherits unrelated defaults for hello() from types B and A` 같은 에러가 발생한다. ### 충돌 해결 크래스와 메서드 관계로 디폴트 메서드를 선택할 수 없는 상황에서는 선택할 수 있는 방법이 없다. 이 경우에는 해당 메서드를 명시적으로 선택해야 한다. ```java public class C implements B, A { void hello() { B.super.hello(); } } ``` ## 13.4.4 다이아몬드 문제 ```java public interface A { default void hello() { System.out.println("Hello from A"); } } public interface B extends A { } public interface C extends A { } public class D implements B, C { public static void main(String[] args) { new D().hello(); } } ``` ![image](https://github.com/MyeoungDev/MyeoungDev/assets/73057935/78daea1d-92ad-4f3a-88d0-3a627746c4ec) - A만 디폴트 메서드를 정의하고 있기에 출력 결과는 `Hello from A`가 된다. - 만약, B에 디폴트 메서드 `hello` 가 존재한다면 B에서 정의한 메서드가 호출된다. - 만약, B 와 C 모두 `hello` 메서드가 존재한다면 앞서 말한것 처럼 충돌이 발생한다. 해결하기 위해선 명시적으로 작성해야 한다. ### 3가지 규칙을 이용한 충돌 해결 1. **클래스가 항상 이긴다.** 1. 클래스나 슈퍼클래스에서 정의한 메서드가 디폴트 메서드보다 우선권을 갖는다. 2. 위 규칙 이외의 상황에서는 **서브인터페이스가 이긴다.** 1. 상속관계를 갖는 인터페이스에서 같은 시그니처를 갖는 메서드를 정의할 때는 서브인터페이스가 이긴다. 3. 여전히 디폴트 메서드의 우선순위가 결정되지 않았다면 **여러 인터페이스를 상속받는 클래스가** **명시적으로 디폴트 메서드를 오버라이드하고 호출해야 한다.**
HoFe-U commented 1 year ago

수고하셨습니다. 긴글 잘 읽었습니다.