개발자는 기록이 답이다

오브젝트 2장 - 객체지향 프로그래밍(2) 본문

기술 서적/OOP

오브젝트 2장 - 객체지향 프로그래밍(2)

slow-walker 2023. 11. 27. 16:52

 

4. 상속과 다형성

 

Movie클래스 어디에서도 할인 정책이 금액인지 비율인지 판단하지 않는다.

Movie 내부에 할인 정책을 결정하는 조건문이 없음에도 어떻게 영화 요금을 계산할 때 할인 정책을 선택할 수 있었을까?

public class Movie {
	private String title;
    private Duration runningTime;
    private Money fee;
    private DiscountPolicy discountPolicy; // 금액할인? 비율할인?
    
    public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
    	this.title = title;
        this.runningTime = runningTime;
        this.fee = fee;
        this.discountPolicy = discountPolicy;
    }     
    public Money getFee() {
    	return fee;    
    }     
    public Money calculateMovieFee(Screening screening) {
    	return fee.minus(discountPolicy.calculateDiscountAmount(screening));
    }
}

 

먼저 의존성의 개념을 살펴보고 상속와 다형성을 이용해 특정 조건을 선택적으로 실행하는 방법을 알아보자

 

컴파일 시간 의존성과 실행 시간 의존성

Movie는 DiscountPolicy와 연결돼 있으며, AmountDiscountPolicy와 PercentDiscountPolicy는 추상클래스인 DiscountPolicy를 상속받는다. 어떤 클래스가 다른 클래스에 접근할 수 있는 경로를 가지거나 해당 클래스의 객체의 메서드를 호출할 경우 두 클래스 사이에 의존성이 존재한다고 말한다.

 

여기서 중요한 부분은 Movie 클래스가 DiscountPolicy클래스와 연결돼 있다는 것이다.

문제는 영화 요금을 계산하기 위해서는 추상 클래스인 DiscountPolicy가 아니라  AmountDiscountPolicy와 PercentDiscountPolicy 인스턴스가 필요하다.

 

따라서 Movie의 인스턴스는 실행시에  AmountDiscountPolicy나 PercentDiscountPolicy의 인스턴스에 의존해야 한다.

하지만 코드 수준에서 Movie클래스는 이 두 클래스 중 어떤 것에도 의존하고 있지 않고 추상 클래스인 DiscountPolicy에만 의존한다.

 

Movie의 인스턴스가 코드 작성 시점에는 그 존재조차 알지 못했던 두 클래스의 인스턴스와 실행 시점에 협력이 가능한 이유는 뭘까?

 

Movie인스턴스를 생성할때 인자로 AmountDiscountPolicy의 인스턴스를 전달하는 걸 볼 수 있다.

Movie avatar = new Movie("아바타",
    Duration.ofMinutes(120),
    Money.wons(10000),
    new AmountDiscountPolicy(Money.wons(800),
    	new SequenceCondition(1),
        new SequenceCondition(10),
        new PeriodCondition(DayOfWeek.MONDAY, LocalTime.of(10,0), LocalTime.of(11,59)),
        new PeriodCondition(DayOfWeek.THURSDAY, LocalTime.of(10,0), LocalTime.of(20,59))));

 

코드의 의존성과 실행 시점의 의존성을 서로 다를 수 있다.

 

클래스 사이의 의존성과 객체 사이의 의존성은 동일하지 않을 수 있다.

그리고 유연하고, 쉽게 재사용할 수 있으며, 확장 가능한 객체 지향 설계가 가지는 특징은 코드의 의존성과 실행 시점의 의존성이 다르다는 것이다. 하지만 코드의 의존성과 실행 시점의 의존성이 다르면 다를수록 코드를 이해하기 어려워진다. 코드를 이해하기 위해서는 코드 뿐만 아니라 객체를 생성하고 연결하는 부분을 찾아야 하기 때문이다.

 

반면 코드의 의존성과 실행 시점의 의존성이 다르면 다를 수록 코드는 더 유연해지고 확장 가능해진다. 이와 같은 의존성의 양면성은 설계가 트레이드 오프의 산물이라는 사실을 잘 보여준다.

 

e.g. Movie인스턴스가 어떤 객체에 의존하고 있는지 객체의 정확한 타입을 알기 위해서는 Movie 클래스의 코드만 살펴보는게 아니라 의존성을 연결하는 부분을 찾아봐야 한다. 지금 같은 경우에는 Movie인스턴스를 생성하는 부분을 찾아 생성자에 전달되는 객체가 어떤것인지 확인한 후에만 의존성의 대상이 무엇인지 알 수 있다.

 

