Java - 상속보다는 합성

Kwon Yongho·2023년 12월 19일
0

Java

목록 보기
6/6
post-thumbnail

1. 상속(Inheritance)과 합성(Composition)

개발을 할 때 가장 신경써야 하는 것 중 하나가 중복을 제거하여 변경을 쉽게 만드는 것이다. 객체지향 프로그래밍의 장점 중 하나는 코드를 재사용하여 중복을 제거하기에 용이하다는 것인데, 이를 위한 방법에는 크게 상속과 합성 두 가지가 있다.

1-1. 상속(Inheritance) 이란?

상속은 상위 클래스에 중복 로직을 구현해두고 이를 물려받아 코드를 재사용하는 방법이다. 흔히 상속은 Is-a 관계라고 많이 불린다.

class Animal {
    void eat() {
        System.out.println("Eating...");
    }
}

class Dog extends Animal {
    void bark() {
        System.out.println("Barking...");
    }
}

public class InheritanceExample {
    public static void main(String[] args) {
        Dog myDog = new Dog();
        myDog.eat();  // 상속받은 메서드 호출
        myDog.bark(); // 자식 클래스의 메서드 호출
    }
}

1-2. 합성(Composition) 이란?

  • 합성은 중복되는 로직들을 갖는 객체를 구현하고, 이 객체를 주입받아 중복 로직을 호출함으로써 퍼블릭 인터페이스를 재사용하는 방법이다. 흔히 합성은 Has-a 관계라고 많이 불린다.
  • 객체를 다른 객체의 인스턴스 변수로 포함시켜 기능을 확장하거나 변경한다.
  • 상속보다는 객체 간의 관계를 동적으로 조정하기에 더 용이하다.
class Engine {
    void start() {
        System.out.println("Engine starting...");
    }
}

class Car {
    Engine engine;

    Car(Engine engine) {
        this.engine = engine;
    }

    void drive() {
        engine.start();
        System.out.println("Car is moving...");
    }
}

public class CompositionExample {
    public static void main(String[] args) {
        Engine myEngine = new Engine();
        Car myCar = new Car(myEngine);

        myCar.drive(); // 합성을 통한 기능 조합
    }
}

2. 상속의 단점

  • 캡슐화가 깨지고 결합도가 높아짐
  • 유연성 및 확장성이 떨어짐
  • 다중상속에 의한 문제가 발생할 수 있음
  • 클래스 폭팔 문제가 발생할 수 있음

2-1. 캡슐화가 깨지고 결합도가 높아짐

  • 결합도는 하나의 모듈이 다른 모듈에 대해 얼마나 많은 지식을 갖고 있는지를 나타내는 정도이다.
  • 객체지향 프로그래밍에서는 결합도는 낮을수록, 응집도는 높을수록 좋다.
  • 객체지향의 장점 중 하나는 추상화에 의존함으로써 다른 객체에 대한 결합도는 최소화하고 응집도를 최대화하여 변경 가능성을 최소화하는 것이다.
  • 상속을 이용하면 캡슐화가 깨지고 결합도가 높아지는데, 그 이유는 부모 클래스와 자식 클래스의 관계가 컴파일 시점에 결정되어 구현에 의존하기 때문이다.
  • 컴파일 시점에 결정되는 관계는 유연성을 상당히 떨어뜨리고, 실행 시점에 객체의 종류를 변경하는 것이 불가능하여 다형성 등과 같은 좋은 객체지향 기술을 사용할 수 없다.

대표적인 Vector, Stack 예제
Stack 클래스는 Vector 클래스를 상속받는다. 그래서 Stack 클래스가 제공하는 push, pop 이지만 Vector 클래스의 add 메소드 또한 외부로 노출되게 된다. 그러면서 아래와 같이 의도치 않은 동작이 실행되면서 오류를 범하게 된다.

Stack<String> stack = new Stack<>();
stack.push("1st");
stack.push("2nd");
stack.push("3rd");

stack.add(0, "4th");

assertEquals("4th", stack.pop()); // 실패!!!!

여기서 문제는 Stack 클래스의 add(int index, E element) 메서드를 사용하여 요소를 삽입하면 LIFO (Last-In-First-Out) 구조를 깨뜨리게 됩니다. 스택에서는 새로운 요소가 항상 맨 위에 추가되어야 하는데, add 메서드를 사용하면 지정된 인덱스에 요소가 삽입되어 예상과 다르게 동작합니다.

2-2. 유연성 및 확장성이 떨어짐

