본문 바로가기
스터디/오브젝트

11장 - 합성과 유연한 설계

by 검은도자기 2023. 8. 28.

개요

이번 장에서는 상속의 문제점을 해결하기 위한 방법에 대해서 소개합니다.

 

 

상속의 문제들을 어떻게 해결할 수 있을까?

  • 합성(Composition)을 사용하면 상속의 문제들을 해결할 수 있습니다.
  • 합성(Composition)은 기존의 객체를 재사용하기 위해 그 객체를 포함하는 새로운 객체를 생성하는 디자인 패턴입니다.
  • 다음 간단한 예제들을 통해 어떻게 문제를 해결하는지 알아보겠습니다.

 

상속을 사용한 예제

class Car:
    def __init__(self, fuel_efficiency):
        self.fuel_efficiency = fuel_efficiency  # 초기 연료 효율성 설정

    def fuel_needed(self, distance):
        return distance / self.fuel_efficiency  # 필요한 연료량 계산

    def tune_up(self):
        self.fuel_efficiency += 2  # 연료 효율성을 2만큼 향상시킴

class SportsCar(Car):
    def fuel_needed(self, distance):
        base_fuel = super().fuel_needed(distance)
        return base_fuel * 1.10  # 스포츠카는 10% 더 많은 연료가 필요함

    def tune_up(self):
        super().tune_up()  # 부모 클래스의 tune_up 메서드 호출
        print("Sporty features added!")  # 스포츠카 추가 기능 설정


sports_car = SportsCar(10)
print(sports_car.fuel_needed(100))  # Outputs: 11.0
sports_car.tune_up()
print(sports_car.fuel_needed(100))  # Outputs: 9.090909090909092

코드 설명

  • 이 예제에서 Car 클래스는 자동차의 기본적인 특성과 메서드를 정의하고 있으며, SportsCar 클래스는 Car 클래스를 상속받아 스포츠카의 특성을 추가로 구현하고 있습니다.

 

문제점

  • 불필요한 인터페이스 상속 문제: SportsCar는 Car의 모든 메서드와 속성을 상속받습니다. 만약 Car에 스포츠카와 무관한 메서드가 추가되면, SportsCar도 그것을 상속받게 됩니다.
  • 메서드 오버라이딩의 오작용 문제: SportsCar에서 fuel_needed와 tune_up 메서드를 오버라이드했습니다. 만약 Car의 이러한 메서드들의 기본 동작이 변경되면, SportsCar에도 영향을 미치게 됩니다.
  • 부모와 자식 클래스의 동시 수정 문제: Car 클래스에서 특정 메서드나 속성이 변경되면, 그것을 상속받는 모든 자식 클래스를 함께  수정해야 할 수도 있습니다.

 

합성을 사용한 예제

# 행동(동작)에 대한 인터페이스 정의
class CarBehavior:
    def fuel_needed(self, distance):
        pass

    def tune_up(self):
        pass

# 일반 자동차의 행동 구현
class NormalCarBehavior(CarBehavior):
    def __init__(self, fuel_efficiency):
        self.fuel_efficiency = fuel_efficiency

    def fuel_needed(self, distance):
        fuel = distance / self.fuel_efficiency
        print(f"Normal car needs {fuel} liters for {distance} km")
        return fuel

    def tune_up(self):
        self.fuel_efficiency += 2
        print("Normal car's fuel efficiency improved by 2")

# 스포츠카의 행동 구현
class SportsCarBehavior(CarBehavior):
    def __init__(self, base_behavior):
        self.base_behavior = base_behavior

    def fuel_needed(self, distance):
        base_fuel = self.base_behavior.fuel_needed(distance)
        sports_fuel = base_fuel * 1.10
        print(f"Sports car needs an additional 10% fuel: {sports_fuel} liters for {distance} km")
        return sports_fuel

    def tune_up(self):
        self.base_behavior.tune_up()
        print("Sporty features added!")

class Car:
    def __init__(self, behavior):
        self.behavior = behavior

    def fuel_needed(self, distance):
        return self.behavior.fuel_needed(distance)

    def tune_up(self):
        self.behavior.tune_up()


# 코드 실행 예제
normal_car = Car(NormalCarBehavior(10))
normal_car.fuel_needed(100)  # Outputs: Normal car needs 10.0 liters for 100 km
normal_car.tune_up()  # Outputs: Normal car's fuel efficiency improved by 2

