ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 객체지향 프로그래밍 입문 강의 (3) - 상속보단 조립
    et cetera/TIL 2021. 12. 30. 18:23
    반응형

    상속을 통한 기능 재사용 시 발생할 수 있는 단점

    - 상위 클래스 변경 어려움

        ⇒ 상위 클래스를 변경하면, 그 변경이 모든 하위 클래스에 영향을 줄 수가 있다. (변경의 여파가 전파된다)

        ⇒ 상위 클래스를 조금만 잘못 변경해도 모든 하위클래스가 비정상적으로 동작할 가능성이 생긴다.

     

    - 클래스가 불필요하게 증가

     

    - 상속 오용의 가능성

    예를 들어, ArrayList를 상속받아 다음과 같이 Container 클래스를 구현했다고 치자.

    public class Container extends ArrayList<Luggage> {
        private int maxSize;
        private int currentSize;
        
        public Container(int maxSize) {
            this.maxSize = maxSize;
        }
        
        public void put(Luggage lug) throws NotEnoughSpaceException {
            if (!canContain(lug)) throw new NotEnoughSpaceException();
            super.add(lug);
            this.currentSize += lug.size();
        }
        
        public void extract(Luggage lug) {
            super.remove(lug);
            this.currentSize -= lug.size();
        }    
        
        public boolean canContain(Luggage lug) {
            return this.maxSize >= this.currentSize + lug.size();
        }
    }

    올바르게 사용하는 방법은 다음과 같을 것이다.

    // 올바른 사용법
    Container c = new Conatiner(5);
    if (c.canContain(size2Luggage)) {
        c.put(size2Luggage);
    }

    근데, 개발자 도구에서 위 코드의 'c.' 까지 입력하면 자동완성으로 메서드들이 뜨는데

    이 때 메서드들이 Container 클래스에 정의된 메서드만 나오는 게 아니라,

    Container 클래스가 상속받은 ArrayList의 메서드들까지 전부 표시된다.

    (불필요한 메서드들까지 전부 상속을 받게 되기 때문)

    그래서 다음과 같은 문제가 발생할 수 있다.

    Luggage size1Lug = new Luggage(1);
    Luggage size2Lug = new Luggage(2);
    Luggage size3Lug = new Luggage(3);
    
    Container c = new Container(5);
    if (c.canContain(size3Lug)) {
        c.put(size3Lug); // 정상적인 사용, Container 여분 5에서 2로 줄어듦
    }
    
    if (c.canContain(size2Lug)) {
        c.add(size2Lug); // 비정상적인 사용, Container 여분 2에서 줄지 않음
    }
    
    if (c.canContain(size1Lug)) { // 통과됨. 원래는 통과되면 안됨
        c.add(size1Lug);    
    }

    add 메서드는 이름만 보면 마치 Container에 Luggage를 추가하는 메서드처럼 보인다.

    하지만 add 메서드는 ArrayList의 메서드이기 때문에 Container의 maxSize와 currentSize로 판단하지 못한다.

    그래서 위 예시와 같이 maxSize를 넘어섰는데도 add 메서드를 통해

    Luggage를 최대 크기보다 더 많이 넣을 수 있는 논리적인 오류가 발생하게 된다.

     

    사실 이런 경우는 add 메서드를 잘못 사용한 사람의 잘못이라기보다는,

    Container 클래스를 잘못 사용할 가능성이 높게 만들어 놓은 사람의 잘못이라고 할 수 있겠다.

     

     

    상속의 단점 해결 방법: 조립

    조립(Composition)

    - 여러 객체를 묶어서 더 복잡한 기능을 제공

     

    - 보통 필드로 다른 객체를 참조하는 방식으로 조립, 또는 객체를 필요 시점에 생성/구함

    만약 암호화 기능이 필요하다면, 암호화 기능을 제공하는 Encryptor 클래스를 상속받아서 구현하는 게 아니라, 

    다음 코드와 같이 필드나 메서드에서 Encryptor 객체를 생성하고 재사용하는 것.

    public class FlowController {
        private Encryptor encryptor = new Encryptor(); // 필드로 조립
        
        public void process() {
            // ...
            byte[] encryptedData = encryptor.encrypt(data);
            // ...
        }
    }

    즉, 다양한 기능을 제공하는 객체들을 조립해서 더 복잡한 기능을 제공하는 것.

     

    - 조립하는 방식으로 기능을 재사용하면 상속에서 봤던 것처럼 클래스가 증식하는 문제가 사라지게 된다.

    특정 기능을 재사용하고 싶다면, 그냥 해당 기능을 제공하는 클래스를 객체로 만들어서 사용하면 된다.

     

    - 상속을 오용하는 문제도 사라진다. 불필요한 기능까지 상속하면서 발생했던 문제가 사라지는 것.

     

    상속보다는 조립(Composition over Inheritance)

    - 이렇듯, 기능 재사용을 위해 상속을 사용하지 않고 조립을 사용하는 것이 보통은 더 장점이 많다.

    - 상속하기에 앞서 조립으로 해결할 수 없는지 검토

    - 진짜 하위 타입인 경우에만 상속을 사용할 것.

    위에서 봤던 Container 클래스의 경우에도, Container는 사실 ArrayList의 한 종류가 아니다.

    단지 목록을 관리하는 기능이 필요해서 ArrayList를 상속했던 것.

    이렇게, 진짜 하위 타입이 아닌데 기능 재사용때문에 상속을 오용하게 되면 문제가 발생하게 되는 것이다.

    상속을 하려는 타입이, 상위 타입의 '한 종류' 인 경우에만 상속을 할 것.

    반응형

    댓글

Designed by Tistory.