상속 vs 조합, 합성

상속(Inheritance)조합, 합성(Composition)
부모 클래스와 자식 클래스 사이의 의존성은 컴파일 타임에 해결두 객체 사이의 의존성은 런타임에 해결
is-a 관계has-a 관계
부모클래스의 구현에 의존 결합도가 높음.구현에 의존하지 않음.
내부에 포함되는 객체의 구현이 아닌 인터페이스에 의존.
클래스 사이의 정적인 관계객체 사이의 동적인 관계
부모 클래스 안에 구현된 코드 자체를 물려 받아 재사용포함되는 객체의 public 인터페이스를 재사용

상속이란?

class Mobile {
	// ...
}

class Apple extends Mobile {
	// ... 
}

상속은 코드 재사용을 위한 기법이다.

이미 일반적인 클래스가 구현되어 있는 상태에서 그보다 구체적인 클래스를 구현하기 위해 사용되는 기법이다.

그로 인해 상위 클래스의 코드를 하위 클래스가 재사용 할 수 있을 뿐이다.

근본적인 객체 주체는 같지만, 서로 다른 속성이나 기능들을 가지고 있을 때, 이러한 구조를 상속관계를 통해 논리적으로 개념적으로 연관시키는 것이 상속이다.

따라서 명확한 is-a 관계에 있는 경우, 상위 클래스가 확장할 목적으로 설계되었고 문서화도 잘 되어 있는 경우에 사용하면 좋다.

그러나

상속을 제대로 활용하기 위해서는 부모 클래스의 내부 구현에 대해 상세하게 알아야 하기 때문에

자식 클래스와 부모 클래스 사이 결합도가 높아진다.

상속 관계는 컴파일 타임에 결정되어 고정되기 때문에 코드를 실행하는 도중에 변경할 수 없다.

조합, 합성이란?

class Car {
	Engine engine; // 필드로 Engine 클래스 변수를 갖는다(has)
	
	Car(Engine engine) {
		this.engine = engine; // 생성자 초기화 할 때 클래스 필드의 값을 정하게 된다
	}

	void drive() {
		System.out.printf(engine.EngineType + "으로 드라이브~");
	}

	void breaks() {
		System.out.printf(engine.EngineType + "으로 브레이크~");
	}

}

class Engine {
	String EngineType; // 디젤, 가솔린, 전기

	Engine(String type) {
		Engine = type;
	}
}

public class Main {
    public static void main(String[] args) {
        Car digelCar = new Car(new Engine("디젤"));
        digelCar.drive(); // 디젤엔진으로 드라이브~

        Car electroCar = new Car(new Engine("전기"));
        electroCar.drive(); // 전기엔진으로 드라이브~
    }
}

합성은 기존 클래스를 상속을 통해 확장하는 대신에 필드로 클래스의 인스턴스를 참조하게 만드는 설계이다.

서로 관련없는 클래스 사이에서,

한 클래스가 다른 클래스의 기능을 사용하여 구현해야 한다면

합성의 방식을 사용한다고 보면 된다.

합성을 이용할 때 꼭 클래스 뿐만 아니라 추상 클래스, 인터페이스로도 가능하다!

상속의 문제점

현업에서는 가능하면 extends를 지양하는 편이며

클래스 상속을 해야할 때는 정말 개념적으로 연관 관계가 있을 때만 하는 상당히 제한적인 기능이다.

1. 결합도가 높아지는 문제

객체지향 프로그래밍에서 결합도는 낮을수록, 응집도는 높을수록 좋다.

그래서 추상화에 의존함으로써 다른 객체에 대한 결합도는 최소화하고 응집도를 최대화하여 변경 가능성을 최소화할 수 있다.

상속을 하게 되면

컴파일 시점에 관계가 결정되기 때문에

실행 시점에 객체 종류를 변경할 수 없게 되어

유기적인 다형성 및 객체지향 기술을 사용할 수 없다.

2. 불필요한 기능 상속 문제

부모 클래스에 메소드를 추가하였는데

자식 클래스에는 적합하지 않은 메소드일 수 있다.

3. 부모 클래스의 결함이 그대로 넘어오는 문제

만일 부모 클래스에 결함이 있다면 자식 클래스에게 그대로 결함이 넘어온다.

자식 클래스를 잘 구현해놓았더라도, 부모 클래스에 문제가 있다면 자식 클래스에도 문제가 발생하게 된다.

4. 부모 클래스와 자식 클래스의 동시 수정 문제

class Food {
    final int price;
    
    Food(int price) {
        this.price = price;
    }
}

class Bread extends Food {
    public Bread(int price) {
        super(price);
    }
}

public class Main {
    public static void main(String[] args) {
        Food bread = new Bread(1000);
    }
}
class Food {
    final int price;
    final int count; // 코드 추가
    
    Food(int price, int count) { 
        this.price = price;
        this.count = count; // 코드 추가
    }
}

class Bread extends Food {
    public Bread(int price, int count) {
        super(price, count); // 코드 수정
    }
}

public class Main {
    public static void main(String[] args) {
        Food bread = new Bread(1000, 5); // 코드 수정
    }
}

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

HashSet을 상속받고, HashSet의 add() 메소드와 addAll() 메소드를 오버라이딩하여

나만의 Set 클래스를 구축한 예제이다.

import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;

class CustomSet<E> extends HashSet<E> {
    private int addCount = 0; // 자료형에 몇번 추가되었는지 세는 카운트 변수

