28. 컴포지션(Composition)

박은서's avatar
Jan 04, 2026
28. 컴포지션(Composition)

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