sports_car = Car(SportsCarBehavior(NormalCarBehavior(10)))
sports_car.fuel_needed(100)  # Outputs: Normal car needs 10.0 liters for 100 km, followed by Sports car needs an additional 10% fuel: 11.0 liters for 100 km
sports_car.tune_up()  # Outputs: Normal car's fuel efficiency improved by 2, followed by Sporty features added!

코드 설명

  • CarBehavior라는 행동에 대한 인터페이스를 정의하고, 이를 상속받아 NormalCarBehavior와 SportsCarBehavior로 구체적인 행동을 구현하였습니다.
  • Car는 이 행동을 합성을 통해 포함하고 있습니다.

 

합성을 사용함으로써 다음과 같은 상속 문제를 해결할 수 있습니다.

  • 불필요한 인터페이스 상속 문제: Car 클래스는 CarBehavior의 구체적인 구현을 상속받지 않습니다. 대신 CarCarBehavior 인터페이스에 따른 특정 행동(구현)을 포함하고 있습니다. 이렇게 되면 Car는 필요한 행동만 포함하게 되어 불필요한 인터페이스를 상속받는 문제가 발생하지 않습니다.
  • 메서드 오버라이딩의 오작용 문제: 합성을 사용하면 메서드를 재정의하는 대신 새로운 행동(동작) 클래스를 정의하여 사용할 수 있습니다. 예를 들어, SportsCarBehaviorNormalCarBehavior를 수정하지 않고도 스포츠카의 특성에 맞는 행동을 정의할 수 있습니다.
  • 부모와 자식 클래스의 동시 수정 문제: Car 클래스는 CarBehavior 인터페이스에 의존하며, 각 행동의 구체적인 구현은 별도의 클래스(NormalCarBehavior, SportsCarBehavior 등)에서 처리합니다. 따라서 한 행동 클래스에서의 변경이 다른 클래스에 영향을 미치지 않습니다.

 

 

객체 합성이 클래스 상속보다 더 좋은 방법일까?

  • 위 예제들을 봤을 때 코드를 재사용하면서도 건전한 결합도를 유지할 수 있는 객체 합성이 더 좋은 방법이라고 생각이 듭니다.
  • 하지만 무조건 합성이 좋다는 생각은 좀 위험한 생각이 아닐까 생각이 듭니다.
  • 왜냐하면 상속 계층이 단순하고 잘 정의된 경우, 클래스 상속이 더 간결하고 이해하기 쉬울 수 있기 때문입니다.
  • 개인적인 생각은 대부분의 상황에서는 합성이 좋은 건 동의하지만 주어진 상황 속에서 상속을 사용함으로써의 이점이 큰지 합성을 사용함으로써의 이점이 더 나은지를 고려하며 결정을 내리는 게 올바른 방법이 아닐까?라는 생각이 듭니다.

 

 

믹스인(mixin)

  • 믹스인(mixin)은 객체를 생성할 때 코드 일부를 클래스 안에 섞어 넣어 재사용하는 기법입니다.

 

믹스인 예제

# 믹스인으로 연료 효율 튜닝 기능 분리
class FuelEfficiencyMixin:
    def tune_up(self):
        if hasattr(self, 'fuel_efficiency'):
            self.fuel_efficiency += 2
            print("Fuel efficiency improved by 2")

# 기본 자동차 클래스
class Car:
    def __init__(self, fuel_efficiency):
        self.fuel_efficiency = fuel_efficiency

    def fuel_needed(self, distance):
        fuel = distance / self.fuel_efficiency
        print(f"This car needs {fuel} liters for {distance} km")
        return fuel

# 스포츠카는 Car와 FuelEfficiencyMixin을 사용해 만듦
class SportsCar(Car, FuelEfficiencyMixin):
    def fuel_needed(self, distance):
        base_fuel = super().fuel_needed(distance)
        sports_fuel = base_fuel * 1.10
        print(f"Being a sports car, it needs an additional 10% fuel: {sports_fuel} liters for {distance} km")
        return sports_fuel

sports_car = SportsCar(10)
sports_car.fuel_needed(100) 
# Outputs:
# This car needs 10.0 liters for 100 km
# Being a sports car, it needs an additional 10% fuel: 11.0 liters for 100 km

