티스토리 뷰
(Effective Java) 규칙18. 상속보다는 컴포지션을 사용하라
계승(Inheritance)
-
계승(Inheritance)은 재사용을 돕는 강력한 도구지만, 항상 최선이라고 할 순 없다.
- 계승을 적절히 사용하지 못한 소프트웨어는 깨지기 쉬움.
-
계승은 상위 클래스와 하위 클래스 구현을 같은 프로그래머가 통제하는 단일 패키지 안에 있을 경우 안전하다.
-
일반 객체 생성 클래스(concrete class)라면, 해당 클래스가 속한 패키지 밖에서 계승을 시도하는 것은 위험하다.
- 이 책에서는 계승(Inheritance)이라는 용어를 구현 계승 (implementation Inheritance)의 의미로 사용
- 즉 한 클래스가 다른 클래스를 'extends' 한다는 소리, 인터페이스 끼리의 계승은 해당하지 않음
계승의 문제
-
메서드 호출과 달리, 계승은 캡슐화(encapsulation) 원칙을 위반한다.
- 하위 클래스가 정상 동작하기 위해서는 상위 클래스의 구현에 의존할 수 밖에 없음
- 상위 클래스가 수정됨에 따라 하위 클래스 코드는 영향을 받아 망가질 수 있음
-
하위 클래스 구현을 망가뜨릴 수 있는 요인
- 상위 클래스의 구현을 알지 못하여 생기는 문제
- 예) HashSet을 사용하는 프로그램
- 프로그램의 성능을 높이기 위해서 HashSet 객체가 생성된 이후 얼마나 많은 요소가 추가되었는지 질의해야 한다고 가정
- 계승을 이용해 HashSet에 삽입된 요소의 수를 추적하는 필드와 그 필드에 대한 접근자를 갖는 클래스
//계승을 잘못 사용한 사례! 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; } }
- 이 코드는 제대로 동작하지 않음
InstrumentedHashSet<String> s = new InstrumentedHashSet<>(); s.addAll(Arrays.asList("first", "second", "third"));
- 위 코드를 실행 하면 getAddCount가 3을 반환 할 것이라고 예상이 되지만 실제로 6을 반환함
- 그 이유는 HashSet의 addAll 메서드는 add메서드를 통해 구현이 되어 있기 때문임
- HashSet 문서에는 이런 사실이 명시되어 있지 않음
- 이 클래스가 정상 동작한다는 것은 HashSet의 addAll 메서드가 add 위에서 구현되었다는 사실에 의존함
- 즉, InstrumentedHashSet 클래스는 깨지기 쉬운(fragile) 클래스일 수 밖에 없음
- 예) HashSet을 사용하는 프로그램
- 다음 릴리즈에 상위 클래스에 새로운 메서드가 추가될 수 있는 문제
- 상위 클래스에 컬렉션에 요소를 넣을 수 있는 메서드가 추가되면 하위 클래스에서 재정의하지 않은 새 메서드를 호출하여 "잘못된(illegal)" 객체를 컬렉션에 넣을 수 있게 됨
- 상위 클래스의 구현을 알지 못하여 생기는 문제
-
앞서 살펴본 문제들은 메서드 재정의 때문에 발생한 문제이다.
- 메서드 재정의 대신 새 메서드로 만들어 넣으면 계승이 괜찮을 거라고 생각할 수 있지만, 위험성이 완전히 사라지는 것은 아님
- 예) 새 릴리스에 추가된 상위 클래스 메서가 재수 없게도 하위 클래스에 정의한 메서드와 같은 시그니처인데 반환 값만 다른 경우...
- 메서드 재정의 대신 새 메서드로 만들어 넣으면 계승이 괜찮을 거라고 생각할 수 있지만, 위험성이 완전히 사라지는 것은 아님
계승 대신 구성(Composition)
-
지금껏 봐왔던 문제들을 모두 피할 수 있는 방법
- 기존 클래스를 계승 하는 대신, 새로운 클래스에 기존 클래스 객체를 참조하는 private 필드를 하나 두는 것
- 이런 설계 기법은 구성(composition)이라고 하는데 기존 클래스가 새 클래스의 일부(component)가 되기 때문임
-
새로운 클래스에 포함된 각각의 메서드는 기존 클래스에 있는 메서드 가운데 필요한 것을 호출해서 그 결과를 반환
- 이런 구현 기법은 전달(forwarding)이라고 하고 전달 기법을 사용해 구현된 메서드를 전달 메서드라고 부름
-
구성 기법으로 구현된 클래스는 견고하다.
- 기존 클래스에 또 다른 메서드가 추가되더라도, 새로 만든 클래스에는 영향이 없음
- 예) 앞선 예제의 구성 기법 적용
//계승 대신 구성을 사용하는 포장(wrapper) 클래스 public class InstrumentedSet<E> extends ForawrdingSet<E> { //요소를 삽입하려한 횟수 private int addCount = 0; public InstrumentedSet() { } public InstrumentedSet(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; } } //재사용 가능한 전달(forwarding) 클래스 public class ForwardingSet<E> implements Set<E> { private final Set<E> s; public ForwardingSet(Set<E> s) { this.s = s; } public void clear() { s.clear(); } public boolean contains(Object o) { return s.contains(o); } public boolean isEmpty() { return s.isEmpty(); } public int size() { return s.size(); } public Iterator<E> iterator() { return s.iterator(); } public boolean add(E e) { return s.add(e); } public boolean remove(Object o) { return s.remove(o); } public boolean containsAll(Collection<?> c) { return c.containsAll(c); } .... }
- InsteumentedSet을 이렇게 설계할 수 있는 것은, HashSet이 제공해야 할 기능을 규정하는 Set이라는 인터페이스가 있기 때문
- 이런 설계는 안정적이고 유연성이 아주 높음
- 포장(wrapper class) 기법을 쓰면 어떤 Set 구현도 원하는 대로 수정할 수 있고, 이미 있는 생성자도 그대로 사용 가능
Set<Data> s = new InstrumentedSet<Date>(new TreeSet<Date>(cmp)); Set<E> s2 = new InstrumentedSet<E>(new HashSet<E>(capacity));
- 심지어 이미 사용 중인 객체에 일시적으로 원하는 기능을 넣는데도 활용 가능
static void walk(Set<Dog> dogs) { InstrumentedSet<Dog> iDogs = new InstrumentedSet<Dog>(dogs); ... //이 메서드 안에서는 dogs 대신 iDogs를 사용 } }
- InsteumentedSet과 같은 클래스를 포장 클래스라고 부르는데 다른 Set 객체를 포장하고 있기 때문임
- 이런 구현 기법은 장식자(decorator) 패턴이라고 부름
-
포장 클래스에는 단점이 별로 없지만 역호출(callback) 프레임워크와 함께 사용하기에는 적절치 않다.
- 역호출 프레임워크에서는 객체는 자기 자신에 대한 참조를 다른 객체에 넘겨, 나중에 필요할 때 역호출(callback)하도록 요청함
- 포장된(wrapped) 객체는 포장 객체에 대해서는 모르기 때문에, 자기 자신에 대한 참조(this)를 전달할 것임
- 역호출 과정에서 포장 객체는 제외 되게 됨
-
전달 메서드 호출과 과정에서 성능이 저하되거나, 포장객체 때문에 메모리 요구량이 늘어나지 않을까 걱정하지만 실제로는 큰 영향이 없는 것으로 나타났다.
-
전달 메서드 코딩은 지루한 작업이지만 인터페이스별로 한 번씩만 구현하면 되고 인터페이스와 같은 패키지에 이미 들어있는 경우도 있다.
정리하며
-
계승은 하위 클래스가 상위 클래스의 하위 자료형(subtype)이 확실한 경우에만 바람직하다.
- 클래스 B는 클래스 A와 "IS-A" 관계가 성립할 때만 A를 계승해야함
- 스스로에게 물어본 뒤 "그렇다' 답할 수 없다면 계승하면 안됨
- "아니다" 라고 답했다면, B 안에 A객체를 참조하는 private 필드를 두고 B에는 더 작고 간단한 API를 구현해야 함
- A는 B의 핵심적 부분이 아니며 B의 구현 사항에 불과함
-
자바 플랫폼 라이브러리에는 이 원칙을 분명히 위반하는 사례들이 많다.
- 예) 스택(stack)은 벡터(vector)가 아니므로 Stack은 Vector를 계승하면 안됨 (Properties, Hashtable 역시)
- 두 경우 모두 구성 기법이 더 적합함
- 예) 스택(stack)은 벡터(vector)가 아니므로 Stack은 Vector를 계승하면 안됨 (Properties, Hashtable 역시)
-
구성 기법이 적절한 곳에 계승을 사용하면 구현 세부사항이 쓸데없이 노출된다.
- 그런 API는 원래 구현에서 벗어날 수 없게 되며, 클래스의 성능을 개선하기 어려워짐
- 더 심각하게는 클라이언트가 내부 구현 세부사항에 접근할 수도 있게됨
- 예) Properties의 경우 설계자의 의도는 키와 값으로 문자열을 사용하는 것이었지만, 상위 클래스인 Hashtable에 직접 접근할 수 있으로 이 불변식을 깰 수가 있음
- 불변식이 깨지고 나면 API의 다른 부분 (load, store)은 사용할 수 없게 됨
- 예) Properties의 경우 설계자의 의도는 키와 값으로 문자열을 사용하는 것이었지만, 상위 클래스인 Hashtable에 직접 접근할 수 있으로 이 불변식을 깰 수가 있음
-
구성 대신 계승을 사용하려 할 때 반드시 물어야 하는 질문은 "계승할 클래스의 API에 문제가 있는가?"
- "그렇다면 그 문제들이 계속 새 API의 일부가 되어도 상관없겠는가?"
- 계승은 상위 클래스의 문제를 하위 클래스에 전파시키지만 구성은 그런 약점을 감추는 새로운 API를 설계하도록 도와줌
결론
-
계승은 강력한 도구이지만 캡슐화 원칙을 침해하므로 문제를 발생시킬 소지가 있다.
- 상위 클래스와 하위 클래스 사이에 IS-A 관계가 있을 때만 사용하는 것이 좋음
- 설사 관계가 성립해도 하위 클래스가 상위 클래스와 다른 패키지에 있거나 계승을 고려하지 않은 상위 클래스라면 하위 클래스는 깨지기 쉬움
-
이런 문제를 피하려면 구성과 전달 기법을 사용하는 것이 좋다.
- 포장 클래스 구현에 적당한 인터페이스가 있다면 더더욱
'프로그래밍 > EffectiveJava' 카테고리의 다른 글
(이펙티브 자바) 규칙22. 인터페이스는 타입을 정의하는 용도로만 사용하라 (0) | 2020.02.20 |
---|---|
(이펙티브 자바) 규칙20. 추상 클래스 보다는 인터페이스를 우선하라 (0) | 2020.02.18 |
(이펙티브 자바) 규칙 1. 생성자 대신 정적 팩터리 메서드를 고려하라 (0) | 2020.02.11 |
(이펙티브 자바) 규칙6. 불필요한 객체 생성을 피하라 (0) | 2020.02.10 |
(이펙티브 자바) 규칙4. 인스턴스화를 막으려거든 private 생성자를 사용하라 (0) | 2020.02.06 |
- Total
- Today
- Yesterday
- JavaFX
- JavaFX 종료
- 자바
- 인텔리제이
- 이펙티브 자바
- effectivejava
- springboot
- windows
- 이펙티브자바
- java
- 방통대 과제물
- JavaFX Window Close
- 자전거
- git
- Java UI
- 배낭 여행
- 텐트
- 자전거 여행
- effective java
- 스프링부트
- 일본 여행
- intelij
- 일본여행
- JavaFX 테이블뷰
- 이펙티브
- 배낭여행
- 일본 배낭여행
- 일본 자전거 여행
- JavaFX Table View
- TableView
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |