[객체지향] extends 보단 implements

130moodTRBL·2023년 1월 24일
0

객체지향

목록 보기
1/1
post-thumbnail

다른 사람의 코드를 보다보면 대부분의 클래스들이 죄다 implements가 사용된 것을 볼 수 있다.
여기저기서 합성이 상속보다 좋다고도 말은 하지만 정확하게 어떤 이유에서 상속이 좋지 않은지는 햇갈리곤한다. 예시를 통해 알아보고 더 좋은 설계가 되도록 고쳐보자.

불필요한 인터페이스 상속

자바 초기버전의 Stack을 살펴보자.
StackVector의 삭제, 삽입을 재사용하기 위해 Vector를 상속받아 사용했다. 하지만 Vector를 상속받아 사용하였기 때문에 Vector의 요소를 Stack이 사용가능하게 되었다.

public class Example {
    public static void main(String[] args) {
        Stack<Integer> stk = new Stack<>();
        stk.push(1);
        stk.push(2);
        stk.push(3);

        stk.add(0, 4);
        System.out.println(stk.pop());
    }
}

Stack의 동작방식을 고려한다면 답은 4가 되겠지만 3이 출력된다.
단순히 add를 사용하지 않으면 되지 않은가? 라고 생각할 수 있지만 인터페이스는 이상하게 사용하기는 어렵게, 제대로 사용하기에는 쉽게 설계해야한다.

상속보다 합성관계로 바꾸어보자 Vector를 상속받지 말고 인스턴스 변수로 두어서 사용해보자

public class Stack <T> {
    private Vector<T> vector = new Vector<>();

    public T puss(T ele) {
        vector.addElement(ele);
        return ele;
    }

    public T pop(T ele) {
        if(vector.isEmpty()) {
            throw new EmptyStackException();
        }
        return vector.remove(vector.size() - 1);
    }
}

이제 Vector의 오퍼레이션이 포함되지 않아 원하는 방식으로 Stack이 동작한다.

메서드 오버라이딩의 오작동

InstrumentedHashSetHastSet내부의 저장된 요소의 개수를 새는 기능을 추가한 클래스이다.

public class InstrumentedHashSet<E> extends HashSet<E> {
    private int addCount = 0;
    
    @Overide
    public boolean add(E e) {
    	addCount++;
        return super.add(e);
    }
    
    @Overide
    public boolean addAll(Collection<? extends E> c) {
    	addCount += c.size();
        return super.addAll();
    }
}

하지만 다음과 같은 코드를 실행하면 문제가 생긴다.

InstrumentedHashSet<String> arr = new InstrumentedHashSet<>();
arr.addAll(Arrays.asList("um", "jun"));

아마 위의 코드에서 카운트는 2가 되는것이 의도된 작동이겠으나 4가 나온다.

  • addAll을 호출하면 배열의 개수만큼 카운트가 추가됨
  • super.addAll()을 출력한 순간 HashSet에서 자체적으로 add를 실행
  • 다시 2가 더해지게 되며 4가 출력

Stack의 예시와 마찬가지로 ashSet를 내부 인스턴스로 포함시켜 구현하는 방법을 생각할 수 있다.
하지만 InstrumentedHashSeHashSet이 제공하는 오퍼레이션을 그대로 이용해야 하기 때문에 합성을 사용해준다.
HashSet과의 결합도는 줄여주고 HashSet이 제공하는 인터페이스는 유지할 수 있다.
HashSetSet의 구현체이므로 Set과 합성하도록 한다.

public class InstrumentedHashSet<T> implements Set<T> {
    private int addCount = 0;
    private Set<T> set;

    public InstrumentedHashSet(Set<T> set) {
        this.set = set;
    }

    @Overide
    public boolean add(E e) {
        addCount++;
        return set.add(e);
    }

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

    ...overide methods...
}

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

public class Song {
    private String singer;
    private String title;

    public Song(String singer, String title) {
        this.singer = singer;
        this.title = title;
    }

    ... getter, setter
}
public class PlayList {
    private List<Song> tracks = new ArrayList<>();

    public void append(Song song) {
        getTracks().add(song);
    }

    public List<Song> getTracks() {
        return tracks;
    }
}
public class PersonalPlaylist extends PlayList {
    public void remove(Song song) {
        getTracks().remove(song);
    }
}

만약 PlayList에서 노래의 제목도 관리해야 한다면 PlayListappend만 수정하는 것이 아니라 PersonalPlaylistremove또한 수정해야 할 것이다.
이 예시는 간단한 편이기 때문에 "그정도 수정이야"라고 생각할 수 있지만 좀 더 복잡한 개발을 하다 보면 변경의 파급효과는 하루종일 수정을 해야할 정도로 엄청나다는 것을 느낄 수 있을 것이다.

이 예시는 합성을 사용하더라도 근본적인 문제는 해결되지 않을 것이다. 하지만 합성을 사용하는것이 PlayList의 구현을 변경하더라도 파급효과들을 내부로 캡술화할 수 있으므로 합성을 사용하는 것이 더 좋다.

기본적으로 상속은 자식 클래스와 부모 클래스의 결합도를 높인다. 이로 인해 자식 클래스는 필요 없는 세부사항까지 알아야 하고 부모 클래스의 변경사항이 자식 클래스까지 침범하게되는 일이 발생한다.
한마디로 강한 결합도 때문에 문제가 발생하는 것이다.

0개의 댓글