ITEM 18 "상속보다는 컴포지션을 사용하라"
ITEM 17 과 마찬가지로 OCP(Open Close Priciple) 원칙에 대한 내용이다.
이 ITEM 을 확인하기 전에 위 우아한 테크코스 - OCP 와 전략패턴 영상을 보는 것을 추천한다.
OCP 원칙을 설명하면서 상속은 is-a 관계, 합성은 has-a 관계일 때 사용하여야 한 된다는 점과 템플릿 메서드 패턴과 전략 패턴의 확실한 차이도 확실히 배워갈 수 있다.
우아한 테크코스 영상은 초보 프로그래머가 알아야 되는 내용들을 잘 설명해준다.
이번 장에서는 상속과 비교하면서 합성 연관관계를 권장하고 있다.
상속을 사용하면 상위 클래스와 하위 클래스 개념이 생긴다.
상위 클래스가 버전이 릴리즈 될 때마다 내부 구현이 달라지면 그 여파로 하위 클래스가 오동작할 수 있다.
그래서 상위 클래스가 충분히 확장을 고려하지 않은 상태로 구현되면 캡슐화가 깨질 수 있다.
상속이 잘 되려면...
1 . 상위 클래스와 하위 클래스가 한 프로그래머가 통제하여야 상위 및 하위 클래스가 변경될 시 그 side-effect 를 잘 알고 같이 수정할 수 있다.
2 . 확장할 목적으로 설계되어 있어야 하고 문서화도 잘 되어 있어야 한다.
3 . 상위 클래스와 하위 클래스가 완벽히 IS-A 관계에 있어야 한다.
public class InstrumentedHashSet<E> extends HashSet<E> {
private int addCount = 0;
public InstrumentedHashSet() {}
public InstrumentedHashSet(int initCap, float loadFactor) {
super(initCap, loadFactor);
}
@Override public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
}
위와 같이 HashSet 이 처음 생성된 이후로 아이템이 몇 개가 더해졌는지 알기 위해 상속을 사용해서 addAll 과 add 메서드를 Override 했다고 생각해보자.
InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
s.addAll(List.of("틱", "탁탁", "펑"));
재정의한 addAll 메서드를 호출하면 3을 반환하는 것이 아니라 6을 반환한다.
원인은 addAll 메서드는 add 메서드를 사용해서 구현하기 때문이다. add 함수에도 addCount 변수를 1증가시키고 있으니 6이 반환되는 것이다.
이런 내부 구현 방식은 HashSet 문서에 나와 있지 않다. HashSet 클래스를 만든 프로그래머가 이런 사실을 공유하지 않는다면 잘못된 방식으로 구현할 수 있다는 것을 보여준다. 심지어 HashSet 클래스의 addAll 메서드가 다음 릴리즈 때에도 구조가 유지될 것이라는 보장이 없다.
다시 메서드를 재정의한다면 되지 않을까?...
상위 클래스의 private 변수를 써야하는 상황이라면 아예 불가능하기도 하고, 상위 클래스의 메서드를 다시 재정의 해야 될 수도 있다. 성능, 상속을 한 이유가 사라짐, 오류 발생할 가능성 등 다양한 문제점이 생길 수 있다.
또 메서드를 재정의했는데 상위 클래스에서 추가한 메서드의 이름과 반환 타입이 같다면 컴파일조차 되지 않는다.
합성을 사용하자
새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조한다. 기존 클래스가 새로운 클래스의 구성요소로 쓰인다는 점에서 이러한 설계를 합성(composition)이라고 한다.
그리고 새로운 클래스의 메서드에서 기존 클래스에 대응되는 메서드를 호출하여 그 결과를 받는다.
public class InstrumentedSet<E> extends ForwardingSet<E> {
private int addCount = 0;
public InstrumentedSet(Set<E> s) {
super(s);
}
@Override public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
}
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s) { this.s = s; }
... (생략) ...
public boolean add(E e) { return s.add(e); }
public boolean addAll(Collection<? extends E> c) { return s.addAll(c); }
}
위와 같이 Set 을 클래스 내 변수로 생성해서 Set 의 메서드를 호출하는 전달 메서드를 정의하면 상위 클래스의 메서드 변경에도 유연하게 대처할 수 있다.
"상속은 강력하지만 캡슐화를 해칠 수 있다."
'독후감 > Effective JAVA' 카테고리의 다른 글
[Effective JAVA] 20 "추상 클래스보다는 인터페이스를 우선하라" (0) | 2022.05.11 |
---|---|
[Effective JAVA] 19 "상속을 고려해 설계하고 문서화하라" (0) | 2022.05.10 |
[Effective JAVA] 17 "변경 가능성을 최소화하라" (0) | 2022.05.10 |
[Effective JAVA] 16 "public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라" (0) | 2022.05.10 |
[Effective JAVA] 15 "클래스와 멤버의 접근 권한을 최소화하라" (0) | 2022.05.09 |