본문 바로가기

IT Book Summary/Object: 객체지향설계

Chapter 11 합성과 유연한 설계

 

 

상속 합성

부모클래스와 자식클래스 사이의 의존성은

컴파일 타임에 해결

두 객체 사이의 의존성은 런타임에 해결
 is-a 관계  has-a 관계
부모클래스의 구현에 의존 결합도가 높음.

구현에 의존하지 않음.

내부에 포함되는 객체의 구현이

아닌 퍼블릭 인터페이스에 의존.

클래스 사이의 정적인 관계 객체사이의 동적인 관계
부모클래스 안에 구현된 코드 자체를 재사용 포함되는 객체의 퍼블릭 인터페이스를 재사용

 


01 상속을 합성으로 변경하기

 

 

상속을 남용했을때 직면하는 3가지 문제점

  • 불필요한 인터페이스 상속문제
  • 메서드 오버라이딩의 오작용
  • 부모클래스와 자식클래스의 동시 수정 문제

 

 

불필요한 인터페이스 상속문제 java.util.Properties와 java.util.Stack

 

합성관계로 변경한 후 Properties의 클라이언트는 모든 타입의 키와 값을 저장할 수 있는 Hashtable의 오퍼레이션을 사용할 수 없기 때문에 String타입의 키와 값만 허용하는 Properties의 규칙을 어길 위험이 사라짐.

 

public class Properties {
    private Hashtable<String, String> properties = new Hashtable<>();
    
    public String setProperty(String key, String value) {
        return properties.put(key,value);
    }
    
    public String getProperty(String key) {
        return properties.get(key);
    }
}

 

 

Vector 를 상속받는 Stack은

Vector의 인스턴스 변수를 Stack 클래스의 인스턴스 변수로 선언해서 합성관계로 변경.

 

public class Stack<E> {
    private Vector<E> elements = new Vector<>();
    
    public E push(E item) {
        elements.addElement(item);
        return item;
    }
    
    public E pop() {
        if(elements.isEmpty()) {
            throw new EmptyStackException();
        }
        return elements.remove(elements.size() - 1 );
    }
}

 

마지막 위치에서만 요소를 추가하거나 삭제할 수 있다는 Stack의 규칙을 지킴.

 

 

메서드 오버라이딩의 오작용 : InstrumentedHashSet

 

HashSet 인스턴스를 내부에 포함한 후 HashSet의 퍼블릭 인터페이스에서 제공하는 오퍼레이션들을 이용해 필요한 기능을 구현하자

 

 

public class InstrumentedHashSet<E> {
    private int addCount = 0;
    private Set<E> set;
    
    public InstrumentedHashSet(Set<E> set) {
        this.set = set;
    }
    
    public boolean add(E e) {
        add.Count++;
        return set.add(e);
    }
    
    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return set.addAll(c);
    }
    
    public int getAddCount() {
        return addCount;
    }
}

 

HashSet 에 대한 구현 결합도는 제거하면서도 퍼블릭 인터페이스는 그대로 상속받을 수 있는 방법-> 인터페이스 사용

 

인터페이스를 실체화 하면서 내부에 HashSet 의 인스턴스를 합성

구현결합도는 제거하면서 퍼블릭 인터페이스 유지.

 

 

public class InstrumentedHashSet<E> implements Set<E> {
    private int addCount = 0;
    private Set<E> set;
    
    public InstrumentedHashSet(Set<E> set) {
        this.set = set;
    }
    
    @Override
    public boolean add(E e) {
        addCount++;
        return set.addAll(c);
    }
    
    public int get addCount() {
        return addCount;
    }
    
    @Override
    public boolean remove(Object o) { return set.remove(o); }
    
    ...
    
}

 

Set의 오퍼레이션을 오버라이딩한 인스턴스 메서드에서 내부의 HashSet 인스턴스에게 동일한 메서드 호출을 그대로 전달함.

-> 포워딩 (forwarding)

동일한 메서드를 호출하기 위해 추가된 메서드 -> 포워딩 메서드(forwarding method)

 

