티스토리 뷰

Backend/Java

상속은 정말 나쁜가?

ellie.strong 2022. 3. 17. 00:09
728x90

우아한테크코스 강의 중 네오께서 해주신 "상속", "클래스와 인스턴스(심화)" 강의 중 일부를 정리하면서 추가로 학습한 내용과 나의 소소한 생각을 작성하였다.

 

"상속보다는 컴포지션을 사용하라", "상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라", "추상 클래스보다는 인터페이스를 우선하라" 등 이펙티브 자바 3/E에는 조슈아 블로크님의 상속에 대한 부정적인 의견들이 가득하다. 상속은 정말 나쁜가?

 

상속은 죄가 없다!! 내가 잘 못 사용하고 있을 뿐!!

상속은 코드를 재사용하는 강력한 수단이지만 항상 최선의 선택이 되는 것은 아니다. 상속은 여러가지 단점을 가지지만, 근본적인 원인은 상속 그 자체가 아니라 상속을 잘 못 사용하고 있는 우리들이다. 상속을 잘 못 사용할 경우 큰 부작용이 따라올 수 있기 때문이다. 따라서 상속을 무조건 사용하지 말아야지!!라는 생각보다는 상속을 올바르게 사용하자!!라는 생각을 가지고 여기서 상속을 쓰는게 맞나?를 항상 의심하면서 개발하자. 

 

상속의 단점

1. 안보이는 코드(상위 클래스의 코드)에 대해서 추측을 해야한다.

또한 하위 클래스에서는 상위 클래스의 코드가 보이지 않으므로 코드를 읽거나 변수, 메서드를 찾을 때 불편할 수 있다. 
public class Document {
    public int length() {
        return this.content().length;
    }

    public byte[] content() {
        // 문서의 내용을
        // 바이트 배열로 로드한다
    }
}
public class EncryptedDocument extends Document {
    @Override
    public byte[] content() {
        // 문서를 로드해서,
        // 즉시 복호화하고,
        // 복호화한 내용을 반환한다.
    }
}

Document를 상속한 EncryptedDocument에서 length() 메서드를 호출한다면 암호화 전의 길이를 반환해야할까 암호화 후의 길이를 반환해야할까? 정확히 명시된 문서가 없다면 확신할 수 없다. 

 

클래스와 메서드를 final이나 abstract 둘 중 하나로 제한한다면 문제가 발생할 가능성을 없앨 수 있다. 좋은 객체의 7가지 덕목에서는 좋은 객체를 만들기 위해서는 클래스에 무조건 final이나 abstract를 지정해야한다고 한다. 여기서 final 클래스는 상속을 통해 확장할 수 없는 클래스를 말하며, abstract 클래스는 인스턴스를 가질 수 없는 클래스로 불완전함을 명시한다. 

 

Document를 final로 지정하면 상속이 불가능해진다. 반대로 Document를 abstract로 지정하고, length() 메서드를 final, content() 메서드를 abstract로 지정한다면 Document 안에서 content() 메서드를 구현할 수 없기 때문에 혼란스러운 없이 length() 메서드를 이해할 수 있다. 

 

2. 캡슐화를 깨뜨린다.

캡슐화 : 만일의 상황(타인이 외부에서 조작)에 대비해 외부에서 특정 속성이나 메서드를 사용할 수 없도록 숨겨놓는 것
상위 클래스가 어떻게 구현되느냐에 따라 하위 클래스의 동작에 이상이 생길 수 있다.
하위 클래스에서 상위 클래스의 구현을 알아야하는 경우가 생긴다.  
public class LottoNumbers extends HashSet<LottoNumber> {
    private int addCount = 0;

    @Override
    public boolean add(final LottoNumber lottoNumber) {
        addCount++;
        return super.add(lottoNumber);
    }

    @Override
    public boolean addAll(final Collection<? extends LottoNumber> c) {
        addCount += c.size();
        return super.addAll(c);
    }

    public int getAddCount() {
        return addCount;
    }
}
@Test
void addAll() {
    LottoNumbers lottoNumbers = new LottoNumbers();
    lottoNumbers.addAll(List.of(LottoNumber.valueOf(1), LottoNumber.valueOf(2)));

    assertThat(lottoNumbers.getAddCount()).isEqualTo(2); // Error : it was 4
}

 

HashSet을 상속한 LottoNumbers에서 addAll()을 호출해 2개의 로또 넘버를 추가한 후 getAddCount()를 한다면 2가 반환되야한다고 생각할 수 있다. 하지만 이 테스트코드는 4를 반환한다고 알려주며 실패한다. 무슨일일까?

public boolean addAll(Collection<? extends E> c) {
    boolean modified = false;
    for (E e : c)
        if (add(e))
            modified = true;
    return modified;
}