sports_car.tune_up() 
# Outputs:
# Fuel efficiency improved by 2

코드 설명

  • FuelEfficiencyMixin 클래스: 연료 효율성을 향상하는 특정 기능만을 포함하는 믹스인 클래스입니다. 이렇게 특정 기능을 믹스인으로 분리함으로써 해당 기능을 필요한 다른 클래스에 쉽게 추가할 수 있습니다.
  • Car 클래스: 기본적인 자동차의 특성과 기능을 가지고 있습니다. 이 클래스는 연료 효율성을 기반으로 필요한 연료량을 계산하는 메서드를 포함하고 있습니다.
  • SportsCar 클래스: Car 클래스를 상속받으면서 동시에 FuelEfficiencyMixin의 기능도 포함합니다. 따라서 SportsCar는 기본 자동차의 기능과 함께 연료 효율성을 향상하는 튜닝 기능도 사용할 수 있습니다.

 

믹스인은 다음과 같은 상속 문제를 해결할 수 있습니다.

  • 불필요한 인터페이스 상속 문제: FuelEfficiencyMixin은 연료 효율을 튜닝하는 기능만 제공합니다. 불필요한 기능이나 상태는 상속되지 않습니다.
  • 메서드 오버라이딩의 오작용 문제: 믹스인 메서드는 특정 동작에 초점을 맞추므로 오버라이딩 시 부작용 발생 가능성이 줄어듭니다.
  • 부모 클래스와 자식 클래스의 동시 수정 문제: 연료 효율 튜닝 기능이 믹스인으로 분리되어 있으므로, 이 기능을 변경하려면 믹스인만 수정하면 됩니다. 기본 Car 클래스나 SportsCar 클래스는 수정할 필요가 없습니다.
  •  

 

 

어떤 경우에 상속 (Inheritance) , 합성 (Composition), 믹스인 (Mixin)을 사용해야 할까?

상속 (Inheritance) 합성 (Composition) 믹스인 (Mixin)
계층 구조를 표현하고 싶을 때 변경에 유연하게 대응하고 싶을 때 다양한 클래스에서 공통의 기능을 제공하고 싶을 때
다형성이 필요한 경우 여러 객체들이 서로 다른 기능을 가지지만, 특정 기능을 공유하고 싶을 때 다중 상속의 복잡성을 피하면서 코드를 재사용하고 싶을 때
  'has-a' 관계를 나타내고자 할 때 모듈화와 재사용성을 높이고 싶을 때

 

 

마무리

오늘은 오브젝트 11장을 스터디하고 개인적으로 궁금했던 내용들을 정리해 보았습니다.

개인적으로 이번 장에서 설명하는 합성과 믹스인에 대한 설명이 어렵게 느껴져서 좀 아쉽다는 생각이 들었습니다. 

하지만 여태 생각지 못했던 합성, 믹스인 방식에 대해 알게 되었고, 이 방식들을 어떻게 하면 잘 사용할지 고민할 수 있는 계기를 제공해 줘서 나쁘지 않다고 생각이 드네요.

이번 포스팅은 마무리하면서 다음 포스팅에서 뵙겠습니다.

 

 

참고

https://en.wikipedia.org/wiki/Mixin

 

Mixin - Wikipedia

From Wikipedia, the free encyclopedia Class in object-oriented programming languages This article is about the programming concept. For the ice cream, see Mix-in. In object-oriented programming languages, a mixin (or mix-in)[1][2][3][4] is a class that con

en.wikipedia.org

http://www.yes24.com/Product/Goods/74219491

 

오브젝트 - 예스24

역할, 책임, 협력을 향해 객체지향적으로 프로그래밍하라!객체지향으로 향하는 첫걸음은 클래스가 아니라 객체를 바라보는 것에서부터 시작한다. 객체지향으로 향하는 두번째 걸음은 객체를

www.yes24.com

 

'스터디 > 오브젝트' 카테고리의 다른 글

13장 - 서브클래싱과 서브타이핑  (0) 2023.10.16
12장 - 다형성  (0) 2023.09.20
10장 - 상속과 코드 재사용  (0) 2023.08.06
9장 - 유연한 설계  (0) 2023.07.24
8장 - 의존성 관리하기  (0) 2023.05.22