본문 바로가기
개발 관련 강의 정리/10분 테코톡

[10분 테코톡] 클레이의 상속과 조합 정리

by 코딩개발 2023. 5. 25.
728x90
반응형

상속?

- 기존에 정의되어 있는 클래스의 필드와 메소드를 물려받아 새로운 클래스를 생성하는 기법

- 중복코드 제거와 기능 확장을 쉽게 할 수 있다.

- 클래스들의 계층적인 구조를 만들 수 있다.

 

상속의 대표적인 문제점

하위 클래스가 상위 클래스의 구현에 의존하기 때문에 변경에 취약하다.

상위 클래스릐 모든 퍼블릭 메서드가 하위 클래스에도 반드시 노출된다.

 

 

취약한 기반 클래스 문제 예시

class Lotto {
    protected LottoNumber[] numbers;

    public Lotto(LottoNumber[] numbers) {
        this.numbers = numbers;
    }

    public boolean contains(LottoNumber lottoNumber) {
        return Stream.of(numbers).anyMatch(number -> number.equals(lottoNumber));
    }
}
public class WinningLotto extends Lotto {
    private final LottoNumber bonusNumber;

    public WinningLotto(LottoNumber[] numbers, LottoNumber bonusNumber) {
        super(numbers);
        this.bonusNumber = bonusNumber;
    }

    public long getMatchCount(Lotto lotto) {
        return Stream.of(numbers)
                .filter(lotto::contains)
                .count();
    }
}


로또 번호 배열을 가지는 Lotto 클래스와 당첨 로또 번호 클래스를 위와 같이 만들었다. 이때 리뷰어가 Lotto 클래스를 보고 'Lotto 클래스에 필드로 선언된 로또 번호 배열을 로또 번호 리스트로 수정해라' 라고 피드백을 주었다고 가정한다. 수정하기 위해 Lotto 클래스의 로또 번호 배열을 로또 번호 리스트로 수정하고 Lotto 클래스의 다른 메서드들도 로또 리스트를 활용할 수 있도록 아래와 같이 수정하였다.

 

class Lotto {
    protected List<LottoNumber> numbers;

    public Lotto(List<LottoNumber> numbers) {
        this.numbers = new ArrayList<>(numbers);
    }

    public boolean contains(LottoNumber lottoNumber) {
        return this.numbers.contains(lottoNumber);
    }
}


그런데 피드백을 받고 수정한 부분은 Lotto 클래스인데 WinningLotto 클레스에 컴파일 에러가 생겼다.


무슨 일일까?
WinningLotto 클래스는 부모인 Lotto 클래스와 강하게 의존하며 캡슐화가 지켜지지 않고 있다. 그래서 Lotto 클래스만 수정하였는데  WinningLotto 클래스도 수정해야 되는 일이 생기게 되었다. 이처럼 상속은 부모 클래스와 강하게 의존하고 부모 클래스의 캡슐화를 해치고 결합도가 높아진다. 그래서 부모 클래스의 구현을 변경하면 많은 자식 클래스를 모두 변경해 줘야 하는 상황이 발생할 수도 있다.

 


불필요한 메서드도 상속받는 예시

Stack<String> stack = new Stack<>();
stack.push("첫번째 문자열");
stack.push("두번째 문자열");
stack.add(0, "세번째 문자열");
assertEquals(stack.pop(), "세번째 문자열"); // 불일치 (두번재 문자열)


위와같이 문자열을 저장하는 Stack을 선언하고 Stack에서 지원하는 메서드로 세 가지 문자열을 넣어주었다. Stack에서 문자열을 하나를 꺼내면 마지막에 넣은 문자열이 반환될 것이라고 예상할 수 있지만 실제 실행 결과는 예상과는 다르게 2번째에 넣은 문자열이 반환되었다.

 

왜 그럴까?

세번째 문자열을 Stack에 집어넣는 add 메서드가 범인이었다. add 메서드는 Stack의 규칙을 따르지 않는다.

왜 Stack의 규칙을 망가뜨리는 메서드를 Stack에서 public으로 지원하고 있을까?
답은 Vector에 있다.

 

먼저 자바 API에서 지원하는 Stack 클래스는 Vector 클래스를 상속받고 있다.

 

그리고 Vector의 add 메서드가 공개되어 있어서 Vector를 상속한 Stack은 자신에게 필요하지 않더라도 Vector의 add 메서드를 공개할 수밖에 없다.

 

