Composition Over Inheritance#
“Composition Over Inheritance” 는 객체지향 프로그래밍에서 코드 재사용과 유연한 설계를 위해 상속보다 컴포지션을 우선적으로 활용하라는 설계 원칙이다. 상속의 단점인 강한 결합도와 클래스 폭발 문제를 해결하고자 한다. 인터페이스와 컴포지션을 통해 런타임 유연성을 제공하며, 인터페이스와 컴포지션을 통해 런타임 유연성을 제공하며, Bridge, Decorator, Strategy 등 다양한 디자인 패턴의 기반이 된다. 특히 다중 상속의 Diamond Problem 을 회피하고 단일 책임 원칙을 지원하여 현대적인 소프트웨어 설계에서 필수적인 원칙이다.
핵심 개념#
Composition Over Inheritance (컴포지션 우선 원칙) 은 객체지향 프로그래밍에서 클래스들이 상속을 통한 “is-a” 관계보다는 컴포지션을 통한 “has-a” 관계로 기능을 재사용하고 다형성을 구현해야 한다는 설계 원칙이다.
- 컴포지션 (Composition, 구성): 객체가 다른 객체를 포함 (참조) 하여 기능을 조합하는 방식
- 상속 (Inheritance, 상속): 부모 클래스의 속성과 메서드를 자식 클래스가 물려받는 방식.
전략 패턴 (Strategy Pattern), 데코레이터 패턴 (Decorator Pattern) 등 다양한 디자인 패턴에서 이 원칙이 적용된다.
역사적 배경:
- 1990 년대 초 객체지향 프로그래밍이 대중화되면서 상속 남용 문제 발생
- GoF(Gang of Four) 가 1994 년 “Design Patterns” 책에서 공식적으로 제시
- 상속의 단점인 강한 결합도, 클래스 폭발, 다중 상속 문제 해결 필요성 대두
문제 상황:
- 상속 계층이 깊어질수록 복잡성 증가
- 부모 클래스 변경 시 모든 자식 클래스에 영향
- 다중 상속에서 Diamond Problem 발생
- 런타임에 행동 변경의 어려움
목적 및 필요성#
목적 | 설명 |
---|
유연성 향상 | 런타임에 객체의 행동을 동적으로 변경 가능 |
재사용성 증대 | 기존 컴포넌트를 조합하여 새로운 기능 구현 |
결합도 감소 | 클래스 간 의존성을 최소화하여 독립적 개발 |
테스트 용이성 | Mock 객체를 이용한 단위 테스트 용이 |
유지보수성 | 변경 사항이 다른 클래스에 미치는 영향 최소화 |
주요 원리 및 작동 원리#
객체가 필요한 기능 (행동) 을 인터페이스로 정의하고, 해당 기능을 구현한 객체를 조합하여 원하는 기능을 완성한다.
작동 원리 다이어그램#
graph TD
A[Client] --> B[CompositeClass]
B --> C[Interface1]
B --> D[Interface2]
C --> E[Implementation1A]
C --> F[Implementation1B]
D --> G[Implementation2A]
D --> H[Implementation2B]
style A fill:#e1f5fe
style B fill:#f3e5f5
style C fill:#e8f5e8
style D fill:#e8f5e8
style E fill:#fff3e0
style F fill:#fff3e0
style G fill:#fff3e0
style H fill:#fff3e0
상속 (Inheritance) vs. 컴포지션 (Composition) 비교#
상속은 명확한 계층적 관계와 다형성이 필요한 경우에 적합하며, 컴포지션은 유연성, 재사용성, 테스트 용이성이 중요한 현대적 소프트웨어 개발에 더 적합하다. “Composition Over Inheritance” 원칙에 따라 컴포지션을 기본으로 선택하고, 명확한 is-a 관계가 존재하는 경우에만 상속을 사용하는 것이 권장된다.
구분 | 상속 (Inheritance) | 컴포지션 (Composition) |
---|
정의 | 부모 클래스로부터 속성과 메소드를 물려받는 메커니즘 | 다른 객체의 인스턴스를 포함하여 기능을 재사용하는 메커니즘 |
관계 유형 | “is-a” 관계 (예: 자동차는 차량이다) | “has-a” 관계 (예: 자동차는 엔진을 가진다) |
결합도 | 강한 결합 (Tight Coupling) | 느슨한 결합 (Loose Coupling) |
가시성 | White-box 재사용 (내부 구조 노출) | Black-box 재사용 (내부 구조 숨김) |
기술적 특성 비교#
특성 | 상속 (Inheritance) | 컴포지션 (Composition) |
---|
런타임 유연성 | 컴파일타임에 관계 고정 | 런타임에 동적 변경 가능 |
다중 기능 조합 | 다중 상속 시 Diamond Problem 발생 | 여러 컴포넌트 자유롭게 조합 가능 |
인터페이스 노출 | 부모의 모든 public 메소드 상속 | 필요한 인터페이스만 노출 |
메소드 재정의 | 오버라이딩을 통한 행동 변경 | 델리게이션을 통한 행동 위임 |
메모리 사용 | 단일 객체 인스턴스 | 여러 객체 인스턴스 (약간의 오버헤드) |
성능 | 직접 메소드 호출 | 간접 메소드 호출 (미미한 성능 비용) |
설계 관점 비교#
설계 측면 | 상속 (Inheritance) | 컴포지션 (Composition) |
---|
SOLID 원칙 준수 | 단일 책임 원칙 위반 가능성 높음 | 단일 책임 원칙 준수 용이 |
개방 - 폐쇄 원칙 | 부분적 지원 (확장은 쉬우나 수정 위험) | 완전 지원 (확장 쉽고 수정 불필요) |
리스코프 치환 원칙 | 준수 필수 (위반 시 설계 오류) | 해당 없음 (인터페이스 기반) |
캡슐화 | 부모 클래스 세부사항 노출 | 완전한 캡슐화 유지 |
응집도 | 부모 - 자식 간 강한 응집 | 각 컴포넌트의 독립적 응집 |
개발 및 유지보수 비교#
개발 측면 | 상속 (Inheritance) | 컴포지션 (Composition) |
---|
코드 작성량 | 적음 (자동으로 메소드 상속) | 많음 (위임 메소드 작성 필요) |
초기 개발 속도 | 빠름 | 느림 (인터페이스 설계 필요) |
테스트 용이성 | 어려움 (부모 클래스 의존) | 쉬움 (Mock 객체 활용 가능) |
디버깅 | 복잡함 (상속 체인 추적) | 단순함 (명확한 호출 경로) |
리팩토링 | 어려움 (영향 범위 넓음) | 쉬움 (독립적 컴포넌트) |
문서화 | 상속 관계 이해 필요 | 컴포넌트별 독립적 문서화 |
확장성 및 재사용성 비교#
확장성 측면 | 상속 (Inheritance) | 컴포지션 (Composition) |
---|
새 기능 추가 | 상속 체인 수정 또는 새 클래스 생성 | 새 컴포넌트 추가 및 조합 |
기존 기능 수정 | 모든 하위 클래스에 영향 | 해당 컴포넌트만 수정 |
코드 재사용 | 수직적 재사용 (계층적) | 수평적 재사용 (조합적) |
기능 조합 | 제한적 (클래스 폭발 위험) | 자유로운 조합 |
버전 관리 | 호환성 유지 어려움 | 컴포넌트별 독립적 버전 관리 |
사용 시나리오 비교#
시나리오 | 상속 권장 | 컴포지션 권장 | 이유 |
---|
명확한 is-a 관계 | ✅ | ❌ | 개념적 모델링에 적합 |
코드 재사용 목적 | ❌ | ✅ | 유연한 재사용 구조 |
다형성 구현 | ✅ | ✅ | 둘 다 가능, 컴포지션이 더 유연 |
기능 확장 | ❌ | ✅ | 런타임 확장성 |
테스트 중심 개발 | ❌ | ✅ | Mock 객체 활용 |
라이브러리 설계 | ❌ | ✅ | 사용자 확장성 |
프레임워크 설계 | 부분적 | ✅ | 플러그인 아키텍처 |
도메인 모델링 | ✅ | ✅ | 상황에 따라 선택 |
성능 비교#
성능 측면 | 상속 (Inheritance) | 컴포지션 (Composition) |
---|
메소드 호출 비용 | 직접 호출 (빠름) | 간접 호출 (약간 느림) |
메모리 사용량 | 적음 | 약간 많음 (추가 객체) |
객체 생성 비용 | 적음 | 많음 (여러 객체 생성) |
캐시 지역성 | 좋음 | 보통 |
JIT 최적화 | 쉬움 | 보통 |
실제 성능 영향 | 거의 없음 | 거의 없음 (마이크로 벤치마크 수준) |
상속 (Inheritance) vs. 컴포지션 (Composition) 예시#
상속 (Inheritance)#
1
2
3
4
5
6
7
8
9
10
11
12
| # 부모 클래스 정의
class Animal:
def speak(self):
print("동물이 소리를 냅니다.")
# 자식 클래스가 부모 클래스를 상속
class Dog(Animal):
def speak(self):
print("멍멍!")
dog = Dog()
dog.speak() # 출력: 멍멍!
|
graph LR
subgraph "상속 (Inheritance)"
A1[BaseClass] --> A2[DerivedClass1]
A1 --> A3[DerivedClass2]
A2 --> A4[SubDerivedClass]
end
style A1 fill:#ffcdd2
style A2 fill:#ffcdd2
style A3 fill:#ffcdd2
style A4 fill:#ffcdd2
컴포지션 (Composition)#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| # 동물의 행동을 별도의 클래스로 분리
class SpeakBehavior:
def speak(self):
print("동물이 소리를 냅니다.")
class Dog:
def __init__(self, speak_behavior):
self.speak_behavior = speak_behavior
def speak(self):
self.speak_behavior.speak()
dog = Dog(SpeakBehavior())
dog.speak() # 출력: 동물이 소리를 냅니다.
|
graph LR
subgraph "컴포지션 (Composition)"
B1[MainClass] --> B2[Component1]
B1 --> B3[Component2]
B1 --> B4[Component3]
end
style B1 fill:#c8e6c9
style B2 fill:#c8e6c9
style B3 fill:#c8e6c9
style B4 fill:#c8e6c9
구조 및 아키텍처#
구성 요소#
구성 요소 | 기능 | 역할 |
---|
인터페이스/추상 클래스 | 계약 정의 | 구현체들의 공통 규격 제공 |
구현 클래스들 | 실제 기능 구현 | 특정 행동의 구체적 구현 |
컴포지트 클래스 | 객체 조합 | 여러 컴포넌트를 조합하여 새로운 기능 제공 |
델리게이션 메커니즘 | 작업 위임 | 적절한 컴포넌트에게 작업 전달 |
팩토리 클래스 | 객체 생성 관리 | 적절한 구현체 선택 및 생성 |
설정 관리자 | 구성 정보 관리 | 런타임 구성 변경 지원 |
아키텍처 다이어그램#
graph TB
subgraph "클라이언트 계층"
Client[클라이언트 코드]
end
subgraph "조합 계층"
Composite[컴포지트 클래스]
Factory[팩토리]
end
subgraph "인터페이스 계층"
IFilter[IFilter 인터페이스]
ILogger[ILogger 인터페이스]
IHandler[IHandler 인터페이스]
end
subgraph "구현 계층"
TextFilter[텍스트 필터]
LevelFilter[레벨 필터]
FileLogger[파일 로거]
ConsoleLogger[콘솔 로거]
EmailHandler[이메일 핸들러]
DBHandler[DB 핸들러]
end
Client --> Composite
Client --> Factory
Composite --> IFilter
Composite --> ILogger
Composite --> IHandler
IFilter --> TextFilter
IFilter --> LevelFilter
ILogger --> FileLogger
ILogger --> ConsoleLogger
IHandler --> EmailHandler
IHandler --> DBHandler
Factory --> TextFilter
Factory --> FileLogger
Factory --> EmailHandler
style Client fill:#e3f2fd
style Composite fill:#f3e5f5
style Factory fill:#f3e5f5
style IFilter fill:#e8f5e8
style ILogger fill:#e8f5e8
style IHandler fill:#e8f5e8
style TextFilter fill:#fff3e0
style LevelFilter fill:#fff3e0
style FileLogger fill:#fff3e0
style ConsoleLogger fill:#fff3e0
style EmailHandler fill:#fff3e0
style DBHandler fill:#fff3e0
구현 기법#
기법명 | 정의 및 구성 | 목적 | 예시/시나리오 |
---|
인터페이스 기반 컴포지션 | 인터페이스로 행동 정의, 구현체 주입 | 행동의 유연한 확장 | 전략 패턴 (Strategy Pattern) |
데코레이터 패턴 | 객체에 기능을 동적으로 추가 | 런타임 기능 확장 | 데코레이터 패턴 (Decorator Pattern) |
의존성 주입 (DI) | 외부에서 구현 객체 주입 | 결합도 감소, 테스트 용이성 향상 | 스프링 프레임워크의 DI |
장점과 단점#
구분 | 항목 | 설명 |
---|
✅ 장점 | 유연성 | 런타임에 행동 변경 및 확장 가능, 유지보수성 우수 |
| 캡슐화 | 객체 내부 구현 숨김, 결합도 낮음 |
| 재사용성 | 기능 단위별로 객체 조합, 다양한 조합 가능 |
| 테스트 용이성 | 각 컴포넌트 단위 테스트 가능 |
⚠ 단점 | 구현 복잡성 | 설계가 복잡해질 수 있음, 객체 조합 관리 필요 |
| 코드 길이 | 단순한 경우 상속보다 코드가 길어질 수 있음 |
| 성능 오버헤드 | 객체 참조 및 위임 (Delegation) 으로 인한 오버헤드 발생 가능 |
상속의 문제점#
합성 우선 상속 원칙이 등장한 이유는 상속이 가진 여러 문제점 때문이다.
강한 결합도:
상속은 부모 클래스와 자식 클래스 사이에 강한 결합을 만든다. 부모 클래스의 변경이 모든 자식 클래스에 영향을 미친다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| class Animal {
void breathe() {
System.out.println("Breathing");
}
void eat() {
System.out.println("Eating normally");
}
}
class Dog extends Animal {
void bark() {
System.out.println("Woof");
}
}
// 나중에 Animal 클래스의 eat() 메서드가 변경됨
class Animal {
void breathe() {
System.out.println("Breathing");
}
void eat() {
validateFood(); // 새로운 검증 메서드 추가
System.out.println("Eating with validation");
}
private void validateFood() {
// 검증 로직
}
}
|
- 이 변경은 Dog 클래스의 동작에도 영향을 미친다. Dog 클래스의 개발자는 이러한 변경을 예상하지 못했을 수 있다.
취약한 베이스 클래스 문제:
부모 클래스의 변경이 자식 클래스를 망가뜨릴 수 있는 상황을 취약한 베이스 클래스 문제 라고 한다. 이것은 상속의 주요 문제점 중 하나이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| class Stack {
protected List<Object> elements = new ArrayList<>();
public void push(Object item) {
elements.add(item);
}
public Object pop() {
if (elements.isEmpty()) {
throw new EmptyStackException();
}
return elements.remove(elements.size() - 1);
}
public int size() {
return elements.size();
}
}
// Stack을 확장하는 MyStack
class MyStack extends Stack {
public void pushAll(Collection<?> items) {
for (Object item : items) {
push(item);
}
}
}
// 나중에 Stack 클래스가 변경됨
class Stack {
// 변경: elements를 private으로 변경
private List<Object> elements = new ArrayList<>();
// 나머지 메서드는 동일
}
|
- 이 변경으로 인해 MyStack 클래스의 pushAll 메서드는 더 이상 작동하지 않게 된다.
다중 상속의 제한:
많은 언어 (Java, C# 등) 는 단일 상속만 지원한다. 이는 여러 소스에서 기능을 상속받아야 할 경우 심각한 제한이 된다.
계층 구조의 경직성:
상속은 컴파일 타임에 결정되므로, 런타임에 객체의 행동을 변경하기 어렵다. 이는 설계의 유연성을 제한한다.
도전 과제 및 해결책#
분류 | 도전 과제 | 설명 | 해결책 |
---|
구조적 문제 | 다중 상속 문제 (Diamond Problem) | 여러 클래스가 같은 상위 클래스를 상속받을 때 메서드 호출이 모호해짐 | 인터페이스 기반 설계, 가상 상속 (Virtual Inheritance), 컴포지션 기반 설계 전환 |
설계 문제 | 클래스 폭발 (Class Explosion) | 기능 조합이 많아질수록 클래스 수가 지수적으로 증가 | 런타임 컴포지션 활용, 팩토리 패턴 적용, 설정 기반 동적 구성 |
코드 품질 | 보일러플레이트 코드 | 위임 메서드, 생성자 코드 등 반복되는 코드가 많아짐 | Python __getattr__ , Kotlin 위임, 코드 생성 도구, 추상화 레벨 조정 |
설계 복잡성 | 객체 관계 복잡화 | 조합된 객체 간 관계가 많아져 설계 이해와 유지보수 어려움 | 명확한 인터페이스 설계, UML/문서화, 설계 패턴 활용 (예: 전략 패턴, 퍼사드 패턴 등) |
운영 문제 | 객체 생성/관리 어려움 | 조합 객체의 수명 주기와 생성 방식 관리가 어려움 | DI(Dependency Injection) 프레임워크 활용 (Spring, NestJS 등), 생명주기 관리 자동화 도구 |
성능 문제 | 성능 저하 가능성 | 조합 계층이 깊어질수록 메서드 호출 스택 증가 및 처리 지연 발생 | 필요한 조합만 유지, 병목 부분은 수동 최적화, 캐싱 및 Lazy Initialization 적용 |
유지보수 문제 | 테스트 복잡성 | 조합된 객체 간 단위 테스트 분리가 어려울 수 있음 | 의존성 분리를 통한 테스트 더블 (Mock/Stub) 활용, 단일 책임 설계 |
분류에 따른 종류 및 유형#
컴포지션 종류#
종류 | 설명 | 특징 | 예시 |
---|
집합 (Aggregation) | 부분이 전체와 독립적으로 존재 | 약한 관계, 공유 가능 | 대학교 - 학생 |
합성 (Composition) | 부분이 전체에 종속적으로 존재 | 강한 관계, 생명주기 동일 | 자동차 - 엔진 |
연관 (Association) | 객체 간 사용 관계 | 독립적 존재, 참조 관계 | 사람 - 직업 |
패턴 유형#
유형 | 패턴 | 목적 | 컴포지션 활용 방식 |
---|
구조적 패턴 | Adapter, Bridge, Decorator | 인터페이스 적응 및 확장 | 래핑과 위임 |
행동 패턴 | Strategy, Observer, Command | 알고리즘 및 행동 교체 | 행동 객체 조합 |
생성 패턴 | Builder, Abstract Factory | 복잡한 객체 생성 | 컴포넌트 조립 |
구현 방식#
유형 | 설명 |
---|
인터페이스 기반 컴포지션 | 인터페이스로 행동 정의, 구현체 주입 |
객체 참조 기반 컴포지션 | 객체를 멤버 변수로 포함 |
실무 적용 예시#
분야 | 적용 사례 | 컴포지션 활용 방식 | 이점 |
---|
웹 프레임워크 | Spring Framework DI, 미들웨어 구성 | 객체 조합, 의존성 주입 (Dependency Injection) | 느슨한 결합, 테스트 용이성, 유지보수 용이 |
로깅 시스템 | Java Logging, Python logging | Handler, Filter, Formatter 의 동적 조합 | 유연한 로그 처리 파이프라인 구성 |
UI 프레임워크 | React 컴포넌트 시스템 | 재사용 가능한 컴포넌트의 합성 및 데코레이터 패턴 | 모듈화, 유지보수 용이, 확장성 높음 |
게임 엔진 | Unity 의 GameObject-Component 구조 | GameObject 에 컴포넌트 단위 기능을 조합 | 기능 단위 분리, 동적 조합, 재사용성 |
데이터베이스 ORM | Hibernate, JPA 의 연관 매핑 구조 | Entity 간의 조합 및 위임 | 객체 - 관계 매핑 유연성 |
마이크로서비스 | API Gateway, Service Mesh 구성 | 서비스 단위 조합 및 위임 | 확장성, 서비스 독립성, 탄력적 구조 |
결제 시스템 | 전략 패턴 기반 결제 수단 선택 시스템 | PaymentStrategy 인터페이스로 동적 조합 | 다양한 결제 방식 유연 대응 |
웹 요청 처리 | Express.js, Django 미들웨어 체인 구성 | 요청 처리 흐름을 함수형 컴포넌트로 조합 | 책임 분리, 확장성 |
프론트엔드 UI 확장 | 데코레이터 패턴 기반 기능 확장 (ex. Tooltip) | 기본 UI 에 기능을 조합하여 동적으로 확장 | 코드 재사용, 유연한 UI 동작 |
도메인 객체 설계 | 사용자, 주문, 상품 등의 도메인 조합 | 독립 도메인 객체를 조합하여 기능을 구성 | 높은 응집도, 결합도 낮춤 |
활용 사례#
사례 1: 온라인 쇼핑몰의 결제 시스템#
시나리오: 온라인 쇼핑몰의 결제 시스템에 다양한 결제 수단을 지원.
시스템 구성:
- PaymentStrategy 인터페이스: 결제 방식의 공통 인터페이스를 정의한다.
- CreditCardPayment, PayPalPayment 클래스: PaymentStrategy 인터페이스를 구현하여 각각의 결제 방식을 제공한다.
- ShoppingCart 클래스: PaymentStrategy 객체를 조합하여 결제 기능을 구성한다.
구성 다이어그램:
classDiagram
class PaymentStrategy {
<<interface>>
+pay(amount: float): void
}
class CreditCardPayment {
+pay(amount: float): void
}
class PayPalPayment {
+pay(amount: float): void
}
class ShoppingCart {
-paymentStrategy: PaymentStrategy
+setPaymentStrategy(strategy: PaymentStrategy): void
+checkout(amount: float): void
}
PaymentStrategy <|-- CreditCardPayment
PaymentStrategy <|-- PayPalPayment
ShoppingCart --> PaymentStrategy
워크플로우:
- 사용자가 결제 수단을 선택한다.
- 선택된 결제 수단에 해당하는 PaymentStrategy 구현체가 ShoppingCart 에 주입된다.
ShoppingCart
는 주입된 PaymentStrategy
를 이용하여 pay(amount)
메서드를 호출하고, 해당 구현체의 로직에 따라 결제가 처리된다.- 결제 결과에 따라 후속 처리 (주문 상태 변경, 알림 발송 등) 를 수행한다.
Workflow 다이어그램:
sequenceDiagram
participant User
participant ShoppingCart
participant PaymentStrategy
participant PayPalPayment
participant CreditCardPayment
User->>ShoppingCart: setPaymentStrategy(PayPalPayment)
User->>ShoppingCart: checkout(100.00)
ShoppingCart->>PaymentStrategy: pay(100.00)
PaymentStrategy->>PayPalPayment: pay(100.00)
PayPalPayment-->>ShoppingCart: 결제 성공
ShoppingCart-->>User: 주문 완료 알림
담당 역할
구성 요소 | 역할 설명 |
---|
PaymentStrategy | 다양한 결제 방식의 공통 인터페이스 제공 |
CreditCardPayment | 신용카드 결제 기능 구현 |
PayPalPayment | 페이팔 결제 기능 구현 |
ShoppingCart | 사용자가 선택한 전략 객체를 조합하여 결제 처리 수행 |
사례 2: 게임 캐릭터 시스템#
시나리오: 캐릭터는 점프, 공격, 방어 등 다양한 능력을 가질 수 있음. 각 능력을 컴포넌트로 구현하고, 캐릭터 객체에 필요한 능력을 조합하여 부여.
시스템 구성 및 다이어그램:
classDiagram
class Character {
+addAbility(Ability)
+useAbility()
}
class Ability {
+execute()
}
class JumpAbility {
+execute()
}
class AttackAbility {
+execute()
}
Character --> Ability : has-a
Ability <|-- JumpAbility
Ability <|-- AttackAbility
Workflow:
- 캐릭터 객체 생성
- 점프, 공격 등 능력 (Ability) 컴포넌트 생성
- 캐릭터에 필요한 능력 추가
- 캐릭터가 상황에 따라 능력 사용
역할:
- Character: 능력 관리 및 실행
- Ability: 행동 인터페이스
- JumpAbility/AttackAbility: 구체적 행동 구현
사례 3: 로깅 시스템 구현#
시나리오: 대규모 웹 애플리케이션에서 다양한 레벨의 로그를 여러 출력 대상 (콘솔, 파일, 데이터베이스, 이메일) 으로 전송하고, 각각에 다른 필터링 규칙을 적용해야 하는 상황
시스템 구성:
graph TB
subgraph "클라이언트 애플리케이션"
App[Web Application]
end
subgraph "로깅 시스템"
Logger[Logger]
FilterChain[Filter Chain]
HandlerManager[Handler Manager]
end
subgraph "필터 컴포넌트"
LevelFilter[Level Filter]
TextFilter[Text Filter]
TimeFilter[Time Filter]
end
subgraph "핸들러 컴포넌트"
ConsoleHandler[Console Handler]
FileHandler[File Handler]
DBHandler[Database Handler]
EmailHandler[Email Handler]
end
subgraph "포맷터 컴포넌트"
SimpleFormatter[Simple Formatter]
JSONFormatter[JSON Formatter]
XMLFormatter[XML Formatter]
end
App --> Logger
Logger --> FilterChain
Logger --> HandlerManager
FilterChain --> LevelFilter
FilterChain --> TextFilter
FilterChain --> TimeFilter
HandlerManager --> ConsoleHandler
HandlerManager --> FileHandler
HandlerManager --> DBHandler
HandlerManager --> EmailHandler
ConsoleHandler --> SimpleFormatter
FileHandler --> JSONFormatter
DBHandler --> JSONFormatter
EmailHandler --> XMLFormatter
style App fill:#e3f2fd
style Logger fill:#f3e5f5
style FilterChain fill:#f3e5f5
style HandlerManager fill:#f3e5f5
style LevelFilter fill:#e8f5e8
style TextFilter fill:#e8f5e8
style TimeFilter fill:#e8f5e8
style ConsoleHandler fill:#fff3e0
style FileHandler fill:#fff3e0
style DBHandler fill:#fff3e0
style EmailHandler fill:#fff3e0
활용 사례 Workflow:
sequenceDiagram
participant App as Application
participant Logger as Logger
participant FC as FilterChain
participant HM as HandlerManager
participant CH as ConsoleHandler
participant FH as FileHandler
participant EH as EmailHandler
App->>Logger: log("ERROR: Database connection failed")
Logger->>FC: filter(message)
FC->>FC: LevelFilter.filter(ERROR)
FC->>FC: TextFilter.filter("Database")
FC->>Logger: return true (passed)
Logger->>HM: handle(message)
HM->>CH: handle(message)
CH->>CH: format & output to console
HM->>FH: handle(message)
FH->>FH: format & write to file
HM->>EH: handle(message)
EH->>EH: format & send email alert
Logger->>App: return success
컴포지션의 역할:
유연한 구성:
- 런타임에 필터와 핸들러 조합 변경 가능
- 새로운 출력 형식 추가 시 기존 코드 수정 불필요
독립적 테스트:
- 각 컴포넌트를 Mock 객체로 대체하여 단위 테스트
- 통합 테스트에서 실제 구현체 조합
확장성:
- 새로운 필터 유형 추가 (예: 사용자별 필터)
- 새로운 출력 대상 추가 (예: Slack, Teams)
실무에서 효과적으로 적용하기 위한 고려사항 및 주의할 점#
구분 | 항목 | 설명 | 권장사항 |
---|
설계 단계 | 인터페이스 정의 | 기능 단위로 인터페이스를 분리하여 각 객체의 책임을 명확히 함 | SRP(단일 책임 원칙) 기반 설계, 공통 동작을 추상화하여 전략 객체 설계 |
| 책임 분리 | 조합된 객체에 과도한 책임이 몰리는 것을 방지 | 한 객체가 하나의 역할만 수행하도록 분리 (SoC: Separation of Concerns) |
| 설계 복잡성 관리 | 조합 구조가 복잡해지면 이해와 유지보수가 어려워짐 | 설계 문서화, UML 다이어그램 활용, 리뷰 문화 도입 |
구현 단계 | 객체 수명 주기 관리 | 동적으로 조합되는 객체의 생성/소멸 및 공유 범위 명확히 관리 필요 | DI(Dependency Injection) 프레임워크 활용, Singleton/Lifecycle 적용 |
| 코드 중복 방지 | 위임 메서드 등에서 반복 코드 발생 가능성 존재 | Python: __getattr__ , Kotlin: 위임 키워드, Java: Lombok 사용 |
성능 관리 | 메소드 호출 오버헤드 | 컴포지션으로 인해 호출 계층이 증가하면서 미세한 성능 저하 발생 가능 | 성능 민감한 로직은 직접 구현 (Flat Delegation), Hot Path 는 단순화 |
| 메모리 사용 최적화 | 불필요한 객체 다수 생성 시 메모리 과다 사용 가능 | Singleton, 객체 풀링, Lazy Initialization 적용 |
테스트/운영 | 테스트 용이성 확보 | 각 조합 단위 객체에 대한 테스트가 어려울 수 있음 | DI 기반 Mock 객체 주입, 단위 테스트 시나리오 설계 |
| 변경 대응력 | 객체 간 조합 관계가 많을수록 변경에 의한 영향도 증가 | 인터페이스 기반 설계, 버전 관리 전략 도입, Contract 기반 개발 도입 |
| 코드 가독성 | 조합된 객체 간의 흐름이 명확하지 않으면 유지보수가 어려움 | 가독성 중심의 설계, 주석 또는 문서화를 통한 의도 전달 |
- 기능 단위 분리와 인터페이스화를 선행하지 않으면 컴포지션의 이점이 퇴색된다.
- 조합 대상의 책임이 명확하지 않거나 너무 많을 경우 구조가 오히려 상속보다 더 복잡해진다.
- 의존성 주입 없이 직접 객체 생성 시, 객체 수명 및 테스트 통제가 어려워질 수 있다.
- 설계 문서와 코드 간 괴리가 생기지 않도록, 초기 설계와 실제 구현을 지속적으로 동기화해야 한다.
- 성능 저하 요소(과도한 위임, 깊은 객체 참조 등) 는 반드시 사전 프로파일링과 모니터링으로 검증해야 한다.
최적화하기 위한 고려사항 및 주의할 점#
카테고리 | 항목 | 설명 | 권장사항 |
---|
메서드 호출 | 호출 계층 최소화 | 조합이 깊어질수록 호출 체인이 길어지고 스택 오버헤드 증가 가능 | 핵심 기능만 조합, 불필요한 위임 제거, 인라인 최적화 또는 함수 캐싱 활용 |
메서드 호출 | 위임 오버헤드 | 위임 방식이 많아질 경우 메서드 호출 체인으로 인한 성능 저하 발생 | 위임은 핵심 로직에 최소화, 중첩 조합 대신 단일 목적 함수로 분리 |
객체 생성 | 초기화 비용 | 전략 객체나 조합 객체가 복잡한 초기화 과정을 가지면 성능 저하 요인 | Lazy Initialization(지연 초기화), Singleton 또는 프로토타입 패턴 활용 |
객체 생성 | 객체 과다 생성 | 조합된 구조에서 반복적으로 객체 생성 시 GC 부담 증가 | Factory 패턴 활용, 객체 풀링 적용 (특히 I/O, 네트워크 객체 등) |
메모리 | 메모리 사용 최적화 | 상태를 유지하는 전략 객체나 중첩 조합은 메모리 사용량을 증가시킬 수 있음 | Stateless 설계 우선, 약한 참조 (weakref , WeakMap ) 활용, 불필요한 참조 제거 |
런타임 동작 | Reflection 및 동적 바인딩 | Reflection, eval, Proxy 등 런타임 해석 기반 조합은 CPU 오버헤드 유발 | 컴파일 타임 검증 가능한 언어 구조 활용 (Java interface, TypeScript 타입 등) |
동시성 | 스레드 안전성 확보 | 조합 객체 간 상태 공유 시 동시성 이슈 발생 가능 | Immutable 객체 설계, 필요한 경우 최소 범위의 동기화 적용 |
관찰 및 대응 | 성능 병목 진단 및 모니터링 | 조합 구조는 문제 발생 위치 추적이 어려움 | APM, 프로파일러 도구 활용 (예: Py-Spy, JFR, Chrome DevTools), 병목 로그화 |
주제와 관련하여 주목할 내용#
주제 | 항목 | 설명 |
---|
함수형 프로그래밍 | 고차 함수 (Higher-Order Functions) | 함수를 조합하여 새로운 기능 구현 |
마이크로서비스 | 서비스 컴포지션 패턴 | 여러 서비스를 조합하여 복잡한 비즈니스 로직 구현 |
리액티브 프로그래밍 | 스트림 컴포지션 | 데이터 스트림을 조합하여 복잡한 데이터 플로우 구성 |
도메인 주도 설계 | 애그리게이트 패턴 | 도메인 객체들을 조합하여 비즈니스 규칙 구현 |
클라우드 아키텍처 | 서버리스 컴포지션 | 함수들을 조합하여 애플리케이션 구성 |
AI/ML | 파이프라인 컴포지션 | 데이터 전처리, 모델 훈련, 예측을 조합한 ML 파이프라인 |
하위 주제로 분류해서 추가적으로 학습해야 할 내용#
카테고리 | 주제 | 간략한 설명 |
---|
객체지향 설계 원칙 | SOLID 원칙 | 객체 지향 프로그래밍의 5 가지 핵심 원칙 |
디자인 패턴 | 전략 패턴 | 동작을 캡슐화하여 런타임에 교체 가능하게 하는 패턴 |
디자인 패턴 | 데코레이터 패턴 | 기존 기능을 변경하지 않고 기능을 확장할 수 있는 패턴 |
테스트 전략 | 테스트 더블 (Mock, Stub 등) | 외부 의존성을 대체하여 유닛 테스트를 용이하게 함 |
DI 프레임워크 | Spring DI / NestJS DI | 구성 기반 설계의 실제 구현을 돕는 프레임워크 |
리팩토링 기법 | 상속 → 조합 전환 | 기존 상속 기반 구조를 조합 기반으로 리팩토링하는 기법 |
동시성 패턴 | Actor 모델 | 액터들의 조합을 통한 동시성 시스템 구축 |
관련 분야별 추가 학습 내용#
관련 분야 | 주제 | 설명 |
---|
소프트웨어 아키텍처 | 헥사고날 아키텍처 | 포트와 어댑터를 통한 비즈니스 로직과 외부 시스템 분리 |
데이터베이스 설계 | Repository 패턴 | 데이터 액세스 로직의 추상화와 조합 |
웹 개발 | 미들웨어 패턴 | HTTP 요청 처리 과정에서 기능들의 조합 |
게임 개발 | Component-Entity 시스템 | 게임 객체를 컴포넌트들의 조합으로 구성 |
DevOps | Infrastructure as Code | 인프라 구성요소들의 선언적 조합 |
분산 시스템 | 사가 패턴 (Saga Pattern) | 분산 트랜잭션을 작은 트랜잭션들의 조합으로 구현 |
용어 정리#
객체지향 설계 핵심 개념#
용어 | 설명 |
---|
컴포지션 (Composition) | 기능을 객체 간 조합으로 구성하는 방식 |
상속 (Inheritance) | 부모 클래스의 속성을 자식 클래스가 물려받는 구조 |
Delegation (위임) | 책임을 다른 객체에 위임하여 수행하는 구조 |
캡슐화 (Encapsulation) | 객체의 내부 상태를 외부에서 숨기고 인터페이스만 노출 |
인터페이스 (Interface) | 기능 명세를 정의하는 추상적 계약 |
덕 타이핑 (Duck Typing) | 객체의 타입보다 ’ 행동 ’ 을 기준으로 간주하는 방식 (동적 언어에 많음) |
인터페이스 기반 설계 | 구현보다 인터페이스를 중심으로 설계하여 유연성과 결합도 최소화 확보 |
객체지향 설계 원칙#
원칙 | 설명 |
---|
SOLID | 객체지향 5 대 원칙 (SRP, OCP, LSP, ISP, DIP) 의 집합적 용어 |
OCP (Open-Closed Principle) | 확장에는 열려 있고 변경에는 닫혀 있어야 한다 |
Liskov Substitution Principle | 자식 클래스는 부모 클래스를 대체할 수 있어야 한다 |
디자인 패턴#
패턴 | 설명 |
---|
전략 패턴 (Strategy Pattern) | 알고리즘을 객체화하고 런타임에 교체 가능하게 하는 패턴 |
데코레이터 패턴 (Decorator Pattern) | 기존 객체에 기능을 동적으로 확장하는 패턴 |
의존성 주입 (Dependency Injection) | 외부에서 의존 객체를 주입받아 결합도를 낮추는 구조 |
Mixin | 다중 클래스에 공통 기능을 주입하기 위한 재사용 기술 (주로 다중 상속 대안) |
소프트웨어 품질 속성 (결합도/응집도)#
용어 | 설명 |
---|
결합도 (Coupling) | 클래스/모듈 간의 의존성 정도 |
응집도 (Cohesion) | 클래스/모듈 내부 요소의 목적 집중성 |
Loose Coupling (느슨한 결합) | 낮은 결합도를 유지하여 유연한 변경 대응 구조 |
Tight Coupling (강한 결합) | 높은 결합도로 인해 변경 전파가 크고 유지보수가 어려운 구조 |
객체지향 구조 문제 및 한계#
문제 | 설명 |
---|
Diamond Problem | 다중 상속으로 인한 메서드 충돌 모호성 문제 |
Fragile Base Class Problem | 부모 클래스 변경 시 자식 클래스가 예기치 않게 깨지는 현상 |
성능 및 최적화 관련 기법#
기법 | 설명 |
---|
Lazy Initialization | 실제 사용할 때까지 객체 생성을 지연시키는 기법 |
객체 풀링 (Object Pooling) | 반복 객체 생성을 방지하고 재사용하여 성능 향상 |
프로파일링 (Profiling) | 실행 중인 애플리케이션의 성능 병목/자원 사용 분석 도구 |
참고 및 출처#