☁ 패턴들의 개념과 각 패턴을 적용해서 간단한 예시를 🕹게임 캐릭터를 만들 듯이 C#으로 구현해 보았다.
참고 | GoF 디자인 패턴 총정리
GoF 디자인 패턴의 생성, 구조, 행위 패턴에 대한 총정리는 아래 포스팅을 참고하자!
[CS/Design Pattern] GoF 디자인 패턴 정리
구조 패턴 (Structural Pattern)
구조 패턴은 클래스와 객체를 조합하여 더 큰 구조를 만드는 방법에 대한 패턴이다.
구조 패턴에는 7가지 패턴이 있다.
- 어댑터 (Adapter) 패턴
- 브리지 (Bridge) 패턴
- 컴포지트 (Composite) 패턴
- 데코레이터 (Decorator) 패턴
- 퍼사드 (Facade) 패턴
- 플라이웨이트 (Flyweight) 패턴
- 프록시 (Proxy) 패턴
이 패턴들은 코드의 재사용성 및 유지보수성과 시스템의 효율성을 높여준다.
1. 어댑터 (Adapter)
어댑터 패턴은 호환성 없는 클래스들의 인터페이스를 다른 클래스가 이용할 수 있도록 변환하는 패턴이다.
즉, 인터페이스가 호환되지 않는 클래스들을 연결해 주므로, 기존 클래스의 인터페이스를 변경하지 않고 다른 인터페이스를 사용해야 할 때 유용하다.
장점
- 기존 코드나 서드파티 라이브러리와의 호환성을 높인다.
- 코드의 재사용성을 높여 개발 시간을 단축시킬 수 있다.
- 단일 책임 원칙 (SRP)
- 어댑터 클래스는 기존의 클래스와 새로운 인터페이스 간의 변환을 담당하여 단일 책임을 가진다.
- 개방-폐쇄 원칙 (OCP)
- 기존 코드를 수정하지 않고 새로운 인터페이스를 추가할 수 있다.
단점
- 어댑터 클래스가 많아질 경우, 코드 복잡성이 증가한다.
- 어댑터의 성능 오버헤드가 발생할 수 있다.
예시 코드 | 어댑터 (Adapter)
- 게임에서 오래된 전투 시스템을 새로운 전투 시스템과 동시에 사용할 수 있게 코드에 적용해 보았다.
- 어댑터 클래스인 BattleSystemAdapter는 INewBattleSystem을 IOldBattleSystem에 맞게 변환하여 호환성을 제공한다.
- 클라이언트는 BattleSystemAdapter를 통해 새로운 전투 시스템을 사용한다.
//.# 오래된 전투 시스템 인터페이스
public interface IOldBattleSystem {
void Attack();
}
//.# 오래된 전투 시스템 구현
public class OldBattleSystem : IOldBattleSystem {
public void Attack() { Console.WriteLine("🥊오래된 전투 시스템: 공격!"); }
}
//.# 새로운 전투 시스템 인터페이스
public interface INewBattleSystem {
void PerformAttack();
}
//.# 새로운 전투 시스템 구현
public class NewBattleSystem : INewBattleSystem {
public void PerformAttack() { Console.WriteLine("🏹새로운 전투 시스템: 공격!"); }
}
//.# 어댑터 클래스 : 기존 전투시스템을 오래된 전투시스템에 맞게 변환
public class BattleSystemAdapter : IOldBattleSystem {
private readonly INewBattleSystem _newBattleSystem;
public BattleSystemAdapter(INewBattleSystem newBattleSystem) {
_newBattleSystem = newBattleSystem;
}
public void Attack() {
_newBattleSystem.PerformAttack();
}
}
//.# 클라이언트
class Program {
static void Main(string[] args) {
IOldBattleSystem oldBattleSystem = new OldBattleSystem();
oldBattleSystem.Attack(); // 출력: 🥊오래된 전투 시스템: 공격!
INewBattleSystem newBattleSystem = new NewBattleSystem();
IOldBattleSystem adapter = new BattleSystemAdapter(newBattleSystem);
adapter.Attack(); // 출력: 🏹새로운 전투 시스템: 공격!
}
}
2. 브리지 (Bridge)
브리지 패턴은 구현부에서 추상층을 분리하여 서로가 독립적으로 확장할 수 있도록 구현하는 패턴이다.
즉, 추상화와 구현을 각각 별도의 클래스 계층 구조로 분리하여, 두 계층 사이의 결합도를 낮추고 유연성을 확보한다.
장점
- 추상화와 구현의 분리로 인해 각각의 변화에 대응하기 쉬워진다.
- 개방-폐쇄 원칙 (OCP)
- 추상화와 구현의 분리로 인해 확장에는 열려 있고 수정에는 닫혀 있다.
- 확장성이 높아져 새로운 기능을 추가하거나 변경할 때 유연하게 대응할 수 있다.
- 의존성 역전 원칙 (DIP)
- 추상화에 의존하도록 설계되어 구현 세부사항에 대한 직접적인 의존을 줄일 수 있다.
단점
- 설계가 복잡해질 수 있고, 필요 이상으로 추상화할 경우 오버헤드가 발생할 수 있다.
예시 코드 | 브리지 (Bridge)
- 게임에서 다양한 캐릭터와 무기를 독립적으로 구현하는 코드에 적용해 보았다.
- Character 클래스는 무기를 사용하여 전투를 수행하는 추상 클래스로, 구체적인 캐릭터(Warrior, Archer)는 이 클래스를 상속받아서 구현한다.
- 무기와 캐릭터는 독립적으로 변경될 수 있어 유연성이 높다.
//.# 무기 인터페이스
public interface IWeapon {
void Use();
}
//.# 구체적인 무기 구현
public class Sword : IWeapon {
public void Use() { Console.WriteLine("🔸검을 사용했습니다!"); }
}
public class Bow : IWeapon {
public void Use() { Console.WriteLine("🔹활을 사용했습니다!"); }
}
//.# 캐릭터 클래스
public abstract class Character {
protected IWeapon weapon;
public Character(IWeapon weapon) {
this.weapon = weapon;
}
public abstract void Fight();
}
//.# 구체적인 캐릭터 구현 : Character를 상속받아 각각의 전투 방식을 정의
public class Warrior : Character {
public Warrior(IWeapon weapon) : base(weapon) {}
public override void Fight() {
Console.Write("🔸전사: ");
weapon.Use();
}
}
public class Archer : Character {
public Archer(IWeapon weapon) : base(weapon) {}
public override void Fight() {
Console.Write("🔹궁수: ");
weapon.Use();
}
}
//.# 클라이언트
class Program {
static void Main(string[] args) {
IWeapon sword = new Sword();
IWeapon bow = new Bow();
Character warrior = new Warrior(sword);
warrior.Fight(); // 출력: 🔸전사: 🔸검을 사용했습니다!
Character archer = new Archer(bow);
archer.Fight(); // 출력: 🔹궁수: 🔹활을 사용했습니다!
Character archer2 = new Archer(sword);
archer2.Fight(); // 출력: 🔹궁수: 🔸검을 사용했습니다!
}
}
3. 컴포지트 (Composite)
컴포지트 패턴은 복합 객체와 단일 객체를 구분 없이 다룰 때 사용하는 패턴이다.
이 패턴은 객체들을 트리 구조로 구성하므로, 단일 객체와 복합 객체를 동일하게 취급해야 할 때 유용하다.
즉, 클라이언트에서 개별 객체와 복합 객체를 구별하지 않고 사용할 수 있게 된다.
장점
- 단일 객체와 복합 객체를 일관되게 처리할 수 있다.
- 계층 구조를 관리하기 용이하고, 재귀적인 구조를 갖추어 복잡한 구조를 쉽게 구현할 수 있다.
- 개방-폐쇄 원칙 (OCP)
- 구성 요소의 추가나 변경이 쉽다.
- 단일 책임 원칙 (SRP)
- Leaf와 Composite의 역할을 명확히 분리하여 단일 책임을 부여할 수 있다.
단점
- Leaf와 Composite 객체를 구별하기 위한 추가적인 로직이 필요할 수 있다.
- 구성 요소마다 공통 인터페이스를 갖추어야 하므로 설계를 신중하게 해야 한다.
예시 코드 | 컴포지트 (Composite)
- 게임의 퀘스트(복합 퀘스트)와 서브 퀘스트를 구현하는 코드에 적용해 보았다.
- CompositeQuest는 IQuest를 구현하여 서브 퀘스트들을 트리 구조로 구성하고, Display()를 호출하여 출력할 때 모든 서브 퀘스트를 재귀적으로 출력한다.
- 클라이언트에서 메인 퀘스트에 서브 퀘스트들을 포함시킨 다음, 메인 퀘스트의 이름을 출력시키면 포함되어 있는 서브 퀘스트들도 전부 동일하게 출력되는 모습을 볼 수 있다.
//.# 퀘스트 인터페이스 : 퀘스트의 공통 동작 정의
public interface IQuest {
void Display();
}
//.# 기본 퀘스트 클래스 : 단일 퀘스트
public class SimpleQuest : IQuest {
private string name;
public SimpleQuest(string name) {
this.name = name;
}
public void Display() { Console.WriteLine(name); }
}
//.# 복합 퀘스트 클래스 : 여러 서브 퀘스트를 포함할 수 있음
public class CompositeQuest : IQuest {
private string name;
private List<IQuest> subQuests = new List<IQuest>();
public CompositeQuest(string name) {
this.name = name;
}
public void AddSubQuest(IQuest quest) {
subQuests.Add(quest);
}
public void Display() {
Console.WriteLine(name);
foreach (var quest in subQuests) {
quest.Display();
}
}
}
//.# 클라이언트
class Program {
static void Main(string[] args) {
IQuest mainQuest = new CompositeQuest("💾메인 퀘스트");
IQuest subQuest1 = new SimpleQuest("👀서브 퀘스트 1");
IQuest subQuest2 = new SimpleQuest("👀서브 퀘스트 2");
((CompositeQuest)mainQuest).AddSubQuest(subQuest1);
((CompositeQuest)mainQuest).AddSubQuest(subQuest2);
mainQuest.Display();
// 출력:
// 💾메인 퀘스트
// 👀서브 퀘스트 1
// 👀서브 퀘스트 2
}
}
4. 데코레이터 (Decorator)
데코레이터 패턴은 객체에 객체를 결합시켜 기능을 확장하는 패턴이다.
즉, 임의의 객체에 다른 객체들을 덧붙여서 부가적인 기능을 추가하는 방식으로, 기본 객체를 확장할 때 유용하다.
장점
- 상속을 통해 제한되는 클래스의 수를 줄이고, 객체를 구성함으로써 기능을 추가할 수 있다.
- 개방-폐쇄 원칙 (OCP)
- 기본 객체의 수정 없이 새로운 기능을 추가할 수 있다.
- 즉, 기본 객체에 동적으로 기능을 추가할 수 있어 유연성이 높다.
- 단일 책임 원칙 (SRP)
- 데코레이터는 추가적인 기능만을 담당하므로 단일 책임 원칙(SRP)을 지킨다.
단점
- 많은 수의 데코레이터가 적용될 경우 복잡해질 수 있다.
- 객체가 깊게 중첩될 경우 코드 가독성이 떨어진다.
예시 코드 | 데코레이터 (Decorator)
- 게임에서 힘 버프와 민첩 버프를 중첩시키는 코드에 적용해 보았다.
- StrengthBuff와 AgilityBuff는 CharacterDecorator를 상속받아 캐릭터에 힘과 민첩 버프를 추가한다.
- 클라이언트는 기본 캐릭터에 다양한 버프를 동적으로 추가할 수 있다.
//.# 캐릭터 인터페이스
public interface ICharacter {
void ShowStats();
}
//.# 기본 캐릭터 클래스
public class BasicCharacter : ICharacter {
public void ShowStats() { Console.WriteLine("🔹기본 캐릭터"); }
}
//.# 데코레이터 추상 클래스 : 캐릭터에 새로운 기능(버프)을 추가하는 기능
public abstract class CharacterDecorator : ICharacter {
protected ICharacter character;
public CharacterDecorator(ICharacter character) {
this.character = character;
}
public virtual void ShowStats() {
character.ShowStats();
}
}
//.# 구체적인 데코레이터 : 힘, 민첩
public class StrengthBuff : CharacterDecorator {
public StrengthBuff(ICharacter character) : base(character) {}
public override void ShowStats() {
base.ShowStats();
Console.WriteLine("🥊힘 증가");
}
}
public class AgilityBuff : CharacterDecorator {
public AgilityBuff(ICharacter character) : base(character) {}
public override void ShowStats() {
base.ShowStats();
Console.WriteLine("⚡민첩 증가");
}
}
//.# 클라이언트
class Program {
static void Main(string[] args) {
ICharacter basicCharacter = new BasicCharacter();
ICharacter strongCharacter = new StrengthBuff(basicCharacter);
ICharacter agileStrongCharacter = new AgilityBuff(strongCharacter);
basicCharacter.ShowStats();
// 출력: 🔹기본 캐릭터
strongCharacter.ShowStats();
// 출력: 🔹기본 캐릭터
// 🥊힘 증가
agileStrongCharacter.ShowStats();
// 출력: 🔹기본 캐릭터
// 🥊힘 증가
// ⚡민첩 증가
}
}
5. 퍼사드(Facade)
퍼사드 패턴은 복잡한 서브 클래스들을 피해 더 상위에 인터페이스를 구성하는 패턴이다.
즉, 클라이언트가 복잡한 시스템의 일부분에 직접 접근하는 대신, 간단한 인터페이스를 통해 접근하도록 한다.
이를 통해 시스템 간의 의존성을 줄이고, 클라이언트 코드를 단순화한다.
장점
- 복잡한 시스템을 단순화하여 클라이언트가 쉽게 사용할 수 있다.
- 서브시스템 변경에 대한 영향을 최소화하고, 결합도를 낮춘다.
- 인터페이스 분리 원칙 (ISP)
- 클라이언트에 필요한 최소한의 인터페이스를 제공함으로써 인터페이스 분리 원칙(ISP)을 준수할 수 있다.
- 의존성 역전 원칙 (DIP)
- 클라이언트는 퍼사드 인터페이스에만 의존한다.
단점
- 퍼사드 클래스가 너무 많은 기능을 갖게 되면 단일 책임 원칙을 위반할 수 있다.
- 서브시스템의 세부 사항을 완전히 숨기기 어려운 경우가 생길 수 있다
예시 코드 | 퍼사드 (Facade)
- 게임에서 복잡한 게임 시작 과정(에셋 로드, 설정 초기화, 게임 시작 등등)에 적용해 보았다.
- 클라이언트는 GameFacade를 사용하여 복잡한 게임 시작 과정을 단순화한다.
//.# 하위 시스템 클래스 : "에셋 로드", "설정 초기화", "게임 시작"
public class GameLoader {
public void LoadAssets() { Console.WriteLine("💾.1 게임 에셋 로드"); }
}
public class GameInitializer {
public void InitializeSettings() { Console.WriteLine("💾.2 게임 설정 초기화"); }
}
public class GameStarter {
public void StartGame() { Console.WriteLine("💾.3 게임 시작❕"); }
}
//.# 퍼사드 클래스 : 하위 시스템을 통합하여 게임 시작을 간소화
public class GameFacade {
private GameLoader loader;
private GameInitializer initializer;
private GameStarter starter;
public GameFacade() {
loader = new GameLoader();
initializer = new GameInitializer();
starter = new GameStarter();
}
public void StartGame() {
loader.LoadAssets();
initializer.InitializeSettings();
starter.StartGame();
}
}
//.# 클라이언트
class Program {
static void Main(string[] args) {
GameFacade game = new GameFacade();
game.StartGame();
// 출력:
// 💾.1 게임 에셋 로드
// 💾.2 게임 설정 초기화
// 💾.3 게임 시작❕
}
}
6. 플라이웨이트 (Flyweight)
플라이웨이트 패턴은 유사 객체들 사이에 가능한 많은 데이터를 공유해 사용하는 패턴이다.
즉, 객체를 공유하여 메모리 사용량을 줄이므로, 많은 수의 유사한 객체들을 효율적으로 관리해야 할 때 유용하다.
장점
- 메모리 사용량을 줄이고 성능 개선에 도움이 된다.
- 객체의 공유로 인해 객체 생성 비용을 절감할 수 있다.
- 개방-폐쇄 원칙 (OCP)
- 객체의 공유로 인해 확장성이 좋다.
- 단일 책임 원칙 (SRP)
- 객체의 상태와 동작을 명확히 분리하기 때문에 단일 책임 원칙을 준수한다.
단점
- 객체의 상태가 공유되기 때문에, 변경 시 모든 객체에 영향을 줄 수 있다.
- 객체의 공유로 인해 다중 스레드 환경에서 동기화 문제가 발생할 수 있다.
예시 코드 | 플라이웨이트 (Flyweight)
- 게임에서 동일한 종류의 몬스터를 공유하도록 코드에 적용해 보았다.
- MonsterFactory 클래스는 몬스터를 생성하고, 동일한 타입의 몬스터를 공유하므로 메모리 사용을 줄인다.
- 클라이언트는 MonsterFactory를 사용하여 동일한 몬스터 객체를 재사용할 수 있다.
//.# 몬스터 클래스 : 몬스터의 타입을 저장하고 위치를 출력함
public class Monster {
public string Type { get; private set; }
public Monster(string type) {
Type = type;
}
public void Display(int x, int y) { Console.WriteLine($"👀몬스터 {Type} 위치: ({x}, {y})"); }
}
//.# 몬스터 팩토리 클래스 : 몬스터를 생성하고, 동일한 타입의 몬스터를 공유함
public class MonsterFactory {
private Dictionary<string, Monster> monsters = new Dictionary<string, Monster>();
public Monster GetMonster(string type) {
if (!monsters.ContainsKey(type)) {
monsters[type] = new Monster(type);
}
return monsters[type];
}
}
//.# 클라이언트
class Program {
static void Main(string[] args) {
MonsterFactory factory = new MonsterFactory();
Monster goblin1 = factory.GetMonster("💀고블린");
Monster goblin2 = factory.GetMonster("💀고블린");
Monster dragon = factory.GetMonster("🐉드래곤");
goblin1.Display(1, 2);
goblin2.Display(3, 4);
dragon.Display(5, 6);
// 출력:
// 👀몬스터 💀고블린 위치: (1, 2)
// 👀몬스터 💀고블린 위치: (3, 4)
// 👀몬스터 🐉드래곤 위치: (5, 6)
}
}
7. 프록시 (Proxy)
프록시 패턴은 접근이 어려운 객체에 대한 인터페이스 역할을 하는 패턴이다.
로딩을 지연시키거나 접근 제어를 추가해야 할 때 유용하다.
장점
- 객체에 대한 접근을 제어하고, 필요할 때만 실제 객체를 생성한다.
- 클라이언트에 대한 추가적인 기능을 제공할 수 있다! (예: 보안 검사, 캐싱 등).
- 개방-폐쇄 원칙 (OCP)
- 클라이언트와 실제 서비스 객체 사이의 의존성을 낮춘다.
단점
- 프록시와 실제 객체의 인터페이스가 동일해야 하므로 설계가 복잡해질 수 있다.
- 프록시의 사용이 과도할 경우 성능 저하가 발생할 수 있다.
예시 코드 | 프록시 (Proxy)
- 게임에서 플레이어의 정보에 접근을 제어하는 코드에 적용해 보았다.
- 클라이언트에서 player.DisplayInfo() 로 호출할 때, 객체가 생성이 되면서 RealPlayer 클래스의 생성자가 호출되는 모습을 볼 수 있다.
- 프록시는 실제 객체를 필요할 때까지 생성하지 않으며, 클라이언트가 PlayerProxy를 통해 실제 플레이어 정보에 접근할 수 있도록 한다.
//.# 플레이어 인터페이스
public interface IPlayer {
void DisplayInfo();
}
//.# 실제 플레이어 클래스 : 플레이어 정보를 관리하고 출력
public class RealPlayer : IPlayer {
private string name;
public RealPlayer(string name) {
this.name = name;
LoadFromDatabase(name);
}
private void LoadFromDatabase(string name) {
Console.WriteLine($"💾플레이어 {name}의 정보를 데이터베이스에서 로드");
}
public void DisplayInfo() { Console.WriteLine($"🏹플레이어: {name}"); }
}
//.# 프록시 클래스 : RealPlayer에 대한 접근을 제어
public class PlayerProxy : IPlayer {
private RealPlayer realPlayer;
private string name;
public PlayerProxy(string name) {
this.name = name;
}
public void DisplayInfo() {
if (realPlayer == null) {
realPlayer = new RealPlayer(name);
}
realPlayer.DisplayInfo();
}
}
//.# 클라이언트
class Program {
static void Main(string[] args) {
IPlayer player = new PlayerProxy("김프록시");
player.DisplayInfo();
// 출력:
// 💾플레이어 김프록시의 정보를 데이터베이스에서 로드
// 🏹플레이어: 김프록시
player.DisplayInfo();
// 출력:
// 🏹플레이어: 김프록시
}
}
❓ 뭔가 비슷한 패턴들이 많은데 차이점이 정확히 뭘까?
예를 들어, 데코레이터와 프록시 패턴은 구조가 매우 비슷하다. 두 패턴 모두 한 객체가 일부 작업을 다른 객체에 위임한다는 점에서 거의 일치한다고 생각할 수 있다.
하지만, 의도 관점에서 차이점이 있다.
데코레이터의 객체 간의 합성은 항상 클라이언트에서 제어가 되지만, 프록시는 자체적으로 자신의 서비스 객체의 수명 주기를 관리하는 게 일반적이다.
💣이런 식으로 구조가 매우 비슷해 보이는 패턴이 여러 가지일 수 있다.
그때는 🔽아래의 링크("리팩토링 구루") 🔽 를 참고하면 더 자세히 알 수 있다!!
참고
https://refactoring.guru/ko/design-patterns/structural-patterns
'💾 Computer Science > Software Engineering' 카테고리의 다른 글
[CS/Design Pattern] GoF 디자인 패턴 | 행위 패턴(Behavioral Pattern) 11가지 정리 (0) | 2024.07.18 |
---|---|
[CS/Design Pattern] GoF 디자인 패턴 | 생성 패턴(Creational Pattern) 5가지 정리 (0) | 2024.07.10 |
[CS/Design Pattern] GoF 디자인 패턴 정리 (0) | 2024.07.09 |