💡 설계가 유연해질 수록 코드를 이해하고 디버깅하기는 점점 더 어려워 진다. 반면 유연성을 억제하면 코드를 이해하고 디버깅하기는 쉬워지지만 재사용성과 확장 가능성은 낮아진다.

여러분이 훌륭한 객체지향 설계자로 성장하기 위해서는 항상 유연성과 가독성 사이에서 고민해야 한다.
무조건 유연한 설계도, 무조건 읽기 쉬운 코드도 정답이 아니다. 이것이 객체지향 설계가 어려우면서도 매력적인 이유다.

 

코드 상에 존재하는 Movie클래스에서 DiscountPolicy클래스로의 의존성이 어떻게 실행 시점에는 다른 인스턴스에 대한 의존성으로 바뀔 수 있을까? 상속을 살펴보자

 

차이에 의한 프로그래밍

 

클래스를 하나 추가하고 싶은데, 그 클래스가 기존의 어떤 클래스와 매우 흡사하다고 가정해보자.

그 클래스의 코드를 가져와 약간만 추가하거나 수정해서 새로운 클래스를 만드는 것보다 더 좋은 방법은 해당 클래스의 코드를 전혀 수정하지 않고도 재사용하는 것일 것이다. 이걸 가능하게 해주는 방법이 바로 상속이다.

 

상속이란?

객체 지향에서 코드를 재사용하기 위해 가장 널리 사용되는 방법이다. 상속을 이용하면 클래스 사이에 관계를 설정하는 것만으로 기존 클래스가 가지고 있는 모든 속성과 행동을 새로운 클래스에 포함시킬 수 있다.

상속을 이용하면 부모 클래스의 구현은 공유하면서도 행동이 다른 자식 클래스를 쉽게 추가할 수 있다.

 

e.g. AmountDiscountPolicy와 PercentDiscountPolicy의 경우 DiscountPolicy에서 정의한 추상 메서드인 getDiscountAmount메서드를 오버라이딩해서 DiscountPolicy의 행동을 수정한다

 

차이에 의한 프로그래밍(programming by difference) : 부모 클래스와 다른 부분만을 추가해서 새로운 클래스를 쉽고 빠르게 만드는 방법

 

상속과 인터페이스

 

상속이 가치있는 이유는 부모 클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려받을 수 있기 때문이다.

이것은 상속을 바라보는 일반적인 인식과 거리가 있는데 대부분의 사람들은 상속의 목적이 메서드나 인스턴스 변수를 재사용하는것이라고 생각하기 때문이다.

 

인터페이스는 객체가 이해할 수 있는 메시지의 목록을 정의한다는것을 기억하라.

상속을 통해 자식 클래스는 자신의 인터페이스에 부모 클래스의 인터페이스를 포함하게 된다.

 

결과적으로 자식 클래스는 부모 클래스가 수신할 수 있는 모든 메시지를 수신할 수 있기 때문에 외부 객체는 자식 클래스를 부모 클래스와 동일한 타입으로 간주할 수 있다.

 

Movie가 DiscountPolicy의 인터페이스에 정의된 calculateDiscountAmount 메시지를 전송하고 있다.

DiscountPolicy를 상속받는 AmountDiscountPolicy와 PercentDiscountPolicy의 인터페이스에도 이 오퍼레이션이 포함돼 있다는 사실에 주목하라.

 

Movie입장에서는자신과 협력하는 객체가 어떤 클래스의 인스턴스인지 중요한 것이 아니라 calculateDiscountAmount메시지를 수신할 수 있다는 사실이 중요하다. 즉, Movie의 협력 객체가 calculateDiscountAmount라는 메시지를 이해할 수만 있다면 그 객체가 어떤 클래스의 인스턴스인지 상관하지 않는다.

 

정리하면 자식 클래스는 상속을 통해 부모 클래스의 인터페이스를 물려받기 때문에 부모 클래스 대신 사용될 수 있다.

컴파일러는 코드 상에서 부모 클래스가 나오는 모든 장소에서 자식 클래스를 사용하는 것을 허용한다.

Movie의 생성자에서 인자의 타입이 DiscountPolicy임에도 AmountDiscountPolicy와 PercentDiscountPolicy의 인스턴스를 할당할 수 있는 이유도 이때문이다.

이처럼 자식 클래스가 부모 클래스를 대신하는 것을 업캐스팅(upcasting)이라고 한다.