HashSet의 addAll() 메서드의 구현 코드를 살펴보면 add()를 반복해서 호출하기때문에 결과적으로 4가 됨을 알 수 있다. 이렇게 상위 클래스의 내부 구현을 알아야지만 하위 클래스를 제대로 사용할 수 있는 경우가 생긴다. 

 

이런 경우 하위 클래스에서 addAll()을 원하는 방식으로 재정의해 사용할 수도 있지만, 상위 클래스의 메서드 동작을 다시 구현하는 것은 매우 어렵고 시간도 많이 걸리며 자칫 오류를 내거나 성능을 떨어뜨릴 수도 있기 때문에 주의해야한다. 

 

추가적으로 상위 클래스의 내부 구현이 달라지면 하위 클래스가 오작동할 수 있으며, 그럴 경우 하위 클래스 전체를 변경시켜줘야할 수 있다.

만약 상위 클래스에 새 메서드가 추가되었는데, 운 없게도 우리가 하위 클래스에 추가했던 메서드와 시그니처가 같다면 문제가 발생한다. 

 

이렇게 상속은 하위 클래스가 상위 클래스에 강하게 의존, 결합하게 하기 때문에 오히려 변화에 유연하게 대처하기 어려워질 수 있다. 이는 상속구조가 깊어질수록 문제가 더 심각해질 수 있다. 

 

상속이 적절한 경우

상속이 적절한 경우는 클래스의 행동을 확장(extend)하는 것이 아니라 정제(refine)할 때이다. 여기서 확장이란 새로운 행동을 덧붙여 기존의 행동을 부분적으로 보완하는 것을 의미하고, 정제란 부분적으로 불완전한 행동을 완전하게 만드는 것을 의미한다. 

상속을 사용하려는 자신에게 할 수 있는 질문

✅  하위 클래스가 상위 클래스의 진짜 하위 타입인 상황인가? (is-a 관계)

Dog is an Animal (O)
WinningNumber is a LottoNumbers (X)

 상속으로 인해 내부 구현을 불필요하게 노출하고 있는 것은 아닌가?

HashSet의 addAll()의 내부 구현을 알아야하는 것 처럼

  확장하려는 클래스에 결함이 있다면 결함이 내 클래스까지 전파돼도 괜찮은가?

하위 클래스는 상위 클래스의 결함까지 모두 상속하기 때문!!

 

객체 지향 초기에 가장 중요시 여기는 개념은 재사용성(reusability)이었지만, 지금은 워낙 시스템이 방대해지고 잦은 변화가 발생하다 보니 유연성(flexibility)이 더 중요한 개념이 되었다. 그러면서 자연스럽게 상속을 지양하는 흐름으로 바뀌어가고 있는게 아닐까..?

 

상속은 재사용을 위한 절차지향 기술이다. 

Implementation inheritance is much closer to a different meaning: “Receive (money, property, or a title) as an heir at the death of the previous holder.” Who is dead, you ask? An object is dead if it allows other objects to inherit its encapsulated code and data.

마지막으로 상속을 아예 사용하지 말자는 글(Inheritance Is a Procedural Technique for Code Reuse)을 소개하며 마치려한다. (한마디로 코드에 final 클래스만 존재해야한다.) 이 글에서는 상속은 그저 재사용을 위한 절자치향 기술일 뿐이며, 이는 객체지향을 망가뜨린다는 이야기를 한다. 상속 자체가 나쁘다!! 여기서의 상속도 마찬가지로 구현 상속을 의미한다. 공감가는 부분이 많았지만, 학습을 하는 내 입장에서는 상속과 조합 등 여러 방면에 대해 오픈된 마인드를 가지고 많은 실패와 경험을 쌓아야한다고 생각한다. 앞으로 우테코 생활동안 나만의 생각을 적립해나가고 싶다 👍

 

나도 언젠가 내 생각을 말하고 싶어!!

추가적으로 오늘 워니조 크루들과 함께 게더에 있게되면서 여러 크루들과 "상속 vs. 인터페이스 vs. 조합"을 주제로 이야기를 나눌 수 있는 기회가 있었다. (솔직히 나는 아직 이에 대한 내 의견이 분명하지 않아 거의 듣기만 했지만.. 🥲 ) 결론적으로 어떤 게 무조건 좋아!!가 아니라, 각자가 어떤 상황에 적합한지, 어떤 상황에서 나쁜지를 알고있어야하며 코드에 다양하게 적용해보며 실제로 느껴보는 것이 좋다고 브라운이 말해주셨다. 브멘.. 🙇‍♀️ 나도 이 부분에 대해 이론적으로만 다가가는게 아니라 실제로 내 코드에 적용해보면서 내가 느끼는 생각들을 정리해보는 시간을 가져야겠다 👍

 

Ref.

https://tecoble.techcourse.co.kr/post/2020-05-18-inheritance-vs-composition/

https://codingnuri.com/seven-virtues-of-good-object/

 

 

728x90
댓글
공지사항
최근에 올라온 글