상속으로 인해 결합도가 높아지면 다음과 같은 두 가지 문제점이 발생한다.

  • 하나의 기능을 추가하거나 수정하기 위해 불필요하게 많은 수의 클래스를 추가하거나 수정해야 한다.
  • 단일 상속만 지원하는 언어에서는 상속으로 인해 오히려 중복 코드의 양이 늘어날 수 있다.

간단한 음식점 예제

class Food {
    protected int price;

    public Food(int price) {
        this.price = price;
    }

    public int getPrice() {
        return price;
    }
}

class Bread extends Food {
    private String type;

    public Bread(int price, String type) {
        super(price);
        this.type = type;
    }

    public String getType() {
        return type;
    }
}

public class FoodExample {
    public static void main(String[] args) {
        // Creating a Food object
        Food food = new Food(10);

        // Creating a Bread object
        Bread bread = new Bread(5, "Whole Wheat");
        displayFoodInfo(bread);
	}
}

discount 추가

class Food {
    protected int price;
    protected int discount; // 코드 추가

    public Food(int price, int discount) {
        this.price = price;
        this.discount = discount;
    }

    public int getPrice() {
        return price - discount;  // Apply discount
    }

    public int getDiscount() {
        return discount;
    }
}

class Bread extends Food {
    private String type;

    public Bread(int price, int discount, String type) {
        super(price, discount); // 코드 수정
        this.type = type;
    }

    public String getType() {
        return type;
    }
}

public class FoodExample {
    public static void main(String[] args) {
        // Creating a Food object
        Food food = new Food(10, 2);  // 코드 추가

        // Creating a Bread object
        Bread bread = new Bread(5, 1, "Whole Wheat");  // 코드 추가
    }

}
  • 자식 클래스 뿐만 아니라 자식 클래스가 선언되어 객체를 생성하는 부분들 역시 모두 수정해주어야 한다.

2-3. 다중상속에 의한 문제가 발생할 수 있음

  • 자바에서는 다중 상속을 허용하지 않는다. 그렇기 때문에 상속이 필요한 해당 클래스가 다른 클래스를 이미 상속중이라면 문제가 발생할 수 있다. 다중 상속과 관련된 문제를 피하기 위해서도 상속의 사용을 지양해야 한다.
  • 결국 클래스를 또 나누고 나누어 구성해야하는데 결국 클래스 폭발로 이어지게 된다.

2-4. 클래스 폭팔 문제가 발생할 수 있음

  • 상속을 남용하게 되면, 새롭게 만든 클래스에 하나의 기존의 기능을 연결하기 위해 상속을 하게 될꺼고, 또다시 새롭게 만든 클래스에 기능 연결하기 위해 상속을 하고, 이렇게 필요 이상으로 많은 수의 클래스를 추가해야 하는 경우를 가리켜 클래스 폭발(class explosion) 문제 또는 조합의 폭발(combinational explosion) 문제라고 부른다.

3. 상속(Inheritance)보다 합성(Composition)을 사용해야 하는 이유

상속은 중복을 제거하기에 아주 좋은 객체지향 기술처럼 보이고, 그에 따라 상속을 무분별하게 남발하는 경우를 자주 볼 수 있다. 하지만 상속을 이용해야 하는 경우는 상당히 선택적이며, 상속이 갖는 단점은 상당히 치명적이기 때문에 상속보다는 합성을 이용할 것을 권장한다.

  • 상속은 컴파일 시점에 부모 클래스와 자식 클래스의 코드가 강하게 결합되는 반면에 합성을 이용하면 구현을 효과적으로 캡슐화할 수 있다. 또한 의존하는 객체를 교체하는 것이 비교적 쉬우므로 설계가 유연해진다. 왜나하면 상속은 클래스를 통해 강하게 결합되지만 합성은 메세지를 통해 느슨하게 결합되기 때문이다.
  • 단일 상속 한계를 해소 해준다.
  • 대표적인 사례가 디자인 패턴 중에 전략 패턴이 될 수 있다.
  • Java의 창시자인 제임스 고슬링(James Arthur Gosling)이 한 인터뷰에서 "내가 자바를 만들면서 가장 후회하는 일은 상속을 만든 점이다" 라고 말할 정도 이다.
  • 조슈야 블로크의 Effective Java에서는 상속을 위한 설계와 문서를 갖추거나, 그럴 수 없다면 상속을 금지하라는 조언을 한다.
  • 따라서 추상화가 필요하면 인터페이스로 implements하거나 객체 지향 설계를 할땐 합성(composition)을 이용하는 것이 추세이다.

참조

0개의 댓글