아래에 위치한 자식 클래스가 위에 위치한 부모 클래스로 자동적으로 타입 캐스팅이 되는것 처럼 보이기 때문에 업캐스팅이라는 용여를 사용한다.

 

다형성

 

 

메시지메서드는 다른 개념이다.

Movie는 DiscountPolicy의 인스턴스에게 calculateDiscountAmount 메시지를 전송한다.

 

그렇다면 실행되는 메서드는 무엇일까?

Movie와 상호작용하기 위해 연결된 객체의 클래스가 무엇인가에 따라 달라진다.

Movie와 협력하는 객체가 AmountDiscountPolicy의 인스턴스라면 AmountDiscountPolicy에서 오버라이딩한 메서드가 실행될 것이고, PercentDiscountPolicy의 인스턴스가 연결된 경우에는 PercentDiscountPolicy에서 오버라이딩한 메서드가 실행될 것이다.

 

다형성이란?

특정 클래스에서 동일한 메시지를 전송하지만 실제로 어떤 메서드가 실행될 것인지를 메시지를 수신하는 개체의 클래스가 무엇이냐에 따라 달라진다.

  • 다형성은 객체 지향 프로그램의 컴파일 시간 의존성과 실행 시간 의존성이 다를 수 있다는 사실을 기반으로 한다.
  • 동일한 메시지를 수신했을 때 객체의 타입에 따라 다르게 응답할 수 있다.
  • 따라서 다형적인 협력에 참여하는 객체들은 모두 같은 메시지를 이해할 수 있어야 한다. 다시 말해서 인터페이스가 동일해야 한다. 동일한 인터페이스를 구현하기 위한 방법이 바로 상속이다.

 

다형성을 구현하는 방법은 매우 다양하지만, 메시지에 응답하기 위해 실행될 메서드를 컴파일 시점이 아닌 실행 시점에 결정한다는 공통점이 있다.

 

  • 지연 바인딩(lazy binding),동적 바인딩(dynamic binding) : 컴파일 시점의 의존성과 실행 시점의 의존성 분리
  • 초기 바인딩(early binding),정적 바인딩(static binding) : 컴파일 시점의 의존성과 실행 시점의 의존성 동일

 

 

🚩 구현상속과 인터페이스 상속

1. 구현 상속(implementation inheritance) - 서브클래싱(subclassing)
2. 인터페이스 상속(interface inheritance) - 서브타이핑(subtyping)

순수하게 코드를 재사용하기 위한 목적으로 상속을 사용하는 것을 구현 상속이라고 부른다.
다형적인 협력을 위해 부모 클래스와 자식 클래스가 인터페이스를 공유할 수 있도록 상속을 이용하는 것을 인터페이스 상속이라고 부른다.

상속은 구현 상속이 아니라 인터페이스 상속을 위해 사용해야 한다. 대부분의 사람들은 코드 재사용을 상속의 주된 목적이라고 생각하지만 이것은 오해다. 인터페이스를 재사용할 목적이 아니라 구현을 재사용할 목적으로 상속을 사용하면 변경에 취약한 코드를 낳게 될 확률이 높다.

 

인터페이스와 다형성

 

DiscountPolicy를 추상 클래스로 구현함으로써 자식 클래스들이 인터페이스와 내부 구현을 함께 상속받도록 만들었다

그러나 종종 구현은 공유할 필요가 없고 순수하게 인터페이스만 공유하고 싶을때가 있다. 자바의 인터페이스는 말 그대로 구현에 대한 고려 없이 다형적인 협력에 참여하는 클래스들이 공유 가능한 외부 인터페이스를 정의한 것이다.

 

DiscountCondition은 구현을 공유할 필요가 없기 때문에 자바의 인터페이스를 이용해 타입 계층을 구현했다.

e.g. DiscountCondition 인터페이스를 실체화 하고 있는 SequenceCondition과 PeriodCondition은 동일한 인터페이스를 공유하며 다형적 협력에 참여할 수 있다. 해당 두 클래스는 동일한 인터페이스를 공유하며 DiscountCondition을 대신해서 사용될 수 있어서 업캐스팅이 적용되며 협력이 다형적이라고 말한다.

 

5. 추상화와 유연성

 

추상화의 힘

 

할인 정책은 금액 할인 정책과 비율 할인 정책을 포괄하는 추상적인 개념이다

할인 조건은 순번 조건과 기간 조건을 포괄하는 추상적인 개념이다.

 

