티스토리 뷰

여기서 상속은 인터페이스 상속을 제외한 구현상속을 의미한다.

상속을 사용할 때 고려햐야할 것(위험성)

  • 상속은 같은 프로그래머가 통제하는 패키지에서는 안전하다. 즉, 외부에서 널리 사용(배포)된다면 상속은 위험 가능성이 많다.
  • 상속은 캡슐화를 깨뜨린다. 하부 클래스에서 상위 클래스의 메소드를 제대로 사용하려면 상위 클래스의 메소드 구현을 잘 알고 있어야 한다. 상위 클래스 메소드 내부에서 호출하는 메소드가 무엇인지 알고 있어야 잘못된 사용을 막을 수 있다. (책에서는 HashSet의 addAll을 예시로 든다)
public class InheritanceSet<E> extends HashSet<E> {
    private int addCount = 0;

    public InheritanceSet(){

    }

    @Override
    public boolean add(E e){
        addCount++;
        return super.add(e);
    }

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

    public int getAddCount(){
        return addCount;
    }
}


//Test
public class InheritanceTest {

    @Test
    @DisplayName("상속")
    void inheritance() {
        final InheritanceSet<Integer> inheritanceSet = new InheritanceSet<>();
        inheritanceSet.addAll(List.of(1,2,3));

        assertThat(inheritanceSet.getAddCount()).isEqualTo(3); // [fail] but was : 6
    }
}
/*

- HashSet 계층구조
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable

- AbstractSet 계층구조
public abstract class AbstractSet<E> extends AbstractCollection<E> implements Set<E>

*/

// AbstractCollection 내부
public abstract class AbstractCollection<E> implements Collection<E> {
    // ...
    public boolean addAll(Collection<? extends E> c) {
        boolean modified = false;
        for (E e : c)
            if (add(e)) 
            // 여기서 add메소드를 호출한다. 즉 InheritanceSet에서 @Override 된 add를 다시 호출한다.
                modified = true;
        return modified;
    }
    // ...
}
  • 상속은 계층구조이므로, 상위 클래스에 대해 제대로 파악되어 있어야 하위 클래스에서 계약(규약)을 만족할 수 있다. 즉, 상위 클래스에서 요구하는 규칙을 만족하지 못할 가능성이 크다.
  • 메소드 재정의로 인한 문제가 다수 발생할 가능성이 있다.
  • 자식 클래스에서 재정의를 하지 않고 새로운 메소드를 추가하더라도 상위 클래스와 시그니처가 같고 반환타입이 다르면 컴파일이 되지 않는다.
  • 상위 클래스의 결함까지 하위 클래스에 그대로 영향을 미친다.
  • is-a 관계. 즉, 계층구조가 확실할 경우에만 사용해야하며 확장을 고려하여 설계된 경우에만 사용해야 한다. 확실하지 않다면 조합과 전달을 사용하는 것이 좋다. 명확하게 상속을 사용해야만 하는 이유가 있지 않은 이상 위험성이 크기 때문에 상속을 사용하지 않는 것이 좋다.
  • 상속을 고려해 설계화하였다면 문서화 하여야 한다. 상속을 고려하지 않았다면 final 선언으로 상속을 금지하게 하는 것이 좋다. 상속을 금지하는 방식에는 클래스의 final 선언 또는 private 생성자로 선언하는 방식이 있다. (좋은 객체의 7가지 덕목에는 클래스가 final 또는 abstract 선언되어 있어야 한다고 한다. 즉, 상속을 고려했는지 아닌지를 명확히 하는 것이 좋다는 의미로 받아들였다.)

상속보다 조합을 사용하라

  • 조합(Composition) : 기존 클래스가 새로운 클래스의 구성요소로 사용한다.
  • 전달 : 새 인스턴스 메소드들은 기존 클래스와 대응하는 메소드를 호출하여 결과를 반환한다.
// 조합을 사용한 클래스 (래퍼 클래스)
public class CompositionSet<E> extends ForwardingSet<E> {
    private int addCount = 0;

    public CompositionSet(Set<E> s){
        super(s);
    }

    @Override
    public boolean add(E e){
        addCount++;
        return super.add(e);
    }

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

    public int getAddCount(){
        return addCount;
    }
}

// 전달클래스

public class ForwardingSet<E> implements Set<E> {
    private final Set<E> s;

    public ForwardingSet(Set<E> s) {
        this.s = s;
    }

