상속과 조합

3 분 소요

업데이트:


상속의 목적은?

  • 하위 클래스에서 상위 클래스의 속성이나 행위를 재사용하고 확장하는 것!


상속의 단점은 무엇일까?

  • 상위 클래스 변경의 어려움
  • 클래스가 많아짐
  • 기능 오용의 가능성


1. 상위 클래스 변경의 어려움

  • Java의 ArrayList 클래스를 예로 들어보자.
  • ArrayList 클래스는 아래와 같은 클래스 구조를 갖는다.

  • 이러한 계층 구도에서 만약 AbstractCollection 클래스에 변경이 있다고 가정해보자.
  • 이럴경우, 다음과 같이 상위 클래스의 변경의 여파가 모든 하위 클래스로 전파될 것이다.

  • 이처럼, 상속은 하위 타입이 상위 타입의 동작 구조에 매우 밀접하게 엮이는 경향이 있다.
  • 둘 간의 결합도가 높아 서로를 강하게 의존하는 구조가 된다는 것이다.
  • 그래서 상위 타입의 구현 변경이 하위 타입에 영향을 줄 가능성이 매우 높아진다.
    • 결국, 캡슐화를 깨뜨릴 가능성이 높다.
      • 캡슐화란?
        • 내부 구현을 외부로부터 감추는 것.
        • 내부 구현의 변경에 따른 외부 영향을 최소화 하기 위함이 목적.
      • 내부 구현의 변경에 따른 외부 영향을 최소화하는 것이 캡슐화의 목적인데, 상속을 이용하면 이러한 목적을 이루지 못할 가능성이 크기 때문에 상속은 캡슐화를 깨뜨릴 가능성이 높은 것이다!


2. 클래스가 많아짐

  • 해당 그림에서 보는 것과 같이 상속 구조가 깊어질 수록 클래스가 점점 많아진다.


3. 기능 오용의 가능성

  • 다음과 같이 ArrayList를 상속받은 클래스가 있다고 가정해보자.

    package test;
      
    import java.util.ArrayList;
      
    class Luggage {
      
        private int size;
      
        public Luggage(int size) {
            this.size = size;
        }
      
        public int getSize() {
            return size;
        }
    }
      
    class MyBag extends ArrayList<Luggage> {
      
        private int maxAvailableSize;
        private int currentSize;
      
        public MyBag(int maxAvailableSize) {
            this.maxAvailableSize = maxAvailableSize;
        }
      
        public void put(Luggage luggage) {
      
            if (!canPutLuggage(luggage)) {
                throw new RuntimeException("가방에 짐을 넣을 수 있는 공간이 충분하지 않습니다!!!");
            }
      
            super.add(luggage);
      
            this.currentSize += luggage.getSize();
        }
      
        public void extract(Luggage luggage) {
      
            super.remove(luggage);
      
            this.currentSize -= luggage.getSize();
        }
      
        public boolean canPutLuggage(Luggage luggage) {
      
            return this.currentSize + luggage.getSize() <= this.maxAvailableSize;
        }
    }
      
    public class Test {
      
        public static void main(String[] args) {
      
            // 10만큼의 짐을 넣을 수 있는 가방 객체 생성
            MyBag bag = new MyBag(10);
      
            Luggage luggage8 = new Luggage(8);
            Luggage luggage2 = new Luggage(2);
            Luggage luggage1 = new Luggage(1);
      
            if (bag.canPutLuggage(luggage8)) {
                System.out.println("크기 8짜리의 짐을 넣습니다.");
                bag.put(luggage8);                  // 정상적인 사용
            }
      
            if (bag.canPutLuggage(luggage2)) {
                System.out.println("크기 2짜리의 짐을 넣습니다.");
                bag.add(luggage2);                  // 기능 오용, current size가 증가하지 않음
            }
      
            if (bag.canPutLuggage(luggage1)) {
                System.out.println("크기 1짜리의 짐을 넣습니다.");
                bag.put(luggage1);                  // 원래 해당 if문을 실행해선 안되지만 실행되는 상황
            }
        }
      
    }
    
  • ArrayList에 미리 구현돼있는 add 메서드를 오용하면서 원하는 결과를 얻지 못할 가능성이 생긴다.


조합

  • Composition
  • 조합을 이용하면 위에서 보았던 상속의 단점을 해결할 수 있다.
  • 조합은 다음과 같이 멤버필드로 다른 객체를 참조하는 방식이다.
      
    ...
      
    class MyBag {
      
        private int maxAvailableSize;
        private int currentSize;
        private List<Luggage> luggageList = new ArrayList<>();
      
        public MyBag(int maxAvailableSize) {
            this.maxAvailableSize = maxAvailableSize;
        }
      
        public void put(Luggage luggage) {
      
            if (!canPutLuggage(luggage)) {
                throw new RuntimeException("가방에 짐을 넣을 수 있는 공간이 충분하지 않습니다!!!");
            }
      
            luggageList.add(luggage);
      
            this.currentSize += luggage.getSize();
        }
      
        public void extract(Luggage luggage) {
      
            luggageList.remove(luggage);
      
            this.currentSize -= luggage.getSize();
        }
      
        public boolean canPutLuggage(Luggage luggage) {
      
            return this.currentSize + luggage.getSize() <= this.maxAvailableSize;
        }
    }
      
    ...
      
    
    • 위와 같이, 조합으로 클래스를 작성한다면 그 객체의 내부 구현를 신경 써야 한다거나, 변화에 오류가 생기는 일이 없어질 것이다.
      • 조합은 제공하는 기능을 사용하므로 기능의 내부 구현이 변경되더라도 영향을 받을 가능성이 줄어든다.
      • 즉, 캡슐화를 깨뜨릴 가능성이 적다.
    • 그리고 기능을 오용할 수 있는 문제도 해결할 수 있다.


정리

  • 상속을 이용하면 상위 클래스의 기능 추가에 대해 유연하게 대처하기 힘들다.
  • 또한, 별도의 클래스를 선언함으로써 클래스가 많아 질수 있으며, 상위 클래스를 확장한 하위 클래스에서 기능 오용의 우려가 있다.
  • 그렇다면 언제 상속을 사용하고 언제 조합을 사용해야 할까?
    • 두 클래스간의 관계가 완벽한 is-a 관계가 아니라면 일반적으로 상속보다는 조합을 고려하는 것이 좋은 선택이다.
    • 그밖에 상속을 사용하기 좋을 때는…
      • 상위 클래스와 하위클래스를 모두 같은 프로그래머가 통제하는 패키지일 경우.
      • 확장할 목적으로 설계되었고 문서화도 잘 됐을 경우.
      • 하위 클래스가 상위 클래스의 ‘진짜’ 하위 타입인 상황에서만 쓰자.
        • 클래스간 관계가 is-a 관계일때만 상속
      • 상속을 결정하는 질문
        • 확장하는 클래스의 API에 아무런 결함이 없는가?
        • 결함이 있다면 이 결함이 내 클래스의 API까지 전파되도 괜찮은가?
          • 컴포지션으로는 이런 결함을 숨길 수 있지만, 상속은 아니다.


참고

댓글남기기