프로그래밍 언어 측면에서 DiscountPolicy와 DiscountCondition이 더 추상적인 이유는 인터페이스에 초점을 맞추기 때문이다.

DiscountPolicy는 모든 할인 정책들이 수신할 수 있는 calculateDiscountAmount메시지를 정의한다.

DiscountCondition은 모든 할인 조건들이 수신할 수 있는 isSatisfiedBy메시지를 정의한다.

 

둘 다 같은 계층에 속하는 클래스들이 공통으로 가질 수 있는 인터페이스를 정의하며 구현의 일부(추상 클래스인 경우) 또는 전체(자바 인터페이스인 경우)를 자식 클래스가 결정할 수 있도록 결정권을 위임한다.

 

🚩 추상화를 사용할 경우 장점?

추상화를 사용하면 세부적인 내용을 무시한 채 상위 정책을 쉽고 간단하게 표현할 수 있다. 세부사항에 억눌리지 않고 상위 개념만으로도 도메인의 중요한 개념을 설명할 수 있게 한다.

 

1. 추상화의 계층만 따로 떼어 놓고 살펴보면 요구사항의 정책을 높은 수준에서 서술할 수 있다.

  • e.g. 영화 예매 요금은 최대 하나의 '할인 정책'과 다수의 '할인 조건'을 이용해 계산할 수 있다.
  • e.g. 영화 예매 요금은 '금액 할인 정책'과 '두 개의 순서 조건, 한 개의 기간 조건'을 이용해서 계산할 수 있다. 라는 문장을 포괄할 수 있다는 사실이 중요하다
  • 새로운 자식 클래스들은 추상화를 이용해서 정의한 상위의 협력 흐름을 그대로 따른다. 이 개념은 재사용 가능한 설계의 기본을 이루는 디자인 패턴(design pattern)이나 프레임워크(framework) 모두 추상화를 이용해 상위 정책을 정의하는 객체 지향의 매커니즘을 활용하고 있기 때문이다.

2. 추상화를 이용하면 설계가 좀 더 유연해진다.

  • 추상화를 이용해 상위 정책을 표현하면 기존 구조를 수정하지 않고도 새로운 기능을 쉽게 추가하고 확장할 수 있다.

 

유연한 설계

 

만일 할인 저액이 적용되어있지 않다면, 할인 요금을 계싼할 필요 없이 영화에 설정된 기본 금액을 그대로 사용하면 된다.

하지만 아래 방식은 할인 정책이 없는 경우를 예외 케이스로 취급하기 때문에 지금까지 일관성이 있던 협력 방식이 무너지게 된다.

    public Money calculateMovieFee(Screening screening) {
        if(disCountPolicy == null) return fee;
        return fee.minus(disCountPolicy.calculateDiscountAmount(screening));
    }

 

기존 할인 정책의 경우 할인할 금액을 계산하는 책임이 DiscountPolicy의 자식 클래스에 있엇지만, 할인 정책이 없는 경우 할인 금액이 0원이라는 사실을 결정하는 책임이 DiscountPolicy가 아닌 Movie쪽에 있기 때문이다.

 

따라서 책임의 위치를 결정하기 위해 조건문을 사용하는 것은 협력의 설계 측면에서 대부분의 경우 좋지 않은 선택이다.

항상 예외 케이스를 최소화하고 일관성을 유지할 수 있는 방법을 선택하라

 

일관성을 유지시키기 위해 NoneDiscountPolicy를 생성하여 0원이라는 할인 요금을 계산할 책임을 그대로 DiscountPolicy에 유지시키면 된다.

public class NoneDiscountPolicy extends DiscountPolicy {
    @Override
    protected Money getDiscountAmount(Screening screening) {
        return Money.ZERO;
    }
}
        // 할인되지 않는 영화
        Movie starWars = new Movie("스타워즈",
                Duration.ofMinutes(210),
                Money.wons(10000),
                new NoneDiscountPolicy());
    }

 

 

여기서 중요한 것은 기존의 Movie와 DiscountPolicy는 수정하지 않고 NoneDiscountPolicy라는 새로운 클래스를 추가하는 것만으로 애플리케이션의 기능을 확장했다는 것이다. 이처럼 추상화를 중심으로 코드의 구조를 설계하면 유연하고 확장 가능한 설계를 만들 수 있다.

 

🚩  추상화가 유연한 설계를 가능하게 하는 이유?

 

설계가 구체적인 상황에 결합되는것을 방지하기 때문이다.

