결론
일반적으로 다중 구현용 타입으로는 추상 클래스보다 인터페이스가 가장 적합합니다.
복잡한 인터페이스라면 구현하는 수고를 덜어주는 골격 구현을 함께 제공하는 방법을 꼭 고려해봅시다.
골격 구현은 '가능한 한' 인터페이스의 디폴트 메서드로 제공하여 그 인터페이스를 구현한 모든 곳에서 활용하도록 하는 것이 좋습니다.
'가능한 한' 이라고 한 이유는, 인터페이스에 걸려 있는 구현상의 제약 때문에 골격 구현을 추상 클래스로 제공하는 경우가 더 흔하기 때문입니다.
설명
자바가 제공하는 다중 구현 매커니즘은 인터페이스와 추상 클래스, 이렇게 두가지입니다.
자바8부터 인터페이스도 디폴트 메서드를 제공할 수 있게 되어 이제는 두 매커니즘 모두 인스턴스 메서드를 구현 형태로 제공할 수 있습니다.
추상 클래스보다 인터페이스를 우선해야 하는 이유에 대해 작성하도록 하겠습니다.
이유 1. 추상 클래스는 타입의 제약이 따른다.
둘의 가장 큰 차이점은 추상 클래스가 정의한 타입을 구현한 클래스는 반드시 추상 클래스의 하위 클래스가 되어야 한다는 점입니다.
자바는 단일 상속만 지원하므로, 추상 클래스 방식은 타입을 정의하는 데 커다란 제약을 안게 됩니다.
반면, 인터페이스는 선언한 메서드를 모두 정의하고 그 일반 규약을 잘 지킨 클래스라면 다른 어떤 클래스를 상속했든 같은 인터페이스를 구현한 클래스는 같은 타입으로 취급합니다.
이유2. 기존 클래스에 손쉽게 새로운 인터페이스를 구현해넣을 수 있다.
인터페이스가 요구하는 메서드를 추가하고, 클래스 선언에 implements 구문만 추가하면 끝입니다.
반면, 기존 클래스에 새로운 추상 클래스를 끼워넣는 건 어렵습니다.
만약 두 클래스가 같은 추상 클래스를 확장하길 원한다면, 그 추상 클래스는 계층구조상 두 클래스의 공통 조상이어야 합니다.
추상 클래스의 모든 메서드가 강제적으로 원치 않더라도 하위 클래스에 상속을 하기 때문에 이는 계층구조에 커다란 혼란을 가져옵니다.
이유 3. 인터페이스는 믹스인(mixin) 정의에 안성맞춤이다.
믹스인(mixin)?
믹스인이란 클래스가 구현할 수 있는 타입으로, 인터페이스를 사용하면 원래의 '주된 기능' 외에도 특정 선택적 기능을 '혼합(mixed in)'해서 제공한다고 선언하는 효과를 줍니다.
예를 들어, Comparable은 자신을 구현한 클래스의 인스턴스끼리는 순서를 정할 수 있다고 선언하는 믹스인 인터페스입니다.
반면, 추상 클래스는 믹스인을 정의할 수 없습니다.
이유는 기존 클래스에 덧씌울 수 없기 때문입니다. 클래스는 두 부모를 섬길 수 없고, 클래스 계층구조에는 믹스인을 삽입하기에 적절한 위치가 없습니다.
이유 4. 인터페이스로는 계층구조가 없는 타입 프레임워크를 만들 수 있다.
현실에 타입을 계층적으로 정의할 수 있는 구조도 있지만, 계층을 엄격하게 구분하기 어려운 개념도 있습니다.
예를 들어, 가수(Singer) 인터페이스와 작곡가(Songwriter) 인터페이스가 있다고 해봅시다.
public interface Singer {
void sing(Song s);
}
public interface Songwriter {
Song compose(int charPosition);
}
이 코드처럼 타입을 인터페이스로 정의하면, 작곡도 하는 가수 인터페이스를 간편하게 생성할 수 있습니다.
심지어 새로운 메서드까지 추가한 제 3의 인터페이스를 정의할 수도 있습니다.
public interface SingerSongwriter extends Singer, Songwriter {
void actSensitive(); //새로운 메서드 추가
}
반면, 같은 구조를 클래스로 만들려면 가능한 조합 전부를 각각의 클래스로 정의해야 합니다. (다중 상속은 불가하므로)
속성이 n개라면 지원해야 할 조합의 수는 2^n개나 됩니다.
흔히 조합 폭발(combinatorial explosion)이라 부르는 현상입니다.
이유 5. 디폴트 메서드를 구현하여 프로그래머의 일감을 덜어줄 수 있다.
인터페이스의 메서드 중 구현 방법이 명백한 것이 있다면, 그 구현을 디폴트 메서드로 제공함으로써 프로그래머의 일감을 덜어줄 수 있습니다.
예) 자바 8의 Collection 인터페이스에 추가된 디폴트 메서드
default boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
boolean result = false;
for (Iterator<E> it = iterator(); it.hasNext(); ) {
if (filter.test(it.next())) {
it.remove();
result = true;
}
}
return result;
}
참고 @implSpec
디폴트 메서드를 제공할 때는 상속하려는 사람을 위한 설명을 @impleSpec 자바독 태그를 붙여 문서화해야 합니다. (아이템 19)
디폴트 메서드의 제약
- equals와 hashCode와 같은 Object 메서드를 디폴트 메서드로 제공하면 안 됩니다.
- 인터페이스는 인스턴스 필드를 가질 수 없습니다.
- public이 아닌 정적 멤버를 가질 수 없습니다. (단, private 정적 메서드는 예외) (오버라이딩 때문)
- 여러분이 만들지 않은 인터페이스에는 디폴트 메서드를 추가할 수 없습니다.
인터페이스와 추상 클래스의 혼합: 추상 골격 구현(skeletal implementation)
인터페이스와 추상 골격 구현 클래스를 함께 제공함으로써 인터페이스와 추상 클래스의 장점을 모두 취할 수 있습니다.
- 인터페이스로 타입을 정의합니다.
- 필요하다면, 디폴트 메서드도 함께 제공합니다.
- 골격 구현 클래스에는 특정 구현 클래스에 필요한 메서드들을 구현합니다.
이렇게 해두면 단순히 골격 구현을 확장하는 것만으로 이 인터페이스를 구현하는 데 필요한 일이 대부분 완료됩니다.
바로 템플릿 메서드 패턴입니다.
참고 골격 구현 클래스의 관례상 이름 Abstract
인터페이스의 이름이 Interface라면, 그 골격 구현 클래스의 이름은 AbstractInterface로 짓습니다.
예) 컬렉션 프레임워크의 AbstractCollection, AbstractSet, AbstractList, AbstractMap 각각이 바로 핵심 컬렉션 인터페이스의 골격 구현입니다.
어쩌면 SkeletalCollection, SkeletalSet, SkeletalList, SkeletalMap 형태가 더 적절할지도 모르겠지만, 이미 Abstract를 접두어로 쓰는 형태가 확고히 자리잡았습니다.
예시) 골격 구현을 활용해 List 구현체를 반환하는 정적 팩터리 메서드
static List<Integer> intArrayAsList(int[] a) {
Objects.requireNonNull(a);
return new AbstractList<>() {
@Override public Integer get(int i) {
return a[i];
}
@Override public Integer set(int i, Integer val) {
int oldVal = a[i];
a[i] = val;
return oldVal;
}
@Override public int size() {
return a.length;
}
};
}
정리
골격 클래스는 추상 클래스처럼 구현을 도와주는 동시에, 추상 클래스로 타입을 정의할 때 따라오는 심각한 제약에서는 자유롭습니다.
출처
https://www.yes24.com/Product/Goods/65551284
'책 > effective java' 카테고리의 다른 글
[effective java] item 22. 인터페이스는 타입을 정의하는 용도로만 사용하라 (0) | 2023.08.21 |
---|---|
[effective java] item 21. 인터페이스는 구현하는 쪽을 생각해 설계하라 (0) | 2023.08.21 |
[effective java] item 18. 상속보다는 컴포지션을 사용하라 (0) | 2023.08.07 |
[effective java] item 17. 변경 가능성을 최소화하라 (0) | 2023.08.07 |
[effective java] item 16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라 (0) | 2023.08.07 |