
현대적인 소프트웨어 아키텍처의 핵심 원칙인 "Composition over Inheritance"를 파이썬 실무 관점에서 깊이 있게 파헤칩니다.
1. 서론: 왜 '상속'이 만능 해결사가 아닐까?
객체 지향 프로그래밍(OOP)을 처음 배울 때 우리는 상속(Inheritance)을 코드 재사용의 마법처럼 학습합니다. 하지만 프로젝트의 규모가 커질수록 깊고 복잡한 상속 계층은 '취약한 기반 클래스(Fragile Base Class)' 문제를 야기하며 유지보수를 불가능하게 만듭니다. 반면, 합성(Composition)은 객체가 다른 객체를 포함하여 기능을 수행하는 방식으로, 런타임에 행동을 변경할 수 있는 유연성을 제공합니다. 파이썬은 덕 타이핑(Duck Typing)과 동적 특성 덕분에 합성을 구현하기에 가장 이상적인 환경을 제공합니다. 본 글에서는 상속의 늪에서 벗어나 유연한 코드를 작성하는 구체적인 전략을 다룹니다.
2. 상속(Inheritance) vs 합성(Composition) 핵심 차이 비교
두 개념의 근본적인 차이점과 선택 기준을 명확한 표로 정리하였습니다.
| 비교 항목 | 상속 (Inheritance) | 합성 (Composition) |
|---|---|---|
| 관계 정의 | IS-A (~은 ~이다) | HAS-A (~은 ~을 가진다) |
| 결합도 (Coupling) | 강한 결합 (부모 변경 시 자식 영향) | 느슨한 결합 (독립적인 부품화) |
| 캡슐화 | 낮음 (부모의 내부가 자식에게 노출) | 높음 (인터페이스를 통해서만 상호작용) |
| 유연성 | 정적 (컴파일/정의 시점에 결정) | 동적 (런타임에 부품 교체 가능) |
| 주요 문제점 | 상속 계층의 폭발적 증가 | 객체 간 위임 코드가 늘어날 수 있음 |
3. 실무 적용을 위한 7가지 구현 Sample Examples
실제 개발 현장에서 상속의 한계를 극복하기 위해 합성을 어떻게 활용하는지 보여주는 7가지 사례입니다.
Example 1: 다중 로깅 시스템 구현 (Strategy Pattern 활용)
상속을 사용하면 FileLogger, ConsoleLogger 등을 각각 만들어야 하지만, 합성을 쓰면 실행 중에 로거를 교체할 수 있습니다.
class ConsoleHandler:
def emit(self, message):
print(f"[Console]: {message}")
class FileHandler:
def emit(self, message):
with open("log.txt", "a") as f:
f.write(f"[File]: {message}\n")
class Logger:
def __init__(self, handler):
self.handler = handler # 합성: 핸들러 객체를 포함
def log(self, message):
self.handler.emit(message)
# 실무 적용: 상황에 따라 핸들러만 교체
logger = Logger(ConsoleHandler())
logger.log("작업 시작")
logger.handler = FileHandler() # 런타임 교체 가능
logger.log("파일 저장 완료")
Example 2: 게임 캐릭터의 능력치 주입
날아다니는 기사, 수영하는 기사 등 조합이 복잡해질 때 상속은 답이 없습니다.
class FlyBehavior:
def move(self): return "하늘을 납니다."
class SwimBehavior:
def move(self): return "강을 헤엄칩니다."
class Hero:
def __init__(self, move_behavior):
self.move_behavior = move_behavior
def perform_move(self):
print(self.move_behavior.move())
knight = Hero(FlyBehavior())
knight.perform_move()
Example 3: 데이터베이스 연결 엔진 분리
특정 DB에 종속되지 않는 레포지토리 패턴을 합성을 통해 구현합니다.
class MySQLConnector:
def connect(self): return "MySQL Connected"
class PostgresConnector:
def connect(self): return "PostgreSQL Connected"
class UserRepository:
def __init__(self, db_engine):
self.db = db_engine
def get_user(self):
return f"Using {self.db.connect()} to fetch user."
Example 4: 데코레이터를 통한 기능 확장 (Wrapper)
기존 클래스를 수정하지 않고 기능을 덧붙일 때 합성이 유용합니다.
class RawData:
def get_content(self):
return "보안이 중요한 데이터"
class EncryptedData:
def __init__(self, data_source):
self._source = data_source
def get_content(self):
content = self._source.get_content()
return f"Encrypted({content})"
# 상속 없이 기능 추가
secure_data = EncryptedData(RawData())
Example 5: 파이썬 딕셔너리 확장하기 (UserDict 활용)
dict를 직접 상속받으면 예기치 못한 버그가 생길 수 있습니다. 대신 합성을 사용하는 것이 권장됩니다.
from collections import UserDict
class ValidatedDict(UserDict):
def __setitem__(self, key, value):
if not isinstance(key, str):
raise TypeError("Key must be string")
super().__setitem__(key, value)
Example 6: 플러그인 아키텍처 (Component Based)
복잡한 시스템의 각 기능을 컴포넌트로 분리하여 조립합니다.
class Engine:
def start(self): print("Engine start")
class GPS:
def get_location(self): print("Locating...")
class AutonomousCar:
def __init__(self):
self.engine = Engine()
self.gps = GPS()
def drive(self):
self.engine.start()
self.gps.get_location()
Example 7: 권한 검사 로직의 합성
비즈니스 로직과 권한 검사 로직을 분리하여 재사용성을 높입니다.
class AdminPermission:
def check(self): return True
class EditorPermission:
def check(self): return False
class PageService:
def __init__(self, permission_service):
self.permission = permission_service
def delete_page(self):
if self.permission.check():
print("Page deleted")
else:
print("Access Denied")
4. 결론: 합성으로 나아가는 해결 전략
상속은 "진정한 관계의 수직적 계층"이 존재할 때만 사용해야 합니다. 예를 들어, 모든 Cat이 Animal의 한 종류임이 절대 불변일 때입니다. 하지만 기능의 재사용이나 확장이 목적이라면 합성이 정답입니다.
파이썬 개발자로서 우리가 가져야 할 자세는 다음과 같습니다.
- 클래스 설계 전 "이것이 다른 것의 한 종류인가(IS-A)?" 혹은 "이것이 기능을 포함하는가(HAS-A)?"를 먼저 질문하십시오.
- 상속 계층이 3단계를 넘어간다면 즉시 합성을 고려하십시오.
- 인터페이스와 위임(Delegation)을 적극 활용하여 객체 간의 대화를 유도하십시오.
'Artificial Intelligence > 60. Python' 카테고리의 다른 글
| [PYTHON] 믹스인(Mixin) 설계 시 상속 구조 문제를 해결하는 3가지 방법과 실무적 차이점 (0) | 2026.03.29 |
|---|---|
| [PYTHON] TDD 적용 시 코드 구조 설계를 최적화하는 3가지 방법과 실무적 차이점 분석 (0) | 2026.03.29 |
| [PYTHON] 어댑터 패턴으로 레거시 코드를 통합하는 7가지 방법과 구조적 차이 해결 가이드 (0) | 2026.03.29 |
| [PYTHON] unittest와 pytest의 5가지 차이점 분석 및 pytest가 대세가 된 해결 방법 (0) | 2026.03.29 |
| [PYTHON] 외부 API 테스트를 위한 Mocking과 Patching의 3가지 차이점과 해결 방법 (0) | 2026.03.29 |