Movie는 특정한 할인 정책에 묶이지 않고, 할인정책을 구현한 클래스가 DiscountPolicy를 상속한다면 어떤 클래스와도 협력이 가능하다.

DiscountPolicy는 특정 할인 조건에 묶여있지 않다. DiscountCondition을 상속받은 어떤 클래스와도 협력이 가능하다.

컨텍스트 독립성(context independency)라고 불리는 이 개념은 프레임워크과 같은 유연한 설계가 필수적인 분야에서 그 진가를 발휘한다.

유연성이 필요한 곳에 추상화를 사용하라.

 

추상 클래스와 인터페이스 트레이드 오프

 

NoneDiscountPolicy클래스 코드를 자세히 보면 getDiscountAmount메서드가 어떤 값을 반환하더라도 상관이 없다는 사실을 알 수 있다. DiscountPolicy클래스에서 할인조건을 만족하지 못할 경우 getDiscountAmount메서드를 호출하지 않기 때문이다.

 

이것은 부모 클래스인 DiscountPolicy와 NoneDiscountPolicy를 개념적으로 결합시킨다 NoneDiscountPolicy의 개발자는 getDiscountAmount 메서드가 호출되지 않을 경우 DiscountPolicy가 0원을 반환할것이라는 사실을 가정하고 있기 때문이다.

 

 

해당 하는 문제는 DiscountPolicy를 인터페이스로 바꾸고, NoneDiscountPolicy가 DiscountPolicy의 getDiscountAmount메서드가 아닌 calculateDiscountAmount()오퍼레이션을 오버라이딩하면서 해결할 수 있다.

원래의 DiscountPolicy 클래스는 DefaultDiscountPolicy로 변경하고 인터페이스를 구현하도록 수정한다.

그리고 NoneDiscountPolicy가 DiscountPolicy를 구현하도록 변경하면 개념적인 혼란과 결합을 제거할 수 있다.

 

 

 

🚩 어떤 설계가 더 좋은 것일까?

 

구현과 관련된 모든 것들이 트레이드오프의 대상이 될 수 있다.

 

이상적으로는 인터페이스를 사용하도록 변경한 설꼐가 더 좋을 것이다. 현실적으로는 NoneDiscountPolicy만을 위해 인터페이스를 추가하는 것이 과하다는 생각이 들 수도 있다. 어쨋든 변경 전의 NoneDiscountPolicy 클래스 역시 할인 금액이 0원이라는 사실을 효과적으로 전달하기 때문이다.

여러분이 작성하는 모든 코드에는 합당한 이유가 있어야 한다.
비록 아주 사소한 결정이더라도 트레이드 오프를 통해 얻어진 결론과 그렇지 않은 결론 사이의 차이는 크다.
고민하고 트레이드 오프해라.

 

코드 재사용

상속은 코드를 재사용하기 위해 널리 사용되는 방법이긴 하지만 가장 좋은 방법은 아니다

객체지향 설계와 관련된 자료를 조금이라도 본 사람들은 코드를 재사용하기 위해 상속보다는 합성(composition)이 더 좋은 방법이라는 이야기를 많이 들었을 것이다.

 

합성은 다른 객체의 인스턴스를 자신의 인스턴스 변수로 재사용하는 방법을 말한다.

 

Movie가 DiscountPolicy의 코드를 재사용하는 방법이 바로 합성이다. 이 설계를 상속을 사용하도록 변경할 수도 잇따.

아래 그림과 같이 Movie를 직접 상속받아 2개의 클래스를 추가하면 합성을 사용한 기존 바업과 기능적인 관점에서 완벽히 동일하다. 그럼에도 많은 사람들이 상속 대신 합성을 선호하는 이유는 무엇일까?

 

상속

 

상속은 2가지 관점에서 설계에 안 좋은 영향을 미친다.

 

1. 상속이 캡슐화를 위반한다

 

상속을 이용하기 위해서 부모 클래스의 내부 구조를 잘 알고 있어야 한다.

AmountDiscountMovie와 PercentDiscountMovie를 구현하는 개발자는 부모 클래스인 Movie의 calculateMovieFee메서드 안에서 추상 메서드인 getDiscountAmount 메서드를 호출한다는 사실을 알고 있어야 한다.

 

결과적으로 부모 클래스의 구현이 자식 클래스에게 노출되기 때문에 캡슐화가 약화된다. 캡슐화의 약화는 자식 클래스가 부모 클래스에 강하게 결합되도록 만들기 때문에 부모 클래스를 변경할 때 자식 클래스도 함께 변경될 확률을 높인다.

 