    @Override
    public boolean add(E e) {
        // 만일 add되면 카운트를 증가 시키고, 부모 클래스 HashSet의 add() 메소드를 실행한다.
        addCount++;
        return super.add(e);
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {
        // 만일 리스트 자체로 들어와 통쨰로 add 한다면, 컬렉션의 사이즈를 구해 카운트에 더하고, 부모 클래스 HashSet의 addAll() 메소드를 실행한다.
        addCount += c.size();
        return super.addAll(c);
    }

    public int getAddCount() {
        return addCount;
    }
}

public class Main {
    public static void main(String[] args) {
        CustomSet<String> mySet = new CustomSet<>();

        mySet.addAll(Arrays.asList("가", "나", "다", "라", "마"));
        mySet.add("바");

        System.out.println(mySet.getAddCount()); // ! 6이 나와야 정상이지만 11이 나오게 된다.
    }
}

main을 살펴보자.

CustomSet 클래스에서 add() 메소드와 addAll() 메소드를 사용하여 데이터 6개를 추가하였다.

추가한 데이터가 6개니까, 결과는 6이 나와야하지만

실제로 결과는 11이 나왔다.

문제는 부모의 addAll() 메소드를 제대로 알지도 못한 채로 오버라이딩하고 super 키워드로 호출했기 때문이다.

addAll() 메소드 내부 로직을 보면, 내부적으로 add() 메소드를 호출하고 있다.

따라서, addAll() 메소드를 호출할 때 add() 메소드를 호출하니 addCount 변수가 두 배로 더해진 것이다.

제대로 addAll() 동작을 구현하기 위해서는

상속하는 부모의 클래스의 내부 로직을 자세히 뒤져보아야 한다.

이게 맞냐?

6. 불필요한 인터페이스 상속 문제

초기 자바 버전에서 발생했던 대표적인 Vector 클래스와 이를 상속받는 Stack 클래스 예제가 있다.

Stack<String> stack = new Stack<>();
stack.push("one");
stack.push("two");
stack.push("three");

stack.add(0, "four"); // add 메소드를 호출함으로써 stack의 의미와는 다르게 특정 인덱스의 값이 추가

String str = stack.pop(); // three
System.out.println(str.equals("four")); // false

Stack의 대표적인 동작은 push(), pop()이다.

그러나 Vector 클래스를 상속받은 Stack 클래스는 Vertor 클래스가 가지고 있던 add() 메소드를 사용할 수 있게 되었다.

자바 공식 문서에도

잘못 구현된 Stack이 아닌 Deque를 사용할 것을 권장하고 있다.

7. 클래스 폭발 문제

클래스 폭발 문제는 자식 클래스가 부모 클래스의 구현과 강하게 결합되도록 강요하는 상속의 근본적인 한계 때문에 발생한다.

부모와 자식 클래스가 강하게 결합된 상속 관계 상태라면, 다양한 조합이 필요한 상황이 왔을 때 결국 유일한 해결 방법은 조합의 수만큼 새로운 클래스를 상속하는 것 뿐이다.

8. 단일 상속의 한계

자바에서는 다중 상속을 허용하지 않는다.

그렇게 때문에 상속이 필요한 클래스가 이미 다른 클래스를 상속중이라면 문제가 발생하게 된다.

결국 클래스를 나누고 나누고 나누어 구성해야하는데, 클래스 폭발 문제로 이어질 것이다.

final 키워드를 붙여서 상속을 못하게 하는 문법이 있다.
위와 같은 문제점으로 이러한 기법이 생겨났다.

합성을 사용해야 하는 이유

위와 같은 문제들로 인해, 합성을 사용하는 것이 좋다.

합성을 사용했을 때 문제점이라고는,,, 클래스 간 관계를 파악하기 힘들다는 정도,,? 이다.

상속을 지양하고 합성을 지향하라는 말은
상속은 반드시 어떠한 특정한 관계일 때만 사용하라고 엄격하게 제한하라는 말이다.
따라서 클래스 간에 확실히 is-a 관계로 구성될 수 밖에 없으면 상속을 하고, 그외 왠만한 경우에는 합성을 하자.

불필요한 인터페이스 상속 문제 해결

public class Stack<E> {
    private Vector<E> elements = new Vector<>(); // 합성
    
    public E push(E item) {
        elements.addElement(item);
        return item;
    }

    public E pop() {
        if (elements.isEmpty()) {
            throw new EmptyStackException();
        }
        return elements.remove(elements.size() -1);
    }
}

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

import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

class CustomSet<E> {
    private int addCount = 0; // 자료형에 몇번 추가되었는지 세는 카운트 변수
    private Set<E> set = new HashSet<>(); // 합성

    public boolean add(E e) {
        addCount++;
        return set.add(e); // 합성된 객체의 메서드를 실행
    }

    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return set.addAll(c); // 합성된 객체의 메서드를 실행
    }

    public int getAddCount() {
        return addCount;
    }
}

public class Main {
    public static void main(String[] args) {
        CustomSet<String> mySet = new CustomSet<>();

        mySet.addAll(Arrays.asList("가", "나", "다", "라", "마"));
        mySet.add("바");

        System.out.println(mySet.getAddCount()); // 6
    }
}

단일 상속 문제 해결

public class Phone {
    private RatePolicy ratePolicy; // 클래스 합성
    private List<Call> calls = new ArrayList<>(); // 클래스 합성

    public Phone(RatePolicy ratePolicy) {
        this.ratePolicy = ratePolicy;
    }

    public List<Call> getCalls() {
        return Collections.unmodifiableList(calls);
    }
}

참조

https://inpa.tistory.com/entry/OOP-💠-객체-지향의-상속-문제점과-합성Composition-이해하기

profile
나는 아직 멍청하다

0개의 댓글

Powered by GraphCDN, the GraphQL CDN