본문 바로가기
Study/오브젝트

[오브젝트] 10장. 상속과 코드 재사용

by Nahwasa 2023. 1. 3.

스터디 메인 페이지

목차

    - ☆ 표시가 붙은 부분은 스터디 중 나온 얘기 혹은 제 개인적인 생각이나 제가 이해한 방식을 적어놓은 것으로, 책에 나오지 않는 내용입니다. 따라서 책에서 말하고자 하는 바와 다를 수 있습니다.

    - 모든 이미지의 출처는 오브젝트(조용호 저) 책 입니다.

     


     

    CHAPTER 10. 상속과 코드 재사용                                                       

    ⚈ 코드 재사용

    • 전통적인 패러다임 : 코드를 복사한 후 수정
    • 객체지향 : 코드를 재사용하기 위해 '새로운' 코드를 추가. 객체지향에서 클래스를 재사용하는 전통적인 방법은 새로운 클래스를 추가하는 것.

     

    ⚈ 이번 장에서는 상속을 통한 코드 재사용을 알아보고, 11장에서 코드를 효과적으로 재사용할 수 있는 합성을 알아본 후 상속과 합성의 장단점을 비교하게 된다.

     


    01 상속과 중복 코드

    ⚈ 중복 코드는 사람들의 마음속에 의심과 불신의 씨앗을 뿌린다.

    • ☆ ㄹㅇㅋㅋ 단순 복붙해둔 중복코드 보면 다른 부분은 보고싶지도 않음.

     

    중복 여부를 판단하는 기준은 변경이다.

    • 요구사항이 변경됐을 때 두 코드를 함께 수정해야 한다면 이 코드는 중복이다.
    • ☆ 똑같이 생긴걸 복붙한걸 중복 코드라고 생각했는데, 이 책에서 말하는 중복의 기준을 보니 확실히 책의 말이 맞는 것 같다. 동일한 이유로 수정이 되는 부분이라면 중복이 맞지!
    • DRY 원칙 : Don't Repeat Yourself - 동일한 지식을 중복하지 말라. 모든 지식은 시스템 내에서 단일하고, 애매하지 않고, 정말로 믿을 만한 표현 양식을 가져야 한다.

     

    중복 코드는 서로 다르게 수정하기가 쉽다

    • 많은 코드 더미 속에서 어떤 코드가 중복인지를 파악하는 일은 쉬운 일이 아니다.
    • 중복 코드는 항상 함께 수정돼야 한다.
    • 중복 코드는 새로운 중복 코드를 부른다.
    • 중복 코드를 제거하지 않은 상태에서 코드를 수정할 수 있는 유일한 방법은 새로운 중복 코드를 추가하는 것뿐이다.
    • ☆ 변경되는 부분과 안되는 부분을 잘 파악해서 디자인 패턴을 적용하자!

     

    ⚈ 타입 코드 사용하기

    • 두 클래스 사이의 중복 코드를 제거하는 한 가지 방법은 클래스를 하나로 합치는 것이다.
    • 타입 코드를 추가하고 타입 코드의 값에 따라 로직을 분기시키는 방식
    • 낮은 응집도와 높은 결합도라는 문제에 시달리게 된다.
    public Money calculateFee() {
        Money result = Money.ZERO;
    
        for(Call call : calls) {
            if (type == PhoneType.REGULAR) {
                result = result.plus(amount.times(call.getDuration().getSeconds() / seconds.getSeconds()));
            } else {
                if (call.getFrom().getHour() >= LATE_NIGHT_HOUR) {
                    result = result.plus(nightlyAmount.times(call.getDuration().getSeconds() / seconds.getSeconds()));
                } else {
                    result = result.plus(regularAmount.times(call.getDuration().getSeconds() / seconds.getSeconds()));
                }
            }
        }
    
        return result;
    }

     

    ⚈ 상속 사용하기

    • 상속은 타입 코드를 사용하지 않고도 중복 코드를 관리할 수 있는 효과적인 방법을 제공한다.
    • 이미 존재하는 클래스와 유사한 클래스가 필요하다면 코드를 복사하지 말고 상속을 이용해 코드를 재사용하자 -> ☆ 바로 단점이 나오긴 하지만, 어쨌든 코드 복붙보단 낫다!
    public class NightlyDiscountPhone extends Phone {
        private static final int LATE_NIGHT_HOUR = 22;
    
        private Money nightlyAmount;
    
        public NightlyDiscountPhone(Money nightlyAmount, Money regularAmount, Duration seconds) {
            super(regularAmount, seconds);
            this.nightlyAmount = nightlyAmount;
        }
    
        @Override
        public Money calculateFee() {
            // 부모클래스의 calculateFee 호출
            Money result = super.calculateFee();
    
            Money nightlyFee = Money.ZERO;
            for(Call call : getCalls()) {
                if (call.getFrom().getHour() >= LATE_NIGHT_HOUR) {
                    nightlyFee = nightlyFee.plus(
                        getAmount().minus(nightlyAmount).times(
                            call.getDuration().getSeconds() / getSeconds().getSeconds()));
                }
            }
    
            return result.minus(nightlyFee);
        }
    }

     

    위 코드로 알 수 있는 상속을 통한 코드 재사용의 문제점

    • 위 코드의 부모 클래스인 Phone의 calculateFee()는 아래와 같다.
    public Money calculateFee() {
        Money result = Money.ZERO;
    
        for(Call call : calls) {
            result = result.plus(amount.times(call.getDuration().getSeconds() / seconds.getSeconds()));
        }
    
        return result;
    }

     

    • 저걸 재사용하기 위해 상속을 통해 짰더니 minus를 통한 계산 식이 복잡하게 짜여졌다. -> 개발자의 가정을 이해하기 전에는 코드를 이해하기 어렵다.
    • 상속을 염두에 두고 설계되지 않은 클래스를 상속을 이용해 재사용하는 것은 생각처럼 쉽지 않다. 또한 직관에도 어긋날 수 있다.
    • 상속을 이용해 코드를 재사용하기 위해서는 부모 클래스의 개발자가 세웠던 가정이나 추론 과정을 정확하게 이해해야 한다.
    • 따라서 상속은 결합도를 높인다. 상속이 초래하는 부모 클래스와 자식 클래스 사이의 강한 결합이 코드를 수정하기 어렵게 만든다.

     

      자식 클래스의 메서드 안에서 super 참조를 이용해 부모 클래스의 메서드를 직접 호출할 경우 두 클래스는 강하게 결합된다. super 호출을 제거할 수 있는 방법을 찾아 결합도를 제거하라.

    • 위 코드에 추가로 세금을 부과하는 요구사항이 추가되었다고 해보자. 그럼 아래와 같이 변경될 것이다.
    // Phone에 추가된 세금 요구사항 로직(taxRate)
    public Money calculateFee() {
        Money result = Money.ZERO;
    
        for(Call call : calls) {
            result = result.plus(amount.times(call.getDuration().getSeconds() / seconds.getSeconds()));
        }
    
        return result.plus(result.times(taxRate));
    }
    
    // NightlyDiscountPhone에 추가된 세금 요구사항 로직(taxRate)
    @Override
    public Money calculateFee() {
        // 부모클래스의 calculateFee() 호출
        Money result = super.calculateFee();
    
        Money nightlyFee = Money.ZERO;
        for(Call call : getCalls()) {
            if (call.getFrom().getHour() >= LATE_NIGHT_HOUR) {
                nightlyFee = nightlyFee.plus(
                    getAmount().minus(nightlyAmount).times(
                        call.getDuration().getSeconds() / getSeconds().getSeconds()));
            }
        }
    
        return result.minus(nightlyFee.plus(nightlyFee.times(getTaxRate())));
    }

     

    • Phone의 코드를 재사용하고 중복 코드를 제거하기 위해 Phone의 자식 클래스로 NightlyDiscountPhone을 만든건데, 세금을 부과하는 로직을 추가하기 위해 Phone을 수정할 때 유사한 코드를 NightlyDiscountPhone에도 추가해야 했다. 즉 새로운 중복 코드를 만들었다.
    • 이것은 NightlyDiscountPhone이 Phone의 구현에 너무 강하게 결합돼 있기 때문에 발생하는 문제다.
    • ☆ 부모클래스를 알고 있어야 하는 것도 캡슐화를 어기는 것이다. 부모가 바뀌면 자식도 바뀌어야 하니

     


    02 취약한 기반 클래스 문제

    위에서 본 것 처럼 상속 관계로 연결된 자식 클래스가 부모 클래스의 변경에 취약해지는 현상을 가리켜 취약한 기반 클래스 문제(Fragile Base Class Problem, Brittle Base Class Problem)라고 한다.

    • 상속을 사용한다면 피할 수 없는 객체지향 프로그래밍의 근본적인 취약성이다.
    • 상속이라는 문맥 안에서 결합도가 초래하는 문제점을 가리키는 용어다. 상속 관계를 추가할수록 전체 시스템의 결합도가 높아진다는 사실을 알고 있어야 한다.
    • 객체를 사용하는 이유는 구현과 관련된 세부사항을 퍼블릭 인터페이스 뒤로 캡슐화할 수 있기 때문이다. 캡슐화는 변경에 의한 파급효과를 제어할 수 있기 때문에 가치있다.
    • 안타깝게도 상속을 사용하면 부모 클래스의 퍼블릭 인터페이스가 아닌 구현을 변경하더라도 자식 클래스가 영향을 받기 쉬워진다. 상속은 코드의 재사용을 위해 캡슐화의 장점을 희석시키고 구현에 대한 결합도를 높임으로써 객체지향이 가진 강력함을 반감시킨다.

     

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

    • 자바의 Stack은 Vector를 상속받는다. Vector의 퍼블릭 인터페이스를 이용하면 임의의 위치에서 요소를 추가하거나 삭제할 수 있다. 따라서 Stack의 규칙을 쉽게 위반할 수 있다.
    • 아래와 같은 코드는 스택 구조의 규칙에 따르면 3이 출력되야 하지만, 실제론 2가 출력된다.
    Stack<Integer> stack = new Stack<>();
    stack.push(0);
    stack.push(1);
    stack.push(2);
    stack.add(0, 3);
    System.out.println(stack.pop());

     

    • 상속받은 부모 클래스의 메서드가 자식 클래스의 내부 구조에 대한 규칙을 깨트릴 수 있다.

     

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

    • 이하의 코드는 set에 요소가 추가된 횟수를 기록하기 위해 HashSet을 상속해 작성된 코드이다.
    • addAll에 3개의 요소를 집어넣을 경우 예상은 addCount가 3이 되는 것이지만, 실제론 6이 된다. 부모 클래스인 HashSet의 addAll에서 add를 호출하기 때문이다.
    public class InstrumentedHashSet<E> extends HashSet<E> {
    	private int addCount = 0;
        
        @Override
        public boolean add(E e) {
        	addCount++;
            return super.add(e);
        }
        
        @Override
        public boolean addAll(Collection<? extends E> c) {
        	addCouhnt += c.size();
            return super.addAll(c);
        }
    }

     

    • 자식 클래스가 부모 클래스의 메서드를 오버라이딩할 경우 부모 클래스가 자신의 메서드를 사용하는 방법에 자식 클래스가 결합될 수 있다.
    • 조슈아 블로치 : 클래스가 상속되기를 원한다면 상속을 위해 클래스를 설계하고 문서화해야 하며, 그렇지 않은 경우에는 상속을 금지시켜야 한다.
    • 객체지향의 핵심이 구현을 캡슐화하는 것인데도 이렇게 내부 구현을 공개하고 문서화하는 것이 옳은가?
    • 설계는 트레이드오프 활동이다.
    • 상속은 코드 재사용을 위해 캡슐화를 희생한다. 완벽한 캡슐화를 원한다면 코드 재사용을 포기하거나 상속 이외의 다른 방법을 사용해야 한다.

     

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

    • 음악 목록을 추가할 수 있는 플레이리스트를 구현한다고 가정하자.음악 정보를 저장할 Song 클래스와 음악 목록을 저장할 Playlist는 아래와 같다.
    public class Song {
        private String singer;
        private String title;
    
        public Song(String singer, String title) {
            this.singer = singer;
            this.title = title;
        }
    
        public String getSinger() {
            return singer;
        }
    
        public String getTitle() {
            return title;
        }
    }
    
    ----
    
    public class Playlist {
        private List<Song> tracks = new ArrayList<>();
    
        public void append(Song song) {
            getTracks().add(song);
        }
    
        public List<Song> getTracks() {
            return tracks;
        }
    }

     

    • 이제 노래를 삭제할 수 있는 기능이 추가된 PersonalPlaylist가 필요하다고 해보자. 상속을 통해 Playlist의 코드를 재사용하는게 가장 빠른 방법일 것이다.
    public class PersonalPlaylist extends Playlist {
        public void remove(Song song) {
            getTracks().remove(song);
        }
    }

     

    • 문제는 요구사항이 변경돼서 Playlist에서 노래의 목록뿐만 아니라 가수별 노래의 제목도 함께 관리해야 한다고 가정하자. 그럼 PlayList와 PersonalPlaylist는 아래와 같이 변경될 것이다. 
    public class Playlist {
        private List<Song> tracks = new ArrayList<>();
        private Map<String, String> singers = new HashMap<>();
    
        public void append(Song song) {
            tracks.add(song);
            singers.put(song.getSinger(), song.getTitle());
        }
    
        public List<Song> getTracks() {
            return tracks;
        }
    
        public Map<String, String> getSingers() {
            return singers;
        }
    }
    
    ----
    
    public class PersonalPlaylist extends Playlist {
        public void remove(Song song) {
            getTracks().remove(song);
            getSingers().remove(song.getSinger());
        }
    }

     

    • 위의 예를 통해 자식 클래스가 부모 클래스의 메서드를 오버라이딩하거나 불필요한 인터페이스를 상속받지 않았음에도 부모 클래스를 수정할 때 자식 클래스를 함께 수정해야 할 수도 있다는 사실을 알 수 있다.
    • 코드 재사용을 위한 상속은 부모 클래스와 자식 클래스를 강하게 결합시키기 때문에 함께 수정해야 하는 상황 역시 빈번하게 발생할 수박에 없다.
    • 클래스를 상속하면 결합도로 인해 자식 클래스와 부모 클래스의 구현을 영원히 변경하지 않거나, 자식 클래스와 부모 클래스를 동시에 변경하거나 둘 중 하나를 선택할 수밖에 없다.

     


    03 Phone 다시 살펴보기

    상속으로 인한 피해를 최소화할 수 있는 방법을 찾아보자. 취약한 기반 클래스 문제를 완전히 없앨 수는 없지만 어느 정도까지 위험을 완화시키는 것은 가능하다.

     

    열쇠는 추상화다. 추상화에 의존하자.

    • 부모 클래스와 자식 클래스 모두 추상화에 의존하도록 해야 한다.

     

    코드 중복을 제거하기 위해 상속을 도입할 때 따르는 두 가지 원칙

    • 두 메서드가 유사하게 보인다면 차이점을 메서드로 추출하라.
    • 부모 클래스의 코드를 하위로 내리지 말고 자식 클래스의 코드를 상위로 올려라.

     

    기존 Phone, NightlyDiscountPhone

    public class Phone {
        private Money amount;
        private Duration seconds;
        private List<Call> calls = new ArrayList<>();
        private double taxRate;
    
        public Phone(Money amount, Duration seconds, double taxRate) {
            this.amount = amount;
            this.seconds = seconds;
            this.taxRate = taxRate;
        }
    
        public void call(Call call) {
            calls.add(call);
        }
    
        public List<Call> getCalls() {
            return calls;
        }
    
        public Money getAmount() {
            return amount;
        }
    
        public Duration getSeconds() {
            return seconds;
        }
    
        public Money calculateFee() {
            Money result = Money.ZERO;
    
            for(Call call : calls) {
                result = result.plus(amount.times(call.getDuration().getSeconds() / seconds.getSeconds()));
            }
    
            return result.plus(result.times(taxRate));
        }
    }
    public class NightlyDiscountPhone {
        private static final int LATE_NIGHT_HOUR = 22;
    
        private Money nightlyAmount;
        private Money regularAmount;
        private Duration seconds;
        private List<Call> calls = new ArrayList<>();
        private double taxRate;
    
        public NightlyDiscountPhone(Money nightlyAmount, Money regularAmount, Duration seconds, double taxRate) {
            this.nightlyAmount = nightlyAmount;
            this.regularAmount = regularAmount;
            this.seconds = seconds;
            this.taxRate = taxRate;
        }
    
        public Money calculateFee() {
            Money result = Money.ZERO;
    
            for(Call call : calls) {
                if (call.getFrom().getHour() >= LATE_NIGHT_HOUR) {
                    result = result.plus(nightlyAmount.times(call.getDuration().getSeconds() / seconds.getSeconds()));
                } else {
                    result = result.plus(regularAmount.times(call.getDuration().getSeconds() / seconds.getSeconds()));
                }
            }
    
            return result.minus(result.times(taxRate));
        }
    }

     

    차이를 메서드로 추출 후 중복 코드를 부모 클래스로 올린 코드

    public abstract class AbstractPhone {
        private List<Call> calls = new ArrayList<>();
    
        public Money calculateFee() {
            Money result = Money.ZERO;
    
            for(Call call : calls) {
                result = result.plus(calculateCallFee(call));
            }
    
            return result;
        }
    
        abstract protected Money calculateCallFee(Call call);
    }
    public class Phone extends AbstractPhone {
        private Money amount;
        private Duration seconds;
    
        public Phone(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 NightlyDiscountPhone extends AbstractPhone {
        private static final int LATE_NIGHT_HOUR = 22;
    
        private Money nightlyAmount;
        private Money regularAmount;
        private Duration seconds;
    
        public NightlyDiscountPhone(Money nightlyAmount, Money regularAmount, Duration seconds) {
            this.nightlyAmount = nightlyAmount;
            this.regularAmount = regularAmount;
            this.seconds = seconds;
        }
    
        @Override
        protected Money calculateCallFee(Call call) {
            if (call.getFrom().getHour() >= LATE_NIGHT_HOUR) {
                return nightlyAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
            } else {
                return regularAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
            }
        }
    }

     

    자식 클래스들 사이의 공통점을 부모 클래스로 옮김으로써 실제 코드를 기반으로 상속 계층을 구성할 수 있다. 이제 우리의 설계는 추상화에 의존하게 된다.

    • '위로 올리기' 전략은 실패했더라도 수정하기 쉬운 문제를 발생시킨다.
    • 위로 올리기에서 실수하더라도 추상화할 코드는 눈에 띄고 결국 상위 클래스로 올려지면서 코드의 품질은 높아진다.

     

    추상화가 핵심이다.

    • 위처럼 공통 코드를 이동시킨 후에 각 클래스는 서로 다른 변경의 이유를 가진다. (☆ 즉, 중복이 없어졌다.) 
    • AbstractPhone은 전체 통화 목록을 계산하는 방법이 바뀔 경우에만 변경된다.
    • Phone은 일반 요금제의 통화 한 건을 계산하는 방식이 바뀔 경우에만 변경된다.
    • NightlyDiscountPhone은 심야 할인 요금제의 통화 한 건을 계산하는 방식이 바뀔 경우에만 변경된다.
    • 세 클래스는 각각 하나의 변경 이유만을 가진다 -> 단일 책임 원칙을 준수하기 때문에 응집도가 높다.
    • 또한 caculateCallFee 메서드의 시그니처가 변경되지 않는한 부모 클래스의 내부 구현이 변경되더라도 자식 클래스는 영향을 받지 않는다 -> 낮은 결합도를 유지하고 있다.
    • 새로운 요금제를 추가하기도 쉽다. 새로운 요금제가 필요하다면 AbstractPhone을 상속받는 새로운 클래스를 추가한 후 caculateCallFee 메서드만 오버라이딩하면 된다. -> OCP (개방-폐쇄 원칙) 역시 준수한다.
    • 위의 모든 장점은 클래스들이 추상화에 의존하기 때문에 얻어지는 장점이다.

     

    의도를 드러내는 이름 선택하기

    • 기존 Phone은 '일반 요금제'와 관련된 내용을 구현한다는 사실을 명시적으로 전달하지 못한다. 또한 AbstractPhone은 전화기를 포괄한다는 의미를 명확하게 전달하지 못한다. 따라서 이하와 같이 변경하는 것이 적절할 것이다.
    • AbstractPhone -> Phone
    • Phone -> RegularPhone
    • NighlyDiscountPhone -> 그대로

     

    이번에 '01'에서 봤던 것 처럼 세금 요구사항을 추가하는건 더 쉬워졌을까?

    public abstract class Phone {
        private double taxRate;
        private List<Call> calls = new ArrayList<>();
    
        public Phone(double taxRate) {
            this.taxRate = taxRate;
        }
    
        public Money calculateFee() {
            Money result = Money.ZERO;
    
            for(Call call : calls) {
                result = result.plus(calculateCallFee(call));
            }
    
            return result.plus(result.times(taxRate));
        }
    
        protected abstract Money calculateCallFee(Call call);
    }
    public class RegularPhone extends Phone {
        private Money amount;
        private Duration seconds;
    
        public RegularPhone(Money amount, Duration seconds, double taxRate) {
            super(taxRate);
            this.amount = amount;
            this.seconds = seconds;
        }
    
        @Override
        protected Money calculateCallFee(Call call) {
            return amount.times(call.getDuration().getSeconds() / seconds.getSeconds());
        }
    }
    public class NightlyDiscountPhone extends Phone {
        private static final int LATE_NIGHT_HOUR = 22;
    
        private Money nightlyAmount;
        private Money regularAmount;
        private Duration seconds;
    
        public NightlyDiscountPhone(Money nightlyAmount, Money regularAmount, Duration seconds, double taxRate) {
            super(taxRate);
            this.nightlyAmount = nightlyAmount;
            this.regularAmount = regularAmount;
            this.seconds = seconds;
        }
    
        @Override
        protected Money calculateCallFee(Call call) {
            if (call.getFrom().getHour() >= LATE_NIGHT_HOUR) {
                return nightlyAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
            } else {
                return regularAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
            }
        }
    }
    • 책임을 아무리 잘 분리하더라도 인스턴스 변수의 추가는 종종 상속 계층 전반에 걸친 변경을 유발한다.
    • 하지만 인스턴스 초기화 로직을 변경하는 것이 두 클래스에 동일한 세금 계산 코드를 중복시키는 것보다는 현명한 선택이다.
    • 객체 생성 로직의 변경에 유연하게 대응할 수 있는 다양한 방법이 존재한다 (8장, 9장 내용이다.)  따라서 객체 생성 로직에 대한 변경을 막기보다는 핵심 로직의 중복을 막아라. 핵심 로직은 한 곳에 모아 놓고 조심스럽게 캡슐화해야 한다. 그리고 공통적인 핵심 로직은 최대한 추상화해야 한다.
    • 상속으로 인한 클래스 사이의 결합을 피할 수 있는 방법은 없다. 상속은 어떤 방식으로든 부모 클래스와 자식 클래스를 결합시킨다.

     


    04 차이에 의한 프로그래밍

    기존 코드와 다른 부분만을 추가함으로써 애플리케이션의 기능을 확장하는 방법을 차이에 의한 프로그래밍(programming by difference)이라고 부른다.

    • 차이에 의한 프로그래밍의 목표는 중복 코드를 제거하고 코드를 재사용하는 것이다.

     

    중복 코드는 악의 근원이다.

    • 코드를 재사용하기 위해서는 중복 코드를 제거해서 하나의 모듈로 모아야 한다.
    • 중복 코드 제거와 코드 재사용은 동일한 행동을 가리키는 서로 다른 단어다.
    • 중복 코드를 제거하기 위해 최대한 코드를 재사용해야 한다.

     

    상속 (Inheritance)

    • 객체지향에서 중복 코드를 제거하고 코드를 재사용할 수 있는 가장 유명한 방법이다.

    • 상속은 너무나도 매력적이기 때문에 객체지향에 갓 입문한 프로그래머들은 이에 도취된 나머지 모든 설계에 상속을 적용하려고 시도하곤 한다.
    • 상속이 코드 재사용이라는 측면에서 매우 강력한 도구이지만, 강력한 만큼 잘못 사용할 경우 돌아오는 피해 역시 크다.
    • 상속의 오용과 남용은 애플리케이션을 이해하고 확장하기 어렵게 만든다.
    • 정말로 필요한 경우에만 상속을 사용하라.

     

    합성 (Composition)

    • 상속은 코드 재사용과 관련된 대부분의 경우에 우아한 해결 방법이 아니다.
    • 상속의 단점을 피하면서도 코드를 재사용할 수 있는 더 좋은방법이 합성이다.
    • ☆ 11장에서 살펴볼 내용임.

     

    댓글