1. 컴포지션(Composition)
1️⃣ 컴포지션이란?
1) 개념
상속 대신 포함(has-a 관계)을 사용하는 설계 방식
객체지향 프로그래밍(OOP)에서 객체를 만들 때, 다른 객체를 포함해서 기능을 구성하는 설계 방식
상속으로 물려받는 대신, 필요한 기능을 가진 객체를 내부에 넣어서 조합해 사용하는 것
상속은 하나만 할 수 있지만, 컴포지션은 여러 개를 결합할 수 있음
A가 B를 상속한다 ❌A가 B를 가지고 사용한다 ⭕
- 상속(Inheritance): is-a 관계
- 컴포지션(Composition): has-a 관계
composition - (영어 뜻) 결합하다 → (프로그래밍에서) has 상태로 가지다
2) 예시
- ❌ 자동차 is a 엔진
- ⭕ 자동차 has an 엔진 → 컴포지션
※ 자동차 만들 때 엔진은 extends 할 수 없음.
자동차는 엔진이 아니기 때문에 타입 일치 할 수 없음.
문법적으로는 문제 없지만 객체지향프로그래밍면에서 안 맞는 것!
ex) 버거 + 콜라 + 감자 → 버거세트
2️⃣ 컴포지션의 목적
컴포지션의 목적은 상속보다 더 유연하고, 변경에 강하고, 재사용 가능한 객체를 만들기 위해 기능을 조합하는 것
잘 만들어진 것을 가져와서 사용
(상속에서 부모 클래스 상태 가져오는 건 부가적인 효과이고 주목적이 아님)
1) 유연한 설계
- 객체에 필요한 기능만 넣어 조립하듯 구성할 수 있음
- 나중에 기능을 바꾸고 싶으면 구성 객체만 교체하면 됨
➡️ 바꿔 끼우기 쉬운 시스템 만들기
2) 낮은 결합도(Loose Coupling)
- 상속은 부모-자식 관계가 강하게 묶여 있어서 부모가 바뀌면 자식도 깨짐
- 컴포지션은 내부에 포함된 객체만 바꾸면 되므로 결합도가 낮음
➡️ 변경에 강한 코드 만들기
3) 재사용성 증가
- 특정 기능을 가진 객체를 여러 다른 클래스에서 재사용할 수 있음
- 다중 기능도 상속 없이 조합만으로 해결할 수 있음
➡️ “필요한 기능만 가져다가 붙이는” 방식
4) 테스트 용이성
- 컴포지션은 객체를 갈아끼울 수 있으므로
테스트할 때 Mock 객체나 Fake 객체를 넣기 쉽다.
➡️ 테스트 가능한 코드 만들기
5) 상속의 단점 보완
- 상속은 계층이 깊어질수록 복잡해짐
- 부모 수정 시 자식이 깨질 가능성 높음
- 불필요한 기능도 물려받음
컴포지션은 이런 문제를 피하게 해줌.
➡️ 상속의 위험 없이 객체 기능 확장
6) SOLID 원칙 중 DIP, OCP 실천
- DIP(의존 역전 원칙): 구현보다 인터페이스에 의존하게 만들기 쉽다
- OCP(개방-폐쇄 원칙): 내부 구성 요소를 바꾸면 행동도 바뀌지만
기존 코드는 수정하지 않아도 됨
➡️ 유지보수가 쉬운 구조
3️⃣ 상속 vs 컴포지션
구분 | 상속 (Inheritance) | 컴포지션 (Composition) |
관계 | IS-A 관계 (A는 B이다) | HAS-A 관계 (A는 B를 가진다) |
구조 | 부모 클래스를 확장하여 기능을 물려받음 | 필요한 객체를 포함해서 기능을 조합 |
결합도 | 결합도가 높음 (부모 변화가 자식에 큰 영향) | 결합도가 낮음 (구성 요소만 교체하면 끝) |
유연성 | 낮음 (상속 계층이 고정됨) | 높음 (구성 요소 바꾸면 행동 변경 가능) |
재사용성 | 코드 재사용이 편리함 | 구성 객체 재사용 및 조립으로 유연한 재사용 |
변경 영향 | 부모 클래스 변경 → 자식 클래스에 영향 큼 | 내부 구성만 교체하면 영향 최소화 |
테스트 용이성 | 어려움 (부모 종속성이 존재) | 쉬움 (구성 요소를 Mock으로 대체 가능) |
사용 목적 | 부모의 기능을 그대로 쓰거나 조금 확장할 때 | 기능을 조합하거나 다양한 형태를 만들 때 |
사용 예 | Bird extends Animal | Car has Engine |
권장 여부 | 남용하면 문제 많음, 신중하게 사용 | 현대 OOP 설계에서 더 권장됨 |
4️⃣ 예시
1) ❌ 상속 사용 X
class Engine {
void start() { System.out.println("Engine start"); }
}
class Car extends Engine { } // ❌ Car가 Engine “이다(IS-A)”? 이상함→ Car는 Engine이 아니라 Engine을 “가지고 있는(HAS-A)” 관계
2) ⭕ 컴포지션 사용 (정답)
class Engine {
void start() { System.out.println("Engine start"); }
}
class Car {
private Engine engine = new Engine(); // Car가 Engine을 포함한다(HAS-A)
void drive() {
engine.start();
System.out.println("Car is driving");
}
}
→ 여기서 Car는 Engine 객체를 **포함(composition)**해서 기능을 사용
5️⃣ 실습_버거세트 만들기
1) 버거 클래스
package comp;
public class Burger {
private String name;
private int price;
public Burger(String name, int price) { // full 생성자(모든 상태를 다 받음)
this.name = name;
this.price = price;
System.out.println(name + "가 만들어졌어요");
}
public String getName() {
return name;
}
public int getPrice() {
return price;
}
@Override
public String toString() {
return "Burger{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
}2) 콜라 클래스
package comp;
public class Coke {
private String name;
private int price;
public Coke(String name, int price) { // full 생성자(모든 상태를 다 받음)
this.name = name;
this.price = price;
System.out.println(name + "가 만들어졌어요");
}
public String getName() {
return name;
}
public int getPrice() {
return price;
}
@Override
public String toString() {
return "Coke{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
}3) 감자튀김 클래스
package comp;
public class Potato {
private String name;
private int price;
public Potato(String name, int price) { // full 생성자(모든 상태를 다 받음)
this.name = name;
this.price = price;
System.out.println(name + "이 만들어졌어요");
}
public String getName() {
return name;
}
public int getPrice() {
return price;
}
@Override
public String toString() {
return "Potato{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
}4) 새우버거 클래스(버거 클래스 상속)
package comp;
public class ShrimpBurger extends Burger {
private String material; // 재료 (새우)
public ShrimpBurger(String name, int price, String material) {
super(name, price);
this.material = material;
}
public String getMaterial() {
return material;
}
@Override
public String toString() {
return "ShrimpBurger{" +
"material='" + material + '\'' +
'}';
}
}⚠️ 상속에서 자식 클래스에 생성자가 없으면 디폴트 생성자 안에 super로 부모의 디폴트 생성자를 호출하게 되어 있음 → 하지만 부모클래스에 생성자를 만들어서 디폴트 생성자가 자동으로 세팅되지 않을 경우, 자식 클래스에서 디폴트 생성자를 불러올 수 없기 때문에 자식 클래스에서도 생성자 만들어줘야 함
5) 버거세트 클래스
package comp;
/**
* 1. is 상속 (타입 일치)
* 2. can do 인터페이스 (행위 제약. 할 수 있는 것을 정해 놓는 것)
* 3. has 결합(Composite) - 상태로 가지면 됨
*/
public class BurgerSet {
private Burger burger;
private Coke coke;
private Potato potato;
public BurgerSet(Burger burger, Coke coke, Potato potato) {
this.burger = burger;
this.coke = coke;
this.potato = potato;
System.out.println("버거세트가 만들어졌어요");
}
public Burger getBurger() {
return burger;
}
public Coke getCoke() {
return coke;
}
public Potato getPotato() {
return potato;
}
@Override
public String toString() {
return "BurgerSet{" +
"burger=" + burger +
", coke=" + coke +
", potato=" + potato +
'}';
}
public int getTotalPrice(){
int totalPrice = burger.getPrice() + coke.getPrice() + potato.getPrice();
totalPrice = (int)(totalPrice * 0.9);
return totalPrice;
}
}6) LotteApp 클래스 (main)
package comp;
public class LotteApp {
public static void main(String[] args) {
// 1. 버거만 주세요.
Burger b1 = new Burger("기본버거",1000);
System.out.println(b1);
System.out.println();
// 2. 콜라만 주세요.
Coke c1 = new Coke("콜라",500);
System.out.println(c1);
System.out.println();
// 3. 쉬림프 버거 세트 주세요.
ShrimpBurger sb2 = new ShrimpBurger("새우버거", 2000, "새우");
Coke c2 = new Coke("콜라",500);
Potato p2 = new Potato("감자튀김", 1500);
BurgerSet set = new BurgerSet(sb2, c2, p2);
System.out.println(set.getTotalPrice());
}
}Share article