[오브젝트] 챕터 11: 합성과 유연한 설계

✏️, 💡,❓ 해당 이모지는 저의 생각임을 나타냅니다.

 

상속과 합성은 객체지향 프로그래밍에서 가장 널리 사용되는 코드 재사용 기법이다.

상속이 부모 클래스와 자식 클래스를 연결해서 부모 클래스의 코드를 재사용하는데, 합성은 전체를 표현하는 객체가 부분을 표현하는 객체를 포함해서 부분 객체의 코드를 재사용한다. 상속은 의존성이 컴파일타임에 해결되고 합성은 런타임에 해결된다. 상속은 is-a 관계이고, 합성은 has-a 관계이다.

 

합성은 포함되는 객체의 퍼블릭 인터페이스를 재사용한다.  따라서 상속 대신 합성을 사용하면 구현에 대한 의존성을 인터페이스에 대한 의존성으로 변경하면서 결합도를 낮춘다.

 

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

상속을 했을 경우 세가지 문제가 발생한다.

1) 불필요한 인터페이스 상속 문제

자식 클래스에게 부적합한 부모 클래스의 오퍼레이션이 상속된다.

2) 메서드 오버라이딩의 오작용 문제

자식 클래스가 메서드를 오버라이딩 할 때 자식 클래스가 부모 클래스의 메서드 호출 방법에 영향을 받는다.

3) 부모 클래스와 자식 클래스의 동시 수정 문제

 

합성은 상속의 문제점을 해결한다.

상속을 합성으로 바꾸려면 자식 클래스에 선언된 상속 관계를 제거하고 부모 클래스의 인스턴스를 자식 클래스의 인스턴스 변수로 선언하면 된다.

public class PernalPlaylist {
    private PlayList playlist = new Playlist();
    
    // 이를 포워딩 메서드라고 한다: 동일한 메서드를 호출하기 위해 추가된 메서드
    public void append(Shong song) {
    	playlist.append(song);
    }
    
    public void remove(Song song) {
    	playlist.getTracks().remove(song);
        playlist.getSingers().remove(song.getSinger());
    }
}

✏️  합성이라는 이름을 붙였지만 그저 변수화한 후 필요한 메서드를 그대로 써주는 것밖에는 안 되는 것 같다 오히려 중복을 유발할 수도 있을 것 같은데...

💡 합성이 생소하게 느껴질 수 있지만 사실 전혀 생소한 게 아니다. 백엔드 개발할때 서비스에서 레포지토리를 의존성 주입 후 사용하는 것, 그것도 합성으로 볼 수 있지 않을까?

 

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

10장의 과금 시스템에 부가 정책을 추가해보자. (코드)

기본 정책은 요금제 관련 정책이고, 부가 정책은 세금 정책과 요금 할인 정책이다. 정책은 선택적 적용이 가능하기 때문에 설계가 다양한 조합을 수용할 수 있도록 해야 한다.

그림 11.2

 

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

그림 11.3

세금 정책을 계산하는 로직을 '계산 이후(afterCalculated)' 함수로 빼고, Phone에서 해당 함수의 기본 로직을 주게 되면 위와같이 세금 정책을 구현할 수 있다. 여기서 문제는 Taxable 클래스들의 구현이 거의 동일하다는 사실이다.

 

중복 코드의 덫에 걸리다

부가 정책은 자유롭게 조합할 수 있어야 하고 적용되는 순서 역시 임의로 결정할 수 있어야 한다.

상속을 이용한 해결 방법은 모든 가능한 조합별로 자식 클래스를 추가하는 것이다. 만약 일반 요금제의 계산 결과에 세금 정책을 조합한 후 기본 요금 할인 정책을 추가하고 싶다면, TaxableRegularPhone을 상속받는 TaxableAndRateDiscountalbeRegularPhone이 나와야 한다. 굉장히 복잡하고, 새로운 정책을 추가하기 위해서는 불필요하게 많은 수의 클래스를 상속 계층 안에 추가해야 한다. (코드)

 

이런 상속의 남용으로 하나의 기능을 위해 필요 이상으로 많은 수의 클래스가 나오는 경우를 클래스 폭발이라고 부른다.

