Post

상속 vs 컴포지션 구분 - delegation, decorator, wrapper

Effective Java : 아이템 18. (기능 확장이 필요할 때)상속보다는 컴포지션을 사용하라

[Effective Java] 4장 클래스와 인터페이스

둘 다 어떤 클래스에 기능을 추가하거나, 책임을 더해서 확장하고 싶을 때 사용 할 수 있으나, 근본적인 의미가 다르다.
무조건 컴포지션을 써야 한다는 의미가 아니라, 의미에 맞게 사용해야 한다. (하단 ‘상속과 컴포지션을 구분하는 방법’ 참고)

상속의 단점?

상속과 컴포지션의 차이는 즉, @Override 하느냐 delegate 하느냐의 차이로 볼 수 있다.

1. 우선 상속은 한 번 밖에 쓸 수 없는 카드다. 사람 객체의 동작을 ‘나이대’와 ‘소득 수준’에 따라 분리하고 싶어도, 딱 한가지 기준 밖에 쓸 수가 없다.

2. @Override를 통해 하위 클래스에서 변경한 메서드는, 상위 클래스 메서드에도 영향을 미칠 수 있기 때문에 상속이 캡슐화를 깨뜨릴 수 있다. (중요)

반면 컴포지션을 통해 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.