기존 클래스의 인터페이스를 그대로 외부에 제공하면서 구현에 대한 결합 없이 일부 작동 방식을 변경하고 싶은경우 사용.

 

 

부모클래스와 자식클래스의 동시 수정 문제: PersonalPlaylist

 

 

public class PersonalPlaylist {
    private Playlist playlist = new Playlist();
    
    public void append(Song song) {
        playlist.append(song);
    }
    
    public void remove(Song song) {
    playlist.getTracks().remove(song);
    playlist.getSingers().remove(song.getSinger());
    }
}

 

 

합성으로 변경해도 결합도에 의한 수정은 해결되지않음.

 

대부분의 경우 구현에 대한 결합보다는 인터페이스에 대한 결합이 더 좋다.

 

몽키패치(Monkey Patch)

-> 현재 실행중인 환경에만 영향을 미치도록 지역적으로 코드를 수정하거나 확장하는 것.

 

루비의 열린 클래스(Open Class)

C#의 확장 메서드(Extension Method)

스칼라의 암시적 변환(implicit conversion)

 

자바는 바이트 코드를 직접 변환하거나 AOP(Aspect-Oriented Programming)을 통해 구현

 

 


02 상속으로 인한 조합의 폭발적인 증가

 

 

상속으로 인해 결합도가 높아지는 경우 생기는 문제점

  • 하나의 기능을 추가하거나 수정하기 위해 불필요하게 많은 수의 클래스를 추가하거나 수정해야 함.
  • 단일 상속만 지원하는 언어에서는 상속으로 인해 오히려 중복 코드의 양이 늘어남.

 

기본 정책과 부가 정책 조합하기

 

기본정책 + 부가정책 => 핸드폰 요금제

 

통화량을 기반으로 한 기본 정책 (일반요금제, 심야할인 요금제)

 

선택적으로 추가할 수 있는 부가 정책 (세금정책,기본요금 할인정책)

 

  • 기본정책의 계산결과에 적용됨(계산이 끝난 기본 정책 요금에 세금을 부과)
  • 선택적으로 적용 (적용할수도 적용안할수도)
  • 조합가능 (세금정책과 기본요금 할인 정책을 함께 적용하는것도 가능)
  • 부가정책은 임의의 순서로 적용 가능 (무엇은 먼저 적용하든 상관 x)

 

상속을 이용해서 기본 정책 구현하기

 

Phone 추상 클래스의 기존의 상속 계층 이용

자식계층으로 RugularPhone 일반요금제 , NightlyDiscountPhone 심야할인요금제

10장 참고

 

기본 정책에 세금 정책 조합하기

 

TaxableRegularPhone 클래스는 부모클래스의 calculateFee 메서드를 오버라이팅한 후 super 호출을 통해 부모클래스에세 메시지 전송.

 

반환된 일반요금제로 계산된 요금에 세금을 부과해서 계산가능.

 

But 부모클래스와 결합도가 높아짐.

 

부모클래스의 메서드를 직접 호출하지 않도록 추상클래스 추상메서드를 도입하자

->필요한 동작의 명세를 기술하는 추상화에 의존

 

afterCalculated 추상메서드를 Phone 추상클래스에 추가해서 전체 요금 계산후에 수행할 로직 추가.

 

유연성을 유지하면서 중복 코드를 제거하는 방법은 afterCalculated 메서드에 대한 기본 구현을 제공하는 것.

-> 오버라이딩 할 필요 x

 

 

public abstract class Phone {
    ...
    protected Money afterCalculated(Money fee) {
        return fee;
    }
    
    protected abstract Money calculateCallFee(Call call);
}

 

추상메서드의 단점 - 모든 자식 클래스가 추상 메서드를 오버라이딩 해야 한다는 것. 상속 전반에 중복 코드 발생.

-> 일부 메서드에 기본구현을 제공하자.

 

Hook method 훅 메서드

추상 메서드와 동일하게 자식 클래스에서 오버라이딩 할 의도로 메서드를 추가 햇지만 편의를 위해 기본 구현을 제공하는 메서드

 

 

 

객체지향 언어는 단일 상속만 지원하기 때문에 상속으로 인해 발생하는 중복 코드 문제를 해결하기 쉽지 않다.

 

 

기본 정책에 기본 요금 할인 정책 조합하기

 

두번째 부가 정책인 기본 요금 할인 정책을 Phone의 상속 계층에 추가해보자.

 

일반요금제와 기본요금 할인 정책을 조합

- RegularPhone 을 상속받는 RateDiscountableRecularPhone 클래스를 추가

 

심야할인 요금제와 기본요금 할인정책을 조합

- NightlyDiscountPhone 을 상속받는 RateDiscountableNightlyDiscountPhone 클래스 추가 

 

But 중복코드가 있다.

 

중복 코드의 덫에 걸리다

 

상속을 이용한 해결방법은 모든 가능한 조합별로 자식클래스를 추가하는 것.

 

새로운 정책을 추가하기 위해서는 불필요한 많은 수의 클래스를 상속 계층안에 추가해야한다.

 

모든 조합의 경우의 수 만큼의 클래스 추가...

 

상속의 남용으로 인한 클래스 폭발 (class explosion) 조합의 폭발(combination explosion)

 

 

 


03 합성 관계로 변경하기

 

상속관계는 컴파일 타임에 결정되고 고정됨.

-> 합성은 컴파일 타임 관계를 런타임 관계로 변경함. 

실행시점에 정책들의 관계를 유연하게 변경, 실행시점에 인스턴스를 조립하는 방법

 

단순한 설계가 좋지만 유연성을 위한 변경에 따른 복잡성을 감수해야 함.

 

때로는 아이러니하게 편리한 설계를 위한 복잡성을 더했는데도 원래의 설계보다 단순해지기도 한다.

 

기본정책 합성하기

 

각 정책을 별도의 클래스로 구현하자.

 

-> 불리된 정책들을 연결할 수 있도록 합성 관계를 이용해서 구조를 개선.

 

 

public interface RatePolicy {
    Money calculateFee(Phone phone);
}

// 중복코드를 담을 추상 클래스
public abstract class BasicRatePolicy implements RatePolicy {
    @Override
    public Money calculatefee(Phone phone) {
        Money result = Money.SERO;
        
        for(Call call: phone.getCall()){
            result.plus(calculateCallFee(call));
        }
        return result;
    }
    
    protected abstract Money calculateCallFee(Call call);
}

//일반요금제 구현. 자식클래스 추가
public class RecularPolicy extends BasicRatePolicy {
    private Money amount;
    private Duration seconds;
    
    public RecularPolicy(Money amount, Duration seconds) {
        this.amount = amount;
        this.seconds = seconds;
    }
    
    @Override
    protected Money calculateCallFee(Call call) {
        return amount.times(call.getDuration().getSeconds() / seconds.getSeconds());
    }
}

//심야 할인 요금제 구현
public class NightlyDiscountPolicy extends BasicRatePolicy {
    private static final int LATE_NIGHT_HOUR = 22;
    
    private Money nightlyAmount;
    private Money regularAnount;
    private Duration seconds;
    
    public NightlyDiscountPolicy(Money nightlyAmount,Money regularAnount,Duration seconds) {
        this.nightlyAmount = nightlyAmount;
        this.regularAnount = regularAnount;
        this.seconds = seconds;
    }
    
    @Override
    protected Money calculateCallFee(Call call) {
        if(call.getFrom().getHour() >= LATE_NIGHT_HOUR) {
            return nightlyAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
        }
        
       return regularAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
    }
}
// 기본정책을 이용해 요금을 계산할수 있도록
public class Phone {
    private RatePolicy ratePolicy;
    private List<Call> calls = new ArrayList<>();
    
    public Phone(RatePolicy ratePolicy){
        this.ratePolicy = ratePolicy;
    }
    
    public List<Call> getCalls() {
        return Collections.unmodifiableList(calls);
    {
    
    public Money calculateFee() {
        return ratePolicy.calculateFee(this);
    }
}

 

Phone 클래스 안에 RatePolicy 참조자 포함되어 합성.

다양한 종류의 객체와 협력하기 위해 합성관계를 사용. 

의존성 주입을 사용해 런타임에 필요한 객체를 설정하는 것이 일반적임.

 

 

Phone -> RatePolicy

                        \

              BasicRatePolicy

                 |           \

RegularPolicy        NightlyDiscountPolicy

 

일반요금제의 경우 합성

Phone phone = new Phone(new RegularPolicy(Money.wons(10), Duration.ofSeconds(10)));

 

 

심야할인 요금제의 경우 합성

Phone phone = new Phone(new NightlyDiscountPolicy(Money.wons(5), Money.wons(10) , Duration.ofSeconds(10)));

 

 

-> 이제 필요에 따라 적절한 인스턴스를 생성하기만 하면 된다.

 

 

부가 정책 적용하기

 

두가지 제약에 따라 부가정책 구현

  • 부가정책은 기본정책이나 다른 부가정책의 인스턴스를 참조할 수 있어야 함 : 부가정책의 인스턴스는 어떤 종류의 정책과도 합성될 수 있어댜 함.
  • 기본정책과 부가정책은 협력 안에서 동일한 '역할'을 수행 : 부가정책이 기본정책과 동일한 RatePolicy 인터페이스를 구현해야 함.

-> 부가정책은 RatePolicy 인터페이스를 구현해야 함. 내부에 또 다른 RatePolicy 인스턴스를 합성할 수 있어야 한다.

 

그림 11.11 (기본정책과 부가정책을 조합할 수 있는 상속구조) 참고

 

기본정책과 부가 정책 합성하기

 

이제 원하는 정책의 인스텀스를 생성한 후 의존성주입을 통해 다른 정책의 인스턴스에 전달하기만 하면 된다.

 

일반 요금제에 기본요금 할인정책을 조합한 결과에 세금정책을 조합하고 싶다면?

Phone phone = new Phone( new TaxablePolicy(0.05,

                                                new RateDiscountablePolicy(Money.wons(1000),

                                                                                                  newRegularPolicy(...))));

 

 

새로운 정책 추가하기

 

합성을 기반으로 한 설계에서 새로운 정책을 추가하려면 새로운 정책을 구현한 클래스 하나만 추가한 후 원하는 방식으로 조합하면 된다.

 

요구사항이 변경될 때도 하나의 클래스만 수정하면 됨

 

 

객체 합성이 클래스 상속보다 더 좋은 방법이다

 

상속의 구현 재사용하는 방법보다 합성의 객체 인터페이스 재사용이 더 좋은 방법

 

상속을 사용해야하는 경우는?

구현상속과 인터페이스상속 두 종류 중 상속의 단점은 모두 구현상속에 관한것.

구현상속을 피하고 인터페이스 상속을 사용해야 함.

 


04 믹스인

 

 

구체적인 코드를 재사용하면서도 낮은 결합도를 유지할 수 있는 유일한 방법은

재사용에 적합한 추상화를 도입하는것

 

믹스인 mixin 

- 객체를 생성할 때 코드 일부를 클래스 안에 섞어 넣어 재사용하는 기법을 가리키는 용어

- Flavors 언어에서 처음 도입. CLOS(Common Lisp Object System)에 의해 대중화

 

상속의 진정한 목적은 자식 클래스를 부모 클래스와 동일한 개념적인 범주로 묶어 is-a 관계를 만들기 위한 것.

믹스인은 코드를 다른 코드안에 섞어 넣기위한 방법.

 

상속은 클래스와 클래스 사이의 관계를 고정시키는데 반해 

믹스인은 합성처럼 유연하게 관계를 재구성

 

 

기본 정책 구현하기

 

스칼라에서 제공하는 트레이트(trait)를 이용해 믹스인을 구현해보자

트레이트는 믹스인의 기본철학을 가장 유사한 형태로 재현.

 

기본정책을 추상클래스로.

표준 요금제를 구현하는 정책은 기본정책을 상속받아 개별 Call의 요금을 계산하는 calculateCallFee메서드를 오버라이딩.

심야할인정책도 같은 방식으로.

 

abstract class BasicRatePolicy {
    def calculateFee(phone : Phone): Money =
        phone.calls.map(calculateCallFee(_)).reduce(_+_)
        
    protected def calculateCallFee(call: Call): Money;
}

class RegularPolicy(val amount: Money, val seconds: Duration) extends BasicRatePolicy {
    override protected def calculateCallFee(call: Call): Money = 
        amount*(call.duration.getSeconds / seconds.getSeconds)
}

class NightlyDiscountPolicy (
    val nightlyAmount: Money,
    val regularAmount: Money,
    val seconds: Duration) extends BasicRatePolicy {
    
    override protected def calculateCallFee(call: Call): Money = 
    if(call.from.getHour >= NightlyDiscountPolicy.LateNightHour){
        nightlyAmount*(call.duration.getSeconds / seconds.gerSeconds)
    }else{
        regularAmount*(call.duration.getSeconds / seconds.getSeconds)
    }
}

object NightlyDiscountPolicy{
    val LateNightHour: Integer = 22
}

 

 

트레이트로 부가 정책 구현하기

 

기본정책에 조합하려는 코드는 부가 정책(세금정책)을 구현하는 코드들

 

trait TaxablePolicy extends BasicRatePolicy {
   def texRate: Double
   
   override def calculateFee(phone: Phone): Money = {
       val fee = super.calculateFee(phone)
       return fee + fee * taxRate
   }
}

 

위 코드에서 extends 는 단지 TaxablePolicy가 사용될 수 있는 문맥을 제한할 뿐

TaxablePolicy는 BasicRatePolicy를 상속받은 경우에만 믹스인 될 수 있다.

super 호출로 실행되는 calculateFee 메서드를 보관한 코드는 실제로 트레이트가 믹스인 되는 시점에 결정

상속의 경우 this 참조는 동적으로 결정되지만 super 참조는 컴파일 시점에 결정

하지만 트레이트에서는 this 호출과 super 호출 역시 실행시점에 바인딩

 

trait RateDiacountablePolicy extends BasicRatePolicy {
    val discountAmount: Money
    
    override def calculateFee(phone: Phone): Money = {
        val fee = super.calculate(phone)
        fee - discountAmount
    }
}

 

부가정책 트레이트 믹스인 하기

 

 

스칼라는 트레이트를 클래스나 다른 트레이트에 믹스인 할 수 있도록 extends,  with 키워드 제공

믹스인 하려는 대상 클래스의 부모 클래스가 존재하는 경우 부모클래스는 extends 를 이용해 상속받고

트레이트는 with 를 이용해 믹스인

-> 트레이트 조합(trait composition)

 

class TaxableRegularPolicy {
    amount: Money,
    seconds: Duration,
    val taxRate: Double)
    extends RegularPolicy(amount, seconds)
    with TaxablePolicy
}

 

믹스인되기 전까지 상속계층 안에서 TaxablePolicy 트레이트의 위치가 결정되지 않음.

어떤 클래스에 믹스인 할지에 따라 트레이트의 위치는 동적으로 변경

 

믹스인에서는 클래스가 늘어나더라도 중복코드 문제가 발생하지 않음.

 

 

쌓을 수 있는 변경

 

믹스인은 상속 계층 안에서 확장한 클래스보다 하위에 위치

대상클래스의 자식클래스처럼 사용될 용도로 만들어지는 것

-> aka 추상 서브클래스 abstract subclass

 

특정한 클래스에 대한 변경 또는 확장을 독립적으로 구현한 후 필요한 시점에 차례대로 추가 가능.

-> 쌓을수 있는 변경 stackable modification