상속 대신 합성을 사용하라

허진혁·2023년 8월 2일
2

기본기를 다지자

목록 보기
7/10

🤔 목적

객체지향 프로그래밍을 설계 할 때, 코드 재사용 목적으로 사용하는 상속과 합성이 있어요. 대부분 상속 보다는 합성을 사용하는 것을 권장해서 왜 합성을 선호하는 지에 대해 알아보려고 해요.

객체지향의 사실과 오해, 오브젝트 책을 참고하여 글을 작성했어요 🥸

코드 재사용하는 것의 목적은 중복 코드를 제거하기 위함인데, 그렇다면 중복 코드는 왜 좋지 않을까요?

🔴 중복 코드는 꼭 제거해야 하는 것일까?

우선 중복이라는 것을 헷갈리지 않게 알아야 해요. 중복을 판단할 수 있는 기준은 변경이에요. 요구사항이 변경했을 때, 같은 부분을 함께 수정해야 한다면 그것이 중복 코드에요.

중복 코드가 갖는 문제점은 무엇일까? 생각나는 것을 단순 나열해 본다면,

  • 코드를 수정할 때 비용이 더 들 것이고,
  • 테스트 또한 비용이 더 들 것이고,
  • 관리해야 하는 코드가 많아 지고,
  • 코드를 이해하는데 드는 시간도 많아질 것이다.

위의 나열한 내용들을 정리하면 다음과 같아요.
🔸 중복 코드는 변경을 방해한다. 그러므로, 중복 코드는 제거해야 할 대상이에요.

⚙️ 상속(Inheritance)과 합성(Composition)

개발을 할 때, 많은 것을 고려하겠지만 코드 재사용성을 중점으로 생각을 해볼게요.

객체지향 프로그래밍은 중복되는 로직을 줄이기 위해 코드를 재사용할 수 있게 해야해요. 그 방법으로 상속과 합성이 있어요.

상속(Inheritance)

상속은 상위 클래스의 로직을 하위 클래스에서 물려 받아 코드를 재사용하는 방법이에요. 즉, 상위 클래스에서만 구현을 해두면 돼요. 이러한 관계를 Is-a 관계라고 해요.

예를 들어 동물 클래스와 고양이 클래스가 있다면 고양이는 동물이기에 이러한 관계를 Is-a 관계라 할 수 있는 것이지요.

public class Animal {

    public void walk() {
        System.out.println("걷는다");
    }

    public void eat() {
        System.out.println("먹는다");
    }
}

public class Cat extends Aniaml {
    
}

고양이 클래스는 동물 클래스에 정의된 메서드들을 사용할 수 있어요.

Animal animal = new Cat();

animal.walk();
animal.eat();

이렇게 상위 클래스에 정의된 것들을 활용해서 코드를 재사용할 수 있어요. 이는 상속의 장점이에요.

합성(Composition)

합성은 중복되는 것을 객체로 구현하여, 이 객체를 상태로 갖음으로써 코드를 재사용하는 방법이에요. 이러한 관계를 Has-a 관계라고 불러요.

합성을 통해 코드를 재사용하는 방식은 상속과는 달리 해당 객체를 참조하여 인터페이스에 정의된 메시지를 통해서만 코드를 재사용 하는방법이에요.

예를 들어 주문에서 아이템들의 총 가격을 구하는 상황이라고 할 때, 주문은 아이템들을 갖고 있어야 해요.

public class Order {

    private List<OrderItem> orderItems;
    
    public Order(List<OrderItem> orderItems) {
        this.orderItems = orderItems;
    }

    public int getTotalPrice() {
        return orderItems.stream()
            .mapToInt(Item::getPrice)
            .sum();
	}
}

이처럼 합성을 사용하면 객체의 내부는 공개되지 않고, 인터페이스를 통해 코드를 재사용할 수 있게되요.

👉 구현에 대한 의존성에서 인터페이스에 대한 의존성으로 변경되기 때문에 결합도를 낮출 수 있어요.

🟢 상속과 합성을 객체지향적 관점에서의 차이를 정리해보면

  • 상속은 is-a 관계이고, 합성은 has-a 관계에요.
  • 상속은 구현에 의존하지만, 합성은 퍼블릭 인터페이스에 의존해요.
  • 상속 관계는 컴파일 시점에 결정되지만, 합성은 런타임 시점에 결정되요.

🤩 상속보다 합성을 사용해야 하는 이유

실제로 상속이 갖는 단점 때문에 상속을 사용 하는 경우가 제한적이고 합성을 더 많이 사용하게 돼요.

상속은 다음과 같은 단점을 갖고 있어요.

  • 상위 클래스는 누가 자신을 상속하는지 모른다. 하지만, 하위 클래스는 상위 클래스의 내부 구조를 알아야 한다.
  • 캡슐화를 위반한다.
  • 설계를 유연하지 못하게 한다.
  • 다중 상속이 불가하다.

1️⃣ 상위 클래스는 누가 자신을 상속하는지 모른다. 하지만, 하위 클래스는 상위 클래스의 내부 구조를 알아야 한다.

