상속 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 클래스를 건너 뛰어도 될 듯.
- 래퍼 클래스의 유일한 단점은 콜백에 넘길 때 래퍼가 아닌 내부 객체를 호출할 가능성이 있다는 점
[!info] Spring AOP는 프록시 패턴일까? 데코레이터 패턴일까? 프록시는 접근 제어, 데코레이터는 기능 확장에 초점이 맞추어져 있으니 데코레이터라고 생각 할 수도 있지만, 프록시 패턴이다.
실제로 AOP proxy라고 많이 부르게 되는데… AOP 동작 방식 자체가 proxy 객체를 생성해서 요청을 가로채고, advice 로직을 실제 메서드 호출 전후로 수행하기 때문이다.
데코레이터의 기능 확장은, 전역 공통 로직이 아니라 특정한 클래스에 국한된 기능 확장을 의미한다. (상속 대신 컴포지션으로 확장한다는 느낌과 유사)
반면 AOP의 기능 확장은, 전역 공통 로직(로깅, tx, 캐싱)을 대상에 적용하는 것을 의미한다. 이는 대상 객체의 기능 확장이라기 보다는 전역 유틸리티의 횡단 적용에 가깝다.
기타 - 상속 컴포지션 예시
1
2
3
interface MailSender
class MailSenderWithoutSave(MailClient): MailSender
class MailSenderWithSave(MailSenderWithoutSave, DB): MailSender
같은 interface를 구현하고 있기 때문에 상속처럼 다형성을 사용 할 수 있다.
상속에 비해 특별한 단점이 없다.