개요
이번 장에서는 상속의 문제점을 해결하기 위한 방법에 대해서 소개합니다.
상속의 문제들을 어떻게 해결할 수 있을까?
- 합성(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의 구체적인 구현을 상속받지 않습니다. 대신 Car는 CarBehavior 인터페이스에 따른 특정 행동(구현)을 포함하고 있습니다. 이렇게 되면 Car는 필요한 행동만 포함하게 되어 불필요한 인터페이스를 상속받는 문제가 발생하지 않습니다.
- 메서드 오버라이딩의 오작용 문제: 합성을 사용하면 메서드를 재정의하는 대신 새로운 행동(동작) 클래스를 정의하여 사용할 수 있습니다. 예를 들어, SportsCarBehavior는 NormalCarBehavior를 수정하지 않고도 스포츠카의 특성에 맞는 행동을 정의할 수 있습니다.
- 부모와 자식 클래스의 동시 수정 문제: 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
http://www.yes24.com/Product/Goods/74219491
'스터디 > 오브젝트' 카테고리의 다른 글
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 |