하위 클래스는 상위 클래스를 상속한다고 extends 를 통해 명시적으로 알려주지만, 상위 클래스는 누가 자신을 상속하는지 알수 없어요. 상속은 하위 클래스가 상위 클래스를 내부 구조를 알아야 재사용하기에 강하게 의존하게 되고, 결합도가 증가하게 됩니다. 이는 상위 클래스의 코드 변경이 하위 클래스에 변경에 영향을 준다는 것을 의미하고, 코드의 유지보수성을 저해해요.

2️⃣ 캡슐화를 위반한다.

객체지향 프로그래밍에서는 높은 응집성과 낮은 결합도를 강조해요. 이는 추상화를 활용을 통해 지킬 수 있어요.

하지만 상속을 사용하면 캡슐화가 깨짐과 동시에 결합도가 증가해요. 상속은 컴파일 시점에 상위 클래스와 하위 클래스가 결정되기 때문이에요. 런타임 시점에 객체를 끼어 맞추는 방식이 불가하다는 것은 다형성을 활용하지 못하는 것이고 객체지향의 품질을 떨어트리게 되는 것이죠.

또한, 하위 클래스에서 상위 클래스를 직접 호출할 수 있어서 상위 클래스의 값을 변경할 수 있어요.

public class Aniaml {
    private String name;
}

public class Cat extends Aniaml {
    public Cat(final String customName) {
			super(customName);
	}
}

이처럼 상위 클래스의 상태가 바뀌는데, 상위 클래스는 이를 감지할 수 없어요. 즉, 상위 클래스는 누가 자신을 상속하는지도 모르는데 심지어 하위 클래스가 자신의 상태를 변경할 수 있게되요.

결국 캡슐화가 깨지게 되는 것이죠.

3️⃣ 설계를 유연하지 못하게 한다.

상위 클래스와 하위 클래스가 강하게 결합되어 있기 때문에 유연성과 확장성이 떨어져요.

상위 클래스에서 변경이 일어나면 하위 클래스들은 그 변경을 고스란히 받아들여 모두 수정해야 하는 상황이 발생하게 되요.

4️⃣ 다중 상속이 불가하다.

자바에서는 다중 상속이 불가능해요. 이미 상속을 하고 있다면 다른 코드를 재사용 하고 싶어도 할 수 없게 되요.

합성의 장점

  • 합성을 사용하면 퍼블릭 인터페이스에 대해 느슨한 결합도를 유지할 수 있다.
  • 합성을 이용하면 캡슐화를 효과적으로 할 수 있다.
  • 런타임 시 외부에 필요한 전략에 따라 교체하여 사용하기에 유연한 설계가 가능하다.

😒 항상 합성을 사용해야 하는 것인가?

아마 모두 알겠지만, 적절한 상황에서 적절한 선택을 해야 한다.

그렇다면 언제 상속을 사용해야 하고 언제 합성을 사용해야 하는지 알아봐요.

상속을 사용하면 좋은 경우

  • 상위클래스와 하위클래스가 Is-a 관계인 경우

두 클래스를 일반적으로 볼 때, (클라이언트 입장에서) ‘하위클래스가 상위클래스다’ 라고 해도 이상하지 않으면 상속을 사용할 조건 중 한 개를 확보해둔 것이에요.

위에서 보았듯이, ‘고양이는 동물이다.’, ‘고등학생은 학생이다.‘ 처럼 이상하지 않은 관계를 말해요.

  • 행동 호환성이 만족할 경우

행동호환성은 ‘클라이언트의 관점에서 슈퍼타입과 서브타입 사이에 행동이 호환되도록 만들어야 한다’를 의미하며, 상위 클래스 타입으로 하위 클래스로 사용해도 무방하다는 뜻이에요. 즉, 클라이언트 입장에서는 두 타입이 동일하게 행동할 것으로 기대하는 것이에요.

합성을 사용하면 좋은 경우

✨ 코드를 재사용 하고 싶은 경우

정리

상속을 하면 코드를 재사용 할 수 있지만, 객체 간의 결합도가 높아지고 캡슐화도 깨지는 것 등의 문제가 발생해요. 또한, 상속으로 인해 유연한 설계가 힘들어지고 확장성도 떨어져요.

그래서 상위 클래스와 하위 클래스와의 관계가 Is-a 관계이고 행동 호환성이 만족할 경우는 상속을 사용하면 좋아요. 자식 클래스들을 추상화 하여 부모 클래스에 코드를 재사용함으로써 재사용성과 응집도를 높일 수 있어요.

그렇지만 완벽하게 결합도를 낮추는 것은 힘들어요. 합성을 이용하면 상속보다는 코드를 많이 써야 하겠지만 쉽고 유연한 설계를 얻을 수 있으므로 가급적이면 합성이라는 더 좋은 방법을 활용하는게 좋아요.

상속의 목적은 코드 재사용이 아니에요. 상속을 통해 얻는 이점 중 한개일 뿐이죠. 상속의 목적은 다형성을 활용하기 위해 상위타입과 하위타입 계층을 구축하는 것이에요.

profile
Don't ever say it's over if I'm breathing

1개의 댓글

comment-user-thumbnail
2023년 8월 2일

정리가 잘 된 글이네요. 도움이 됐습니다.

답글 달기