    // 모든 Set 인터페이스의 메소드들을 구체화 ...
    @Override
    public boolean add(final E e) {
        return s.add(e);
    }
    @Override
    public boolean addAll(final Collection<? extends E> c) {
        return s.addAll(c); // 생성자로(조합을 사용하여) 초기화된 Set의 addAll을 호출
    }
    // ...
}

// Test
{
    @Test
    @DisplayName("조합")
    void composition() {
        final CompositionSet<Integer> compositionSet = new CompositionSet<>(new HashSet<>());
        compositionSet.addAll(List.of(1,2,3));

        assertThat(compositionSet.getAddCount()).isEqualTo(3); // [Passed]
    }
}
  • 위와 같이 인터페이스를 구현한 전달 클래스를 사용하면, 하위 클래스에서 재정의한 메소드의 내용으로만 작동할 수 있게 된다.

  • 실질적인 구체화는 래퍼 클래스가 하게 되므로, 전달(Forwading) 클래스는 재사용이 가능하다.
  • 위의 예시에서 ForwadingSet은 Set 인터페이스의 모든 메소드를 오버라이드하게 되어있으므로 그것을 상속한 compositionSet.size()를 호출이 가능하다. 하지만, CompositionSet에서 재정의 하지 않았으므로 size는 0이 나오게 된다.

    // Test
    {
      @Test
      @DisplayName("조합")
      void composition() {
          final CompositionSet<Integer> compositionSet = new CompositionSet<>(new HashSet<>());
          compositionSet.addAll(List.of(1,2,3));
    
          assertThat(compositionSet.size()).isEqualTo(3); //  [fail] but was : 0
      }
    }
    

추상클래스보다는 인터페이스를 우선하라

  • 계층구조에서 벗어난 타입을 정의할 수 있다. 
  • 인터페이스를 구현하는 것은 인스턴스가 어떤 일을 할 수 있는지 알려주는 것이다. 해당 기능을 할 수 있는 타입을 정의하는 용도로만 사용해야 한다.
  • 인터페이스는 믹스인(mixin) 정의에 알맞는다. 즉, 부가적이고 선택적인 행위의 추가가 가능하다.
  • 3가지 기능(1. 걷기, 2. 달리기, 3. 날기)을 조합하는 계층구조를 만드려고 한다면, 1,2,3,1&2, 2&3, 1&3, 1&2&3 등 수 많은 클래스가 만들어져야 하지만, 인터페이스에서는 해당 기능을 넣고 빼는 기능만 하면 된다.
  • 인터페이스에서 반복되는 구현으로 발생되는 코드의 중복을 줄이고 싶다면, default 메소드를 사용하거나 abstract 클래스로 골격 구현 클래스를 제공하는 경우도 있다.

정리

상속을 사용한다는 것은 계층구조를 만든다는 것이다. 따라서 부모의 영향을 모든 자식들이 받게 되고 자식들은 부모의 룰을 잘 알고 있어야 한다. 제약사항이 상당히 많고, 제약사항이 많다는 것은 모든 제약사항을 만족시키기 어렵다는 것이다. abstract class는 what object is에 초점을 맞춘다.

 

인터페이스는 계층구조에서 벗어난다. 객체가 할 수 있는 여러가지 기능들을 부여할 수 있다. 그 기능은 각각의 객체가 구현한다. 더 유연한 사용이 가능하다. interface는 what object can do에 초점을 맞춘다.

 

예전의 객체지향에서는 재사용성에 포커스를 맞췄다. 따라서 상속을 통한 코드의 중복제거가 효과적으로 다가왔을 것이다. 하지만, 현재는 객체지향의 트렌드가 변화하여 유연성에 포커스가 맞춰지고 있다고 한다. 다소 반복이 생겨도 성능에 문제가 없을만큼 하드웨어의 기능이 좋아졌기 때문에 얼마나 변화에 유연한지에 포커스가 맞춰지고 있는 것이다. 전체적인 내용은 상속을 사용하지 말자가 아니라 상속을 잘 쓰기 상당히 어려우니 잘 생각하고 사용해야 한다는 것이다. 명확한 이유가 없다면 좀 더 안전하게 사용할 수 있는 인터페이스나 조합을 사용하는 것이 좋다.

참고

이팩티브 자바: 아이템 18, 19, 20, 22

우아한테크코스 수업

좋은 객체의 7가지 덕목 번역본 

stackoverflow - abstract class VS interface

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함