결과적으로 상속을 과도하게 사용한 코드는 변경하기도 어려워진다.

 

2. 설계를 유연하지 못하게 한다.

 

 

상속은 부모 클래스와 자식 클래스 사이의 관계를 컴파일 시점에 결정한다. 따라서 실행 시점에 객체의 종류를 변경하는 것이 불가능하다.

 

예를 들어, 실행 시점에 금액 할인 정책인 영화를 비율 할인 정책으로 변경한다고 가정하자.

상속을 사용한 설계에서는 AmountDiscountMovie의 인스턴스를 PercentDiscountMovie의 인스턴스로 변경해야 한다.

 

대부분의 언어는 이미 생성된 객체의 클래스를 변경하는 기능을 지원하지 않기 대문에 이 문제를 해결할 수 있는 최선의 방법은 PercentDiscountMovie의 인스턴스를 생성한 후 AmountDiscountMovie의 상태를 복사하는 것 뿐이다.

 

이것은 부모 클래스와 자식 클래스가 강하게 결합돼 있기 때문에 발생하는 문제다.

 

반면 인스턴스 변수로 연결한 기존 방법을 사용하면 실행 시점에 할인 정책을 간단하게 변경할 수 있다.

public class Movie {

    private DiscountPolicy disCountPolicy;
    
    public void changeDiscountPolicy(DiscountPolicy disCountPolicy) {
        this.disCountPolicy = disCountPolicy;
    }
}
Movie avatar = new Movie("아바타",
        Duration.ofMinutes(120),
        Money.wons(10000),
        new AmountDiscountPolicy(Money.wons(800),
                new SequenceCondition(1),
                new SequenceCondition(10),
                new PeriodCondition(DayOfWeek.MONDAY, LocalTime.of(10,0), LocalTime.of(11,59)),
                new PeriodCondition(DayOfWeek.THURSDAY, LocalTime.of(10,0), LocalTime.of(20,59))));

avatar.changeDiscountPolicy(new PercentDiscountPolicy(0.1));

 

상속보다 인스턴스 변수로 관계르 ㄹ연결한 원래의 설계가 더 유연하다는 사실을 알 수 있을 것이다.

Movie가 DiscountPolicy를 포함하는 이 방법 역시 코드를 재사용하는 바법은 아래에서 더 살펴보자.

 

합성

 

합성이 상속과 다른 점은 상속이 부모 클래스의 코드와 자식 클래스의 코드를 컴파일 시점에 하나의 단위로 강하게 결합하는데 비해 Movie가 DiscountPolicy의 인터페이스를 통해 약하게 결합된다는 것이다.

 

 Movie는 DiscountPolicy가 외부에 calculateDiscountAmount메서드를 제공한다는 사실만 알고 내부 구현에 대해서는 전혀 알지 못한다. 이처럼 인터페이스에 정의된 메시지를 통해서만 코드를 재사용하는 방법을 합성이라고 부른다.

 

합성은 상속이 가지는 2가지 문제점을 모두 해결한다. 인터페이스에 정의된 메시지를 통해서만 재사용이 가능하기 때문에 구현을 효과적으로 캡슐화할 수 있다. 또한 의존하는 인스턴스를 교체하는 것이 쉽기 때문에 설계를 유연하게 만든다.

 

상속은 클래스를 통해 강하게 결합되는데 비해 합성은 메시지를 통해 느슨하게 결합된다.
따라서 코드 재사용을 위해서는 상속보다는 합성을 선호하는 것이 더 좋은 방법이다[GOF94]

 

하지만 상속을 절대 사용하지 말라는 것은 아니다! 대부분의 설계에서는 상속과 합성을 함께 사용해야 한다.

코드를 재사용하는 경우에는 상속보다 합성을 선호하는 것이 옳지만 다형성을 위해 인터페이스를 재사용하는 경우에는  상속과 합성을 함께 조합해서 사용할 수 밖에 없다.

💡 객체지향 설계의 핵심 :
적절한 협력을 식별하고 협력에 필요한 역할을 정의한 후 역할을 수행할 수 있는 적절한 객체에게 적절한 책임을 할당하는 것이다.

 

코드 확인할 수 있는 Github 링크

 

GitHub - codesejin/OOP-Object: [오브젝트] - 코드로 이해하는 객체지향 설계

[오브젝트] - 코드로 이해하는 객체지향 설계. Contribute to codesejin/OOP-Object development by creating an account on GitHub.

github.com