# Chapter04 재사용, 상속보단 조립

# 목표

개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴 스터디를 진행하며 공부한 내용을 정리한다.

# 1. 상속과 재사용

Java에는 데이터 군을 저장하는 클래스를 표준화한 설계인 Collections Framework를 가지고 있다. 이러한 Collections Framework는 다수의 데이터를 쉽고 효과적으로 처리할 수 있는 표준화된 방법을 제공한다.

Collections Framework는 하위 클래스가 상위 클래스의 기능을 확장하고 있다.

상속을 사용하면 쉽게 다른 클래스의 기능을 재사용하면서 기능을 확장할 수 있다. 하지만 이러한 상속은 변경의 유연함에서 치명적인 단점을 가지고 있다.

# 1.1 상위 클래스 변경의 어려움

상속은 상위 클래스의 변경을 어렵게 만든다. 상속 계층을 따라 상위 클래스의 변경이 하위 클래스에 영향을 주기 때문에 최악의 경우 모든 하위 클래스에 영향을 줄 수 있다. 즉 상속은 상위 클래스와 하위 클래스 사이의 강한 결합도를 만든다.

# 1.2 클래스의 불필요한 증가

유사한 기능 확장 과정에서 클래스의 개수가 불필요하게 증가할 수 있다. Java의 경우 다중 상속을 지원하지 않기 때문에 필요한 기능의 조합이 증가할수록 상속을 통한 재사용은 클래스의 개수 증가로 이어진다.

# 1.3 상속의 오용

이것을 보여주는 가장 좋은 예시는 Stack이다. StackVector 클래스를 상속하여 기능을 확장하였다. 아래는 실제 Stack의 내부 구현 코드이다.

public class Stack<E> extends Vector<E> {
   
    public Stack() {
    }

    public E push(E item) {
        addElement(item);

        return item;
    }

    public synchronized E pop() {
        E       obj;
        int     len = size();

        obj = peek();
        removeElementAt(len - 1);

        return obj;
    }

    public synchronized E peek() {
        int     len = size();

        if (len == 0)
            throw new EmptyStackException();
        return elementAt(len - 1);
    }

    public boolean empty() {
        return size() == 0;
    }

    public synchronized int search(Object o) {
        int i = lastIndexOf(o);

        if (i >= 0) {
            return size() - i;
        }
        return -1;
    }

    private static final long serialVersionUID = 1224463164541339165L;
}

StackLIFO(Last in First Out) 구조로 push를 통해 데이터를 삽입하고 pop을 통해 꺼낸다. 하지만 Java의 Stack은 큰 문제점을 가지고 있다. 바로 상속한 Vector에 있는 메서드를 사용할 수 있다는 것이다. Vector는 Array를 기반으로 데이터를 삽입, 삭제, 변경 등의 행위를 유연하게 할 수 있도록 구현된 자료구조이다.

이것은 Stack의 의도와 다르게 동작한다. 이렇게 상속의 오용이 발생하는 이유는 Stack은 Vector와 Is-A 관계과 성립하지 않기 때문이다. 이렇게 같은 종류가 아닌 클래스의 구현을 재사용하기 위해 상속을 받게 되면 잘못된 사용으로 인한 문제가 생긴다.

# 2. 조합을 이용한 재사용

객체 조합(composition)은 여러 객체를 묶어서 더 복잡한 기능을 제공하는 객체를 만들어낸다. 조합은 보통 다른 객체를 참조하는 방식으로 구현된다. 아래는 당첨 번호를 관리하기 위한 WinningNumber이다.

public class WinningNumber {

    private static final String DUPLICATED_WINNING_NUMBER_ERROR_MESSAGE = "로또 번호는 중복될 수 없습니다.";

    private final LottoTicket lottoNumbers;
    private final LottoNumber bonusNumber;

    public WinningNumber(LottoTicket lottoNumbers, LottoNumber bonusNumber) {
        this.lottoNumbers = lottoNumbers;
        validateDuplicateBonusNumber(lottoNumbers, bonusNumber);
        this.bonusNumber = bonusNumber;
    }

    private void validateDuplicateBonusNumber(LottoTicket lottoNumbers, LottoNumber bonusNumber) {
        if (lottoNumbers.contains(bonusNumber)) {
            throw new IllegalArgumentException(DUPLICATED_WINNING_NUMBER_ERROR_MESSAGE);
        }
    }

    public Rank compare(LottoTicket lottoTicket) {
        return Rank.of(lottoNumbers.getSameNumberCount(lottoTicket), lottoTicket.contains(bonusNumber));
    }

    public Set<LottoNumber> getLottoNumbers() {
        return Collections.unmodifiableSet(lottoNumbers.getLottoNumbers());
    }

    public LottoNumber getBonusNumber() {
        return LottoNumber.from(bonusNumber.getLottoNumber());
    }
}

WinningNumber는 LottoTicket과 LottoNumber를 필드로 가지며 로또 번호에 대한 다양한 기능을 재사용하고 있다. 이러한 조합은 앞서 언급한 상속을 통한 재사용의 문제를 해소해준다.

또한 이러한 조합 방식은 런타임에 해당 객체를 교체할 수 있다. 상속은 소스 코드를 작성하는 컴파일 시점에 관계가 형성되기 때문에 런타임에 상위 타임을 교체할 수 없다.

정리하면 상속을 사용하다 보면 변경의 관점에서 유연함을 떨어뜨릴 수 있다. 즉 객체 조합을 먼저 고려해야 한다.

# 2.1 위임

위임(delegation)은 내가 할일 을 다른 객체에게 넘기는 것이다. 위 코드를 보면 LottoNumber에게 포함 여부를 물어보며 위임하고 있다.

if (lottoNumbers.contains(bonusNumber)) {
    throw new IllegalArgumentException(DUPLICATED_WINNING_NUMBER_ERROR_MESSAGE);
}

이러한 위임은 필드로 정의하지 않아도 된다. 다른 객체에게 내가 할 일을 넘기는 행위 이므로 객체를 새로 생성해서 전달해도 된다.

# 2.2 상속은 언제 사용하나

상속은 명확한 Is-A 관계에서 점진적으로 상위 클래스의 기능을 확장해 나갈 때 사용 할 수 있다.

# References

최범균 지음, 『개발자가 반드시 정복해야 할 객체지향과 디자인 패턴』, 인투북스(2014), p87-102.

#우아한테크코스 #개발자가 반드시 정복해야 할 객체지향과 디자인 패턴
last updated: 3/13/2022, 9:02:29 PM