-
아이템 20. 추상 클래스보다는 인터페이스를 우선하라Study/Effective Java 2023. 8. 3. 15:06
자바 8부터 인터페이스도 디폴트 메서드(default method)를 제공할 수 있다.
자바가 제공하는 다중 구현 메커니즘은 인터페이스, 추상 클래스 이렇게 두 가지다.
자바 8부터 인터페이스도 디폴트 메서드(default method)를 제공할 수 있게 되어 이제는 두 메커니즘 모두 인스턴스 메서드를 구현 형태로 제공할 수 있다.public interface TimeClient { void setTime(int hour, int minute, int second); void setDate(int day, int month, int year); void setDateAndTime(int day, int month, int year, int hour, int minute, int second); LocalDateTime getLocalDateTime(); static ZoneId getZonedId(String zoneString) { try { return ZoneId.of(zoneString); } catch (DateTimeException e) { System.err.println("Invalid time zone: " + zoneString + "; using default time zone instead."); return ZoneId.systemDefault(); } } default ZonedDateTime getZonedDateTime(String zoneString) { return ZonedDateTime.of(getLocalDateTime(), getZonedId(zoneString)); } // 인터페이스를 진화시킬 수 있다. }
구현이 명백한 것은 인터페이스의 디폴트 메서드를 사용해 프로그래머의 일감을 덜어줄 수 있다.
추상클래스가 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위 클래스가 되어야 한다. 자바는 단일 상속만을 지원하므로 추상클래스 방식은 새로운 타입을 정의하는 데 커다란 제약을 안게 된다.
기존 클래스에도 손쉽게 새로운 인터페이스를 구현해넣을 수 있다.
인터페이스는 클래스 선언에 implements 구문만 추가하면 끝이다.
반면 기존 클래스 위에 새로운 추상 클래스를 끼워넣기는 어려운 게 일반적이다. 두 클래스가 같은 추상 클래스를 확장하길 원한다면, 그 추상 클래스는 계층구조상 두 클래스의 공통 조상이어야 한다.
인터페이스는 믹스인(mixin)정의에 안성맞춤이다.
원래의 '주된 타입'외에도 특정 선택적 행위를 제공한다고 선언하는 효과를 준다.
public class SimpleTimeClient implements TimeClient, Comparable { // ... 코드생략 @Override public int compareTo(Object o) { // ... 코드생략 } }
Comparable은 자신을 구현한 클래스의 인스턴스끼리는 순서를 정할 수 있다고 선언하는 믹스인 인터페이스다. 대상 타입의 주된 기능에 선택적 기능을 '혼합(mixed in)' 한다고 해서 믹스인이라 부른다.
인터페이스로는 계층구조가 없는 타입 프레임워크를 만들 수 있다.
타입을 계층적으로 정의하면 수많은 개념을 구조적으로 잘 표현할 수 있지만, 현실에는 계층을 엄격히 구분하기 어려운 개념도 있다.
public interface SingerSongwriter extends Singer, Songwriter { // Singer, Songwriter <= 계층구조가 없음. AudioClip strum(); void actSensitive(); }
래퍼 클래스(아이템 18)와 함께 사용하면 인터페이스는 기능을 향상 시키는 안전하고 강력한 수단이 된다.
추상클래스로 정의해두면 그 타입에 기능을 추가하는 방법은 상속뿐이다. 상속해서 만든 클래스는 래퍼 클래스보다 활용도가 떨어지고 깨지기는 더 쉽다.
인터페이스와 추상 골격 구현(skeletal implementation) 클래스를 함께 제공하는 식으로 인터페이스와 추상 클래스의 장점을 모두 취할 수 있다.
인터페이스 - 타입 정의, 디폴트 메서드 구현
추상 골격 클래스 - 나머지 메서드 구현단순히 골격 구현을 확장하는 것만으로 이 인터페이스를 구현하는 데 필요한 일이 대부분 완료된다. => 템플릿 메서드 패턴
관례상 인터페이스 이름이 Interface라면 그 골격 구현 클래스의 이름은 AbstractInterface로 짓는다. 컬렉션 프레임워크의 AbstractCollection, AbstractSet, AbstractList, AbstractMap 각각이 바로 핵심 컬렉션 인터페이스의 골격 구현이다.
public class IntArrays { static List<Integer> intArrayAsList(int[] a) { // int 배열을 받아 Integer 인스턴스의 리스트 형태로 보여주는 어댑터(Adapter)이기도 하다. Objects.requireNonNull(a); // 다이아몬드 연산자를 이렇게 사용하는 건 자바 9부터 가능하다. // 더 낮은 버전을 사용한다면 <Integer>로 수정하자. return new AbstractList<>() { @Override public Integer get(int i) { return a[i]; // 오토박싱(아이템 6) } @Override public Integer set(int i, Integer val) { int oldVal = a[i]; a[i] = val; // 오토언박싱 return oldVal; // 오토박싱 } @Override public int size() { return a.length; } }; } }
만약 AbstractList를 사용하지 않고 List를 쓰려고 했다면, 이렇게나 많은 메서드를 구현해야한다. Skeleton 역할을 하는 AbstractList를 사용하여 아주 일부분만 재정의할 수 있다.
시뮬레이트한 다중 상속 (simulated multiple inheritance)
인터페이스를 구현한 클래스에서 해당 골격 구현을 확장한 private 내부 클래스를 정의하고, 각 메서드 호출을 내부 클래스의 인스턴스에 전달한다. 래퍼 클래스와 비슷한 이 방식은 다중 상속의 많은 장점을 제공하는 동시에 단점은 피하게 해준다.
public abstract class AbstractCat { protected abstract String sound(); protected abstract String name(); }
public class MyCat extends AbstractCat { @Override protected String sound() { return "인싸 고양이 두 마리가 나가신다!"; } @Override protected String name() { return "유미"; } public static void main(String[] args) { MyCat myCat = new MyCat(); System.out.println(myCat.sound()); System.out.println(myCat.name()); } }
AbstractCat을 상속받은 MyCat이 있다.
▼ Flyable interface
public interface Flyable { void fly(); }
▼ AbstractFlyable
public class AbstractFlyable implements Flyable { @Override public void fly() { System.out.println("너랑 딱 붙어있을게!"); } }
MyCat 클래스가 이미 AbstractCat을 상속받고 있기 때문에, AbstractFlyable 을 추가로 상속 받지 못한다.
public class MyCat extends AbstractCat implements Flyable { private MyFlyable myFlyable = new MyFlyable(); @Override protected String sound() { return "인싸 고양이 두 마리가 나가신다!"; } @Override protected String name() { return "유미"; } public static void main(String[] args) { MyCat myCat = new MyCat(); System.out.println(myCat.sound()); System.out.println(myCat.name()); myCat.fly(); } @Override public void fly() { this.myFlyable.fly(); } private class MyFlyable extends AbstractFlyable { @Override public void fly() { System.out.println("날아라."); } } }
1) Flyable interface를 구현하고 2) private class를 AbstractFlyable을 상속을 받는다. 3) Flyable interface 를 구현한 메서드에, 선언한 private MyFlyable myFlyable 필드의 메서드를 위임하면 마치 상속을 한 것처럼 사용할 수 있다.
템플릿 메서드 패턴
알고리즘 구조를 서브 클래스가 확장할 수 있도록 템플릿으로 제공하는 방법이다. 추상 클래스는 템플릿을 제공하고 하위 클래스는 구체적인 알고리즘을 제공한다.
▼ Abstract class
public abstract class FileProcessor { private String path; public FileProcessor(String path) { this.path = path; } public final int process() { // template method try(BufferedReader reader = new BufferedReader(new FileReader(path))) { int result = 0; String line = null; while((line = reader.readLine()) != null) { result = getResult(result, Integer.parseInt(line)); } return result; } catch (IOException e) { throw new IllegalArgumentException(path + "에 해당하는 파일이 없습니다.", e); } } protected abstract int getResult(int result, int number); }
▼ Concrete class
public class Plus extends FileProcessor { public Plus(String path) { super(path); } @Override protected int getResult(int result, int number) { return result + number; } }
▼ client
public class Client { public static void main(String[] args) { FileProcessor fileProcessor = new Plus("number.txt"); System.out.println(fileProcessor.process()); } }
템플릿 콜백 패턴
Plus 클래스는 FileProcessor를 상속받았다. 상속을 사용하지 않고도 확장할 수 있다.
public class FileProcessor { private String path; public FileProcessor(String path) { this.path = path; } public final int process(BiFunction<Integer, Integer, Integer> operator) { try(BufferedReader reader = new BufferedReader(new FileReader(path))) { int result = 0; String line = null; while((line = reader.readLine()) != null) { result = operator.apply(result, Integer.parseInt(line)); } return result; } catch (IOException e) { throw new IllegalArgumentException(path + "에 해당하는 파일이 없습니다.", e); } } // protected abstract int getResult(int result, int number); }
BiFunction은 두 개의 인자를 받아서 하나의 인자값을 돌려주는 오퍼레이션을 정의할 수 있는 함수형 인터페이스이다. BiFunction을 인자로 받으면서 FileProcessor를 추상클래스가 아닌 클래스로 정의한다.
public class Client { public static void main(String[] args) { FileProcessor fileProcessor = new FileProcessor("number.txt"); System.out.println(fileProcessor.process((a, b) -> a + b)); System.out.println(fileProcessor.process(Integer::sum)); // method reference } }
Client에서 FileProcessor의 processor를 호출하며 (a, b) -> a + b를 넘겨주면 된다. 이것을 method reference로 간추릴 수도 있다.
default 메서드는 equals, hashCode, toString과 같은 Object 메서드를 재정의할 수 없다.
Object 클래스를 오버라이딩하려고 하면 "Default method 'toString' overrides a memeber of 'java.lang.Object'" 컴파일 에러가 발생한다.
왜 자바 개발자들은 오버라이딩을 막았을까?
1) The key goal of adding default methods to Java was "interface evolution", not "poor man's traits."
default 메서드의 용도가 아니다.
2) Adds complexity.
복잡도를 증가시킨다.
Rule #1. Classes win over interfaces.
=> 클래스가 인터페이스를 이긴다.
Rule #2. More specific interfaces win over less specific ones (where specificity means "subtyping").
=> 더 구체적인 인터페이스가 덜 구체적인 인터페이스를 이긴다.만약 interface의 default메서드에서 Object 메서드를 오버라이딩하게되면 이 두가지 규칙 중 무엇을 따라야할 지 복잡해진다.
3) Really only makes sense in toy examples.
사실상 toy project에서나 필요한 기능이다. toString, hashCode, equals 기능은 데이터가 있어야 유의미한 기능이라 인터페이스에서 재정의했을 때 의미가 없다.
4) It's brittle.
깨지기 쉽다. 갑자기 구현하고 있는 Interface에서 Object 메서드들을 오버라이딩했을 때, 클래스의 동작이 바뀌게 된다.
'Study > Effective Java' 카테고리의 다른 글
아이템 22. 인터페이스는 타입을 정의하는 용도로만 사용하라 (0) 2023.08.10 아이템 21. 인터페이스는 구현하는 쪽을 생각해 설계해라. (0) 2023.08.10 아이템 19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라. (0) 2023.08.02 아이템 18. 상속보다는 컴포지션을 사용하라 (0) 2023.07.29 아이템 17. 변경 가능성을 최소화 하라. (0) 2023.07.29