상속 vs 컴포지션 구분 - delegation, decorator, wrapper
Effective Java : 아이템 18. (기능 확장이 필요할 때)상속보다는 컴포지션을 사용하라
[Effective Java] 4장 클래스와 인터페이스
- 상속이란?
- extends를 말함. (implements는 아님. 이건 구현.)
- 컴포지션이란?
- Composition은 필요한 객체를 내부 private 변수로 두는 것
- 기존 클래스가 새로운 클래스의 구성요소로 쓰인다는 뜻
- [Coding/CodingNote] - [코딩 노트] 객체 지향 패러다임
[!info] 둘 다 어떤 클래스에 기능을 추가하거나, 책임을 더해서 확장하고 싶을 때 사용 할 수 있으나, 근본적인 의미가 다르다.
무조건 컴포지션을 써야 한다는 의미가 아니라, 의미에 맞게 사용해야 한다. (하단 ‘상속과 컴포지션을 구분하는 방법’ 참고)
상속의 단점?
상속과 컴포지션의 차이는 즉, @Override 하느냐 delegate 하느냐의 차이로 볼 수 있다.
1. 우선 상속은 한 번 밖에 쓸 수 없는 카드다. 사람 객체의 동작을 ‘나이대’와 ‘소득 수준’에 따라 분리하고 싶어도, 딱 한가지 기준 밖에 쓸 수가 없다.
2. @Override를 통해 하위 클래스에서 변경한 메서드는, 상위 클래스 메서드에도 영향을 미칠 수 있기 때문에 상속이 캡슐화를 깨뜨릴 수 있다. (중요)
- addAll - super.addAll - add를 호출하는데, Override한 add가 호출되기 때문에 addCount증가가 addAll에서도, add에서도 호출되어 예상한 대로 동작하지 않는다.
- 상위 클래스에서 메서드를 추가할 때 하위 클래스에 추가한 메서드와 메서드 시그니처가 같다면 충돌이 발생할 수도 있다.
- 상위 클래스의 API(public)에 결함이 있는 경우, 컴포지션으로는 이를 숨길 수 있지만 상속으로는 숨길 수 없다.
- Override를 통해 동작을 변경하는 것은 가능하지만.
- 접근 지정자를 더 private한 수준으로 변경하는 것이 불가능하다. (public 멤버를 상속하면 이를 숨길 방법이 없다.)
- 메서드 이름을 변경하는 것이 불가능하다.
- 그래서 #아이템 19 에서 상속을 금지하라고 가이드 하고 있는 것.
- 보통 상속은 더 구현하기 까다롭고 복잡한 경우가 많다.
반면 컴포지션을 통해 delegate하면 Composition 대상 클래스의 내부 구현을 수정 할 가능성이 없기 때문에 캡슐화를 깨뜨리지 않는다.
상속 구조에서 문제가 발생하는 경우, 흔히들 서브클래스를 Delegation 구조로 바꾸게 된다.
상속과 컴포지션을 구분하는 방법?
- 상속은 진짜 “B is A” 관계가 성립할 때만 써야 한다.
- 상속은 단순 기능 추가 보다는 좀 더 의미론적으로 접근하는게 맞는 것 같다.
- 상속 계층을 따져보면 상속을 받을 수 밖에 없는 경우?
- 타입 문제 때문에 하위 클래스로 만들어 다형성을 이용해야 하는 경우는… interface를 이용한 컴포지션으로 커버 가능하다.
- ForwardingSet과 InstrumentedSet의 예제 (하단 참조)
- 굳이 class를 extends할 필요 없다는 것. 목적이 다형성이면 interface implements로 충분하다.
- 자바 플랫폼 라이브러리에도 잘못된 예들이 있는데
- Stack은 Vector가 아니므로, 상속 보다는 컴포지션을 사용했다면 좋았을 것이고
- Properties도 Hashtable이 아니므로, 상속이 아니라 컴포지션이 더 좋았을 것임
위임(Delegation) 이란?
- 세부 구현을 타 클래스에게 맡기는(위임하는) 것 (=내 관심사가 아닌 것들은 타 모듈에 위임하는 것)
- Composition + Forwarding을 위임(Delegation) 이라 한다.
- 예를 들어,
- 1. 새 클래스 및 메서드 하나 만들고
- 2.Composition으로 필요한 클래스 가져오고
- 3. 새 클래스의 메서드 바디에서, Composition 클래스 메서드 중 기능에 대응하는 메서드를 호출하도록 Forwarding 하면 위임이다.
- 엄밀히 따지면 외부 클래스가 내부 객체에 자기 자신의 참조를 넘기는 경우만 위임으로 보는 시각도 있다.
- OrderBookMap 클래스가 내부의 Updater 객체에 자기 자신의 참조를 넘겨 update 하도록 하는 경우
- 예를 들어,
- 상속도 super()만 호출하면 위임 비슷한거 아니냐..라고 생각할 수 있지만,Delegation의 정의 자체가 Composition을 전제로 하고 있다. 사실상 delegation과 composition은 거의 같은 의미로 사용된다. (상속의 대안으로 자주 거론된다)
기타
- https://en.wikipedia.org/wiki/Delegation_pattern
- 여기서는 Pattern이라고 부르고 있으나, 그 자체로 패턴이라기 보다는 방법(?)이라고 보는게 더 맞는 것 같음. 정의도 그렇고.
- Delegation을 사용해서 Decorator Pattern을 구현하게 되므로…
- 보통 얘기할 때도 얘한테 위임한다. 라고 얘기하지 Delegation Pattern을 사용한다. 라고 얘기하지는 않는 것도 있고
- 코틀린은 언어 차원에서 Delegation 연산자를 지원한다.
Decorator Pattern (Wrapper Class)
https://sourcemaking.com/design_patterns/decorator
1
2
Set<Instant> times = new InstrumentedSet<>(new TreeSet<>(cmp));
Set<E> s = new InstrumentedSet<>(new HashSet<>(INIT_CAPACITY));
- 어떤 클래스를 Wrapper 클래스로 감싸는 패턴을, 기능을 덧씌운다는 의미에서 Decorator Pattern이라고 부른다.
- Wrapper 클래스는(또는 Forwarding 클래스는) delegation을 사용할 수 밖에 없다. 원본 클래스를 wrapping 하는게 목적이므로, 원본 클래스 메서드를 호출해야 의미가 있는 경우가 대부분이니까.
- 예시) Wrapper 클래스 (상속 대신 컴포지션) / 재사용할 수 있는 Forwarding 클래스
- InstrumentedSet 안에 private final로 Set을 가져도 되는데 중간에 ForwardingSet을 한 번 거치는 이유는? 재활용성 때문
- 만약 ForwardingSet이 없었다면,
- InstrumentedSet을 만들 때 clear, isEmpty 같은 Forwarding Method를 각각 다 만들어줘야 하고,
- anotherWrapperSet을 만들 때 또 clear, isEmpty 등을 각각 다 만들어줘야 하니까.
- 그래서 by 같은 키워드 지원이 된다면 굳이 필요 없지 않을까 하는 생각이 든다.
- 또는, 굳이 재활용이 필요하지 않다면 Forwarding 클래스를 건너 뛰어도 될 듯.
- 래퍼 클래스의 유일한 단점은 콜백에 넘길 때 래퍼가 아닌 내부 객체를 호출할 가능성이 있다는 점
기타 - 상속 컴포지션 예시
1
2
3
interface MailSender
class MailSenderWithoutSave(MailClient): MailSender
class MailSenderWithSave(MailSenderWithoutSave, DB): MailSender
같은 interface를 구현하고 있기 때문에 상속처럼 다형성을 사용 할 수 있다.
상속에 비해 특별한 단점이 없다.
This post is licensed under CC BY 4.0 by the author.