솔직히 이번에 공부한 내용이 와닿지 않는다. 이번 Item은 다시 학습해야 한다.
그런데 일단 래퍼클래스의 이점을 취하는 이야기가 또 나와서 좀 이 래퍼 클래스에 대해 알고 싶고, 자바의 AbstractXXX가 골격 구현 개념과 관련 있다는 것을 인지했다. 일단 내가 전체적으로 잘 모르는 것 같고, 아래 설명 중 Object 메서드들은 디폴트 메서드로 제공해서는 안 되므로, 해당 메서드들은 모두 골격 구현 클래스에 구현한다는 것도 와닿지 않는다. 재학습 대상 아이템으로 두자!
✨ 추상 클래스보다는 인터페이스를 우선하라.
자바8부터 인터페이스도 default method를 제공하므로,
인터페이스와 추상클래스 모두 '구현 메서드'를 제공할 수 있다. 다만 추상 클래스 방식은 좀 제약이 걸리는 것이, 자바는 단일 상속만 지원하므로.. 추상 클래스의 하위 클래스가 되어버리면 커다란 제약을 안게 되는 것이다.
반면 인터페이스를 implements하는 것은 깔끔하다. A와 B 두 클래스가 어떤 클래스를 상속하고 있는지는 상관 없이, 원하는 기능의 인터페이스를 끼워넣을 수 있다(손쉽게 새로운 타입을 정의할 수 있다). 예를 들어 자바 플랫폼에서도 Comparable, Iterable, AutoCloseable 인터페이스가 새로 추가됐을 때 표준 라이브러리의 수많은 기존 클래스가 이 인터페이스를 구현한 채 릴리스됐다.
인터페이스는 믹스인(mix-in) 용도로 사용하자. 믹스인이란 클래스에 원래의 '주된 타입' 외에도 특정 선택적 기능을 mixed in 한다는 의미다. → 예를 들어 Comparable은 자신을 구현한 클래스의 인스턴스끼리는 순서를 정할 수 있다고 선언하는 믹스인 인터페이스다. 추상 클래스는 그런 게 없다. 기존 클래스에 맘대로 mixed in 할 수 없다는 그런 의미!
✨ 계층구조가 아님을 인식 → 인터페이스를 적용하자
타입을 계층적으로 정의하면 구조를 잘 표현할 수 있지만, 계층을 엄격히 구분하기 어려운 개념이 많다.
가수와 작곡가를 생각해보았을 때, 둘의 계층구조는 존재하지 않는다. 그래서 추상클래스 말고 인터페이스를 사용한다면 다음 방법이 있을 것이다.
- interface Singer, interface Sonwriter를 두고 이 둘을 모두 구현하는 클래스 두기
- interface SingerSongwriter extends Singer, Songwriter { ... } 와 같이 두 인터페이스를 다중확장하는 제3의 인터페이스 두기
└> 요런 구조를 클래스로 만들려면, 가능한 경우의 조합 전부를 각각의 클래스로 정의해야 한다. 그래서 고도비만 계층구조가 만들어질 것이다. 이를 조합 폭발(combinatorial explosion) 또는 클래스 폭발(class explosion)이라 부른다. 그런 거대한 클래스 계층구조에는 공통기능을 정의해놓은 타입 같은 건 없는 것이다. 이런 과한 결합도를 낮추려면 합성 관계를 사용할 수도 있다.
✨ 또 다시 래퍼클래스를 생각해보자
기억해보자. 래퍼클래스는 '재사용 가능한 전달클래스'를 상속함으로써, 인스턴스 생성시 원하는 구성요소의 타입을 파라미터를 통해 편하게 넣어주는 등 유연하게 생성해낼 수 있다. (Item18)
상속으로 기능을 확장하지 말고,
인터페이스를 사용한 래퍼 클래스로 기능을 추가하도록 하자.
굳이 상속을 해서 불필요한 기능을 확장해버릴 필요가 없고, 활용도가 떨어지니까!
✨ AbstractXXX .... 골격 구현 클래스?
인터페이스와 '추상 골격 구현 클래스'를 함께 제공하는 식으로, 인터페이스 및 추상클래스 장점을 모두 취할 수 있다. 인터페이스로는 타입을 정의 및 디폴트 메서드를 구현하고, 골격 구현 클래스를 통해 나머지 메서드를 구현한다.
이렇게 해두면 장점이, 단순히 골격구현을 확장하는 것만으로 이 인터페이스를 구현하는 것이 된다.
이것을 템플릿 메서드 패턴이라고도 한다.
인터페이스 이름이 Interface면, 골격구현클래스의 이름은 관례상 AbstractInterface로 짓는다. 예를 들어 자바의 컬렉션 프레임워크의 AbstractCollection, AbstractSet, AbstractList, AbstractMap은 각 컬렉션 인터페이스의 골격구현이다.
이렇게 골격구현을 두면, 인터페이스를 확장하려는 프로그래머의 일을 덜어준다.
다음의 정적 팩터리 메서드를 보자.
- 골격 구현을 사용해 잘 완성한 구체 클래스이다.
- 추상클래스처럼 구현을 도와주는 동시에, 추상클래스의 심각한 제약인 타입정의 제약에서는 자유롭다.
- 골격구현을 확장하는 것으로 인터페이스의 구현이 거의 끝난다.
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;
}
};
}
만약 위와 같이 골격구현을 직접 확장하지 못하는 상황이더라도 괜찮다. 인터페이스를 직접 구현하면 되는 거고, 인터페이스도 디폴트메서드를 제공하기 때문에 그 이점을 누리면 된다.
✨ 래퍼클래스 ~ 골격구현클래스
골격 구현 클래스를 우회적으로 사용할 수도 있다. 래퍼클래스(Item18) 와 비슷한 방식이다. '시뮬레이트한 다중 상속(simulated multiple inheritance)'라고 하는데, 다중 상속의 많은 장점을 제공함과 동시에 단점은 피한다.
골격 구현 작성법은 쉽다.
1) 인터페이스를 잘 살펴서 기반 메서드들을 선정한다. 골격 구현의 추상메서드가 될 것이다.
2) 기반 메서드들을 사용해 직접 구현할 수 있는 메서드를 모두 디폴트 메서드로 제공한다.
*but 항상 유념: equals와 hashCode같은 Object의 메서드는 디폴트 메서드로 제공하면 안 된다.
만약 인터페이스의 메서드 모두가 기반메서드, 디폴트메서드가 된다면 골격 구현 클래스를 별도로 만들 이유는 없다.
반면 기반 메서드나 디폴트 메서드로 만들지 못한 메서드가 남아 있다면, 이 인터페이스를 구현하는 골격 구현 클래스를 하나 만들어 남은 메서드들을 작성해 넣는다.
간단한 예로 Map.Entry 인터페이스를 살펴볼 수 있다. getKey(), getValue()는 확실히 기반 메서드이며, 선택적으로 setValue()도 포함할 수 있다. 이 인터페이스는 equals()와 hashCode()의 동작 방식도 정의해놨다. *Object 메서드들은 디폴트 메서드로 제공해서는 안 되므로, 해당 메서드들은 모두 골격 구현 클래스에 구현한다!
// Map.Entry 인터페이스에서 기반메서드를 생각해보자.
// 골격 구현을 두기 위해서 먼저 Map.Entry 인터페이스를 관찰해보자는 것이다.
public abstract class AbstractMapEntry<K, V> implements Map.Entry<K, V> {
// 변경 가능한 엔트리는 이 메서드를 반드시 재정의해야 한다.
@Override
public V setValue(V value) {
throw new UnsupportedOperationException();
}
// Map.Entry.equals의 일반 규약을 구현한다.
@Override
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instance of Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry) o;
return Objects.equals(e.getKey(), getKey());
&& Objects.equals(e.getValue(), getValue());
}
// Map.Entry.hashCode의 일반 규약을 구현한다.
@Overried
public int hashCode() {
return Object.hashCode(getKey())
^ Objects.hashCode(getValue());
}
@Override public String toString() {
return getKey() + "=" + getValue();
}
}
!! Map.Entry 인터페이스나 그 하위 인터페이스로는 이 골격 구현을 제공할 수 없다. 디폴트 메서드는 equals, hashCode, toString같은 Object 메서드를 재정의할 수 없기 때문이다.
.
.
+ 단순 구현(simple implementation)은 골격 구현의 작은 변종이다. AbstractMap.SimpleEntry가 좋은 예다. 단순 구현도 골격 구현처럼 '상속을 위해 인터페이스를 구현한 것'이지만, 추상 클래스가 아니란 점이 다르며, 동작하는 가장 단순한 구현이다. 단순하게 그대로 써도 되고, 필요에 따라 확장해도 된다.
🪄 핵심 정리
일반적으로 다중 구현용 타입으로는 인터페이스가 가장 적합하다. 복잡한 인터페이스라면 구현하는 수고를 덜어주는 '골격 구현'을 함께 제공하는 것을 고려해보자. 골격 구현은 '가능한 한' 인터페이스의 디폴트 메서드로 제공하여, 그 인터페이스를 구현한 모든 곳에서 활용하도록 하는 것이 좋다. (인터페이스에 걸려 있는 구현상 제약 때문에 골격 주현을 추상 클래스로 제공하는 경우가 더 흔함)
'JAVA > Effective Java' 카테고리의 다른 글
상시 업데이트함 -- Effective java 스터디 (1) | 2024.04.16 |
---|---|
[이펙티브 자바] 인터페이스는 타입을 정의하는 용도로만 사용하라 ─ 4장:클래스와 인터페이스:Item22 (0) | 2023.12.10 |
[이펙티브 자바] 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라 ─ 4장:클래스와 인터페이스:Item19 (1) | 2023.10.31 |
[이펙티브 자바] 상속보다는 컴포지션을 사용하라 ─ 4장:클래스와 인터페이스:Item18 (1) | 2023.10.30 |
[이펙티브 자바] 변경 가능성을 최소화하라 ─ 4장:클래스와 인터페이스:Item17 (0) | 2023.10.25 |