이처럼 상속은 자식클래스 입장에서 불필요한 부모 클래스의 public 메서드를 어쩔 수 없이 노출하게 되고 공개된 부모 클래스의 public 메서드가 자식 클래스의 내부 규칙과 맞지 않아도 어쩔 수 없이 공개될 수 밖에 없다.

 

 

조합

- 전체를 표현하는 클래스가 부분을 표현하는 객체를 포함해서 부분 객체의 코드를 재사용하는 방법

 

조합은 상속의 문제점을 상당 부분 해결할 수 있다.

 

 

조합의 장점

- 상속과 달리 재사용되는 객체 내부 구현이 공개되지 않는다.
- 메서드를 호출하는 방식으로 public 인터페이스에 의존하기 때문에 부분 객체의 내부 구현이 변경되어도 비교적 안전하다.
- 조합된 객체의 모든 public 메서드를 공개하지 않아도 된다.

 

 

조합 사용 방법

- 조합하려는 부분 클래스의 인스턴스를 새로운 클래스의 private 필드로 참조하고 그 인스턴스의 메서드를 호출하는 방식으로 기능을 구현

 

public class WinningLotto {
    private final Lotto lotto;
    private final LottoNumber bonusNumber;

    public WinningLotto(Lotto lotto, LottoNumber bonusNumber) {
        this.lotto = lotto;
        this.bonusNumber = bonusNumber;
    }

    public long getMatchCount(Lotto other) {
        return lotto.getMatchCount(other);
    }
}

 

먼저 상속의 문제점을 드러냈던 WinningLotto의 문제를 조합으로 바꾸면 어떻게 될까?
Lotto를 상속 받지 않고 Lotto를 필드로 추가하여 Lotto가 배열형식으로 되어 있든 리스트 형식으로 되어 있든 WinningLotto는 알지 못한다. 그리고 상속은 부모의 구현에 의존하지만 조합은 부분 객체의 public 인터페이스에 의존하므로 부분 객체의 내부 구현이 변경 되어도 영향을 받지 않는다.

 

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

    public void push(T t) {
        vector.add(t);
    }

    public T pop() {
        if (vector.size() == 0) {
            throw new IllegalArgumentException();
        }
        return vector.remove(vector.size() - 1);
    }
}


다음으로 불필요한 메서드가 상속되었던 문제를 조합으로 바꾸면 어떻게 될까?
위와같이 Stack의 부모 클래스였던 Vector를 필드로 조합하여 상속과 다르게 Vector의 모든 public 메서드를 드러내지 않아도 된다.

 

그렇다면 상속을 사용하지 말하야 할까?

아니다. 상속을 사용하려고 할 때 내가 어떤 이유로 상속을 사용하는지 확실하게 알고 사용해야 한다

 

 

상속의 목적

서브타이핑  다형적인 계층구조 구현 부모와 자식 행동이 호환
서브클래싱  다른 클래스의 코드를 재사용 부모와 자식 행동이 호환X


일반적으로 지금까지 알아본 상속의 문제는 코드를 단순 재사용 하려는 서브 클래싱을 했기 때문에 생겼다. 위에 예시로 들었던 Vector와 Stack의 경우 클라이언트 입장에서 두 객체에게 기대하는 행동이 서로 다른다. Vector는 자주 사용하는 ArrayList처럼 행동해주길 기대하고, Stack은 Stack만의 규칙에 맞게 행동하기를 바란다. 그런데 단순 코드 재사용을 위해서 상속을 하다 보면은 서로 다르게 행동하는 객체들이 서로 강하게 결합되고 불필요한 행동도 노출하게 된다.

 


잘못된 상속을 예방하기 위한 방법

상속을 고려하는 두 객체가 서로 IS-A 관계인가?

클라이언트 관점에서 두 객체가 동일한 행동을 할 것이라 기대하는가?

위 두 가지 질문에 모두 '예' 라고 답할 수 있는 경우에만 상속을 고려해야 한다.

 


정리

'단순히 코드를 재활용하고 싶고 중복코드를 없애고 싶다'  ---> 조합 고려
'클라이언트 입장에서 동일하게 행동하는 인스턴스를 그룹화 하고 싶다. 즉, 다형성을 고려하고 싶다' ---> 상속 고려


참고

https://www.youtube.com/watch?v=U4OSS4jJ9ns&ab_channel=%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC 

728x90
반응형

댓글