컴포지트 패턴
어떤 객체들의 구성을 표현하기 위해 사용되며, 개별 객체와 복합 객체를 동일한 인터페이스를 통해 다룰 수 있도록 해주는 디자인 패턴이다. 이 패턴은 개별 객체와 복합 객체를 구분하지 않고 동일한 방식으로 취급함으로써 클라이언트 코드가 단일 객체나 객체 구성을 일관되게 처리할 수 있게 해준다.
구조
- Component - 개별 객체와 복합 객체가 구현해야 하는 공통 인터페이스를 정의한다. 순환 구조에서 요소들을 포함하는 전체 클래스에 접근하는데 필요한 인터페이스를 정의하며, 적절하다면 그 인터페이스를 구현한다.
- Leaf - 개별 객체를 나타내며 Component 인터페이스를 구현한다. Component에
- Composite - 복합 객체를 나타내며 자식 컴포넌트를 관리하고 Component 인터페이스를 구현한다.
기본 인터페이스
// Component 인터페이스
public interface IComponent
{
void Operation();
}
// Leaf 클래스 구현
public class Leaf : IComponent
{
public void Operation()
{
Console.WriteLine("Leaf의 동작");
}
}
// Composite 클래스 구현
public class Composite : IComponent
{
private List<IComponent> children = new List<IComponent>();
public void AddComponent(IComponent component)
{
children.Add(component);
}
public void RemoveComponent(IComponent component)
{
children.Remove(component);
}
public void Operation()
{
Console.WriteLine("Composite의 동작");
foreach (var child in children)
{
child.Operation();
}
}
}
public class Client
{
public static void Main(string[] args)
{
// Leaf 객체 생성
IComponent leaf = new Leaf();
// Composite 객체 생성
Composite composite = new Composite();
// Composite에 Leaf 추가
composite.AddComponent(leaf);
// Composite가 Leaf의 동작을 수행
composite.Operation();
}
}
Composite의 동작
leaf의 동작
왜 사용해야 할까?
컴포지트 패턴은 객체들 간의 계층 구조를 통해 복합 객체와 개별 객체를 동일하게 취급함으로써 유연하고 일관된 방식으로 객체를 다룰 수 있게 해주는 유용한 디자인 패턴이다. 컴포지트 패턴은 트리 구조를 구성하고 있기 때문에 부모 컴포지트 클래스를 통해서 자식 컴포지트에 연쇄적으로 접근할 수 있다.
컴포지트 패턴을 사용하면 클라이언트가 루트 컴포지트 클래스를 통해서만 접근하기 때문에 리프 클래스와의 결합도를 낮출 수 있다. 클라이언트에게 아이템창에 해당하는 컴포지트 클래스를 제공하면 클라이언트는 해당 컴포지트를 통해서 아이템창 안에 있는 다른 아이템 컴포지트에 간접 접근하는 방식으로 설계되기 때문에 클라이언트 코드를 간결하게 작성할 수 있다.
컴포지트 패턴은 트리 구조를 사용하기 때문에 해당 컴포지트 클래스의 모든 자식 컴포지트 클래스를 한번에 관리할 수 있다. 아이템창의 이름을 통한 정렬, 스킬 트리 구현, 장비와 스킬 정보를 통한 RPG 게임의 전투력 시스템 등은 컴포지트 클래스를 통해 구현하기 아주 적합한 기능들이다.
전투력 시스템을 통해 예시를 한번 확인해보자. 전투력 시스템은 현재 캐릭터가 착용하고 있는 장비, 스킬들의 전투력들을 총합하여 클라이언트에게 보여주는 시스템이다. 클라이언트에게 한번에 총 전투력을 보여주기 위해 다음과 같은 설계를 해야한다.
- 장비와 스킬은 전투력을 가지고 있고, 전투력을 출력할 수 있어야 한다.
- 총 전투력을 출력하는 관리자 클래스를 구현해야 한다.
- 관리자 클래스는 장비와 스킬들의 정보를 갖고 있어야 한다.
관리자 클래스를 루트 컴포지트로 설정하고, 장비와 스킬들을 Leaf로 설정하여 관리자 클래스에 저장해놓는다면 클라이언트는 관리자 클래스를 통해 총 전투력을 확인할 수 있을 것이다.
Component 인터페이스
public interface ICharacterComponent
{
int GetCombatPower();
}
Composite 클래스
public class CharacterComposite : ICharacterComponent
{
private List<ICharacterComponent> components = new List<ICharacterComponent>();
public void AddComponent(ICharacterComponent component)
{
components.Add(component);
}
public int GetCombatPower()
{
int totalCombatPower = 0;
foreach (var component in components)
{
totalCombatPower += component.GetCombatPower();
}
return totalCombatPower;
}
}
Leaf 클래스 -( 장비, 스킬 )
public class Equipment : ICharacterComponent
{
private int combatPower;
public Equipment(int power)
{
combatPower = power;
}
public int GetCombatPower()
{
return combatPower;
}
}
public class Skill : ICharacterComponent
{
private string name;
private int combatPower;
public Skill(string name, int combatPower)
{
this.name = name;
this.combatPower = combatPower;
}
public int GetCombatPower()
{
return combatPower;
}
// 추가로 Skill에 특화된 메서드들을 구현할 수 있다
public void UseSkill()
{
Console.WriteLine($"{name} 스킬을 사용!");
}
}
Main(or Client)
class Program
{
static void Main()
{
Equipment sword = new Equipment(50);
Equipment shield = new Equipment(30);
Skill fireball = new Skill("파이어볼", 40);
Skill lightning = new Skill("라이트닝", 35);
CharacterComposite character = new CharacterComposite();
character.AddComponent(sword);
character.AddComponent(shield);
character.AddComponent(fireball);
character.AddComponent(lightning);
int totalCombatPower = character.GetCombatPower();
Console.WriteLine("캐릭터의 전투력: " + totalCombatPower);
// Skill 클래스의 특화된 메서드 사용
if (fireball is Skill)
{
(fireball as Skill).UseSkill();
}
}
}
캐릭터의 전투력: 155 // 50 + 30 + 40 + 35 = 155
파이어볼 스킬을 사용!
만약 추가적으로 전투력을 획득할 수 있는 방식이 생기면 어떨까? 이후 시스템에 보석 시스템이 추가된다고 해보자. 보석 시스템은 특정 장비에 장착함으로써 해당 장비의 전투력을 올리는 시스템이다. 이를 구현하기 위해선 장비를 Leaf가 아닌 추가 Composite 클래스로 사용하고, Jewel을 Leaf 클래스로 사용하면 된다. 혹은 이후 전투력 관련 기능이 나올 가능성에 대비해 모두 Composite 클래스로 사용하는 것도 하나의 방법이다.
//Composite 클래스
public class Equipment : ICharacterComponent
{
private List<ICharacterComponent> components = new List<ICharacterComponent>();
private int combatPower;
public Equipment(int power)
{
combatPower = power;
}
public void AddComponent(ICharacterComponent component)
{
components.Add(component);
}
public int GetCombatPower()
{
int totalCombatPower = combatPower;
foreach (var component in components)
{
totalCombatPower += component.GetCombatPower();
}
return totalCombatPower;
}
}
// 추가 Leaf 클래스: Jewel
public class Jewel : ICharacterComponent
{
private int combatPower;
public Jewel(int power)
{
combatPower = power;
}
public int GetCombatPower()
{
return combatPower;
}
}
Main(Client)
class Program
{
static void Main()
{
Equipment sword = new Equipment(50);
Equipment shield = new Equipment(30);
Jewel ruby = new Jewel(20);
Skill fireball = new Skill("파이어볼", 40);
Skill lightning = new Skill("라이트닝", 35);
CharacterComposite character = new CharacterComposite();
character.AddComponent(sword);
character.AddComponent(shield);
shield.AddComponent(ruby); // Jewel 추가
character.AddComponent(fireball);
character.AddComponent(lightning);
int totalCombatPower = character.GetCombatPower();
Console.WriteLine("캐릭터의 전투력: " + totalCombatPower);
}
}
캐릭터의 전투력: 175
장점
- 일관성 있는 인터페이스를 통해 클라이언트 코드를 단순화시킨다.
- 새로운 Leaf나 Composite 클래스를 추가하더라도 클라이언트 코드 수정이 필요 없다.
- 구조화된 트리 구조를 통해 객체들 간의 계층 구조를 쉽게 표현할 수 있다.
단점
- 복합 객체에 새로운 행위를 추가하려면 Leaf와 Composite 모두 수정이 필요하다.(인터페이스의 변경)
참고자료
GoF의 디자인 패턴 - 예스24
이 책은 디자인 패턴을 다룬 이론서로 디자인 패턴의 기초적이고 전반적인 내용을 학습할 수 있다.
www.yes24.com
'프로그래밍 이론 > 디자인 패턴' 카테고리의 다른 글
10. [Design Pattern] 퍼사드(Facade) 패턴 (0) | 2023.12.25 |
---|---|
09. [Design Pattern] 데코레이터(Decorator) 패턴 (1) | 2023.12.10 |
07. [Design Pattern] 브릿지(Bridge) 패턴 (0) | 2023.11.23 |
06. [Design Pattern] 어댑터(Adapter) 패턴 (0) | 2023.11.21 |
05. [Design Pattern] 싱글톤(Singleton) 패턴 (0) | 2023.11.16 |