✏️ ex. 약정 할인 정책을 적용해야 할 경우, 기존 정책들의 클래스들에 약정 할인 정책이 적용된 자식 클래스가 나와야 한다. 조합의 수만큼 새로운 클래스가 추가된다.

 

03 합성 관계로 변경하기

합성을 사용하면 구현 시점에 정책들의 관계를 고정시킬 필요가 없으며 실행 시점에 정책들의 관계를 유연하게 변경할 수 있게 된다. 상속이 조합의 결과를 개별 클래스 안에로 밀어 넣는 방법이라면 합성은 조합을 구성하는 요소들을 개별 방법으로 구현한 후 실행 시점에 인스턴스를 조립하는 방법이다.

 

기본 정책 합성하기

그림 11.7

상속 관계였던 코드를 합성 관계로 변경하면 이렇게 된다. (코드)

public class Phone {
    // 이것이 합성
    private RatePolicy ratePolicy;
    private List<Call> calls = new ArrayList<Call>();

    public Phone(RatePolicy ratePolicy) {
        this.ratePolicy = ratePolicy;
    }

    public Money calculateFee() {
        return ratePolicy.calculateFee(this);
    }

    public List<Call> getCalls() {
        return Collections.unmodifiableList(calls);
    }
}

Phone 안에 RatePolicy(interface) 참조자가 있게 되고, 이것이 바로 합성이다.

만약 일반 요금제에 따라 통화 요금을 계산하고 싶다면 다음과 같이 Phone과 BasicRatePolicy 인스턴스를 합성한다.

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

 

부가 정책 적용하기

부가 정책은 기본 정책에 대한 계산이 끝난 후 적용되므로, 세금 정책을 추가하게 된다면 RegularPolicy의 계산이 끝나고 Phone에게 반환되기 전에 적용돼야 한다.

public abstract class AdditionalRatePolicy implements RatePolicy{
    private RatePolicy next;

    public AdditionalRatePolicy(RatePolicy next) {
        this.next = next;
    }

    @Override
    public Money calculateFee(Phone phone) {
        Money fee = next.calculateFee(phone);
        return afterCalculated(fee);
    }

    abstract protected Money afterCalculated(Money fee);
}

부가 정책 추상 클래스를 만들고, 이 부가 정책 추상 클래스를 상속받는 각각의 부가 정책 클래스들을 만들 수 있다.

만약 일반 요금제에 기본 요금 할인 정책을 조합한 결과에, 세금 정책도 조합하고 싶다면 다음과 같이 생성하면 된다. 순서의 변경도 자유롭다. (코드)

Phone phone = new Phone(
		new TaxablePolicy(0.05,
        	new RateDiscountablePolicy(Money.wons(1000),
            	new RegularPolicy(...)));

 

새로운 정책 추가하기

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

그리고 더 중요한 것은 요구사항을 변경할 때 오직 하나의 클래스만 수정해도 된다는 것이다. 세금 정책을 변경할 때, TaxablePolicy 클래스 하나만 변경하면 된다. (단일 책임 원칙 준수)

 

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

코드를 재사용하면서도 건전한 결합도를 유지하는 방법은 합성을 이용하는 것이다. 그렇다면 상속은 사용해서는 안 되는가? 의문에 대답하기 위해서는 상속을 구현 상속과 인터페이스 상속 두 가지로 나누어야 한다. (13장에서 다루게 된다.)

 

다음 장으로 넘어가기 전에 코드를 재사용할 수 있는 유용한 기법을 한 가지 더 살펴보자. 믹스인이라는 이름으로 알려진 기법은 상속과 합성의 특성을 보유하고 있는 독특한 코드 재사용 방법이다.

 

04 믹스인

믹스인(mixin)은 객체를 생성할 때 코드 일부를 클래스 안에 섞어 넣어 재사용하는 기법이다. 합성이 실행 시점에 객체를 조합하는 재사용 방법이라면 믹스인은 컴파일 시점에 필요한 코드 조각을 조합한다.

 

믹스인은 책에서는 다른 언어로 설명해주는데, 자바로는 믹스인 디자인패턴에 관한 다른 블로그를 참조하면 좋을 것 같다.

https://xogns93.tistory.com/151