☁ 패턴들의 개념과 각 패턴을 적용해서 간단한 예시를 🕹게임 캐릭터를 만들 듯이 C#으로 구현해 보았다.
참고 | GoF 디자인 패턴 총정리
GoF 디자인 패턴의 생성, 구조, 행위 패턴에 대한 총정리는 아래 포스팅을 참고하자!
[CS/Design Pattern] GoF 디자인 패턴 정리
생성 패턴 (Creational Pattern)
생성 패턴은 객체의 생성 메커니즘을 다루며, 객체들을 생성하는 방식이나 객체 생성 시점을 결정하는 패턴이다.
생성 패턴에는 5가지 패턴이 있다.
- 추상 팩토리 (Abstract Factory) 패턴
- 빌더 (Builder) 패턴
- 팩토리 메서드 (Factory Method) 패턴
- 프로토타입 (Prototype) 패턴
- 싱글톤 (Singleton) 패턴
이 패턴들은 객체 생성 방식을 추상화하여 객체 생성 과정의 유연성과 재사용성을 높여준다.
1. 추상 팩토리 (Abstract Factory)
추상 팩토리 패턴은 구체적인 클래스에 의존하지 않고, 인터페이스를 통해 서로 연관 객체들의 그룹으로 생성하여 추상적으로 표현하는 패턴이다.
이를 위해 관련성이 있거나 독립적인 여러 객체들의 그룹을 생성하기 위한 인터페이스를 제공한다.
장점
- 관련 있는 객체들 간의 일관성을 유지하며 생성할 수 있고, 서로 다른 구현체들을 쉽게 교체할 수 있다.
- 클라이언트는 구체적인 클래스를 알 필요 없이 인터페이스를 통해 객체를 생성할 수 있다.
- 개방-폐쇄 원칙 (OCP)
- 새로운 종류의 객체를 추가하기 위해 수정 없이 확장할 수 있다.
- 예를 들어, 새로운 직업 '마법사'가 추가되어도 클라이언트 코드는 변경되지 않는다.
- 단일 책임 원칙 (SRP)
- 추상 팩토리는 한 가지 종류의 객체를 생성하는 책임을 가진다.
- 예를 들어, 궁수 팩토리는 궁수 무기와 궁수 갑옷을 생성하는 한 가지 책임을 갖는다.
단점
- 새로운 종류의 객체를 추가하려면 추상 팩토리와 모든 구체적인 팩토리를 수정해야 할 수 있다.
- 객체의 계층 구조가 복잡할 경우 유지 관리가 어려워진다.
예시 코드 | 추상 팩토리(Abstract Factory)
- 게임에서 다양한 직업의 아이템들을 생성하는 코드에 적용해 보았다.
- '전사'와 '궁수'라는 직업 팩토리를 이용해서 아이템(무기, 갑옷)을 만든다.
//.# 아이템 인터페이스
public interface IWeapon {
void Display();
}
public interface IArmor {
void Display();
}
//.# 궁수 아이템 구현
public class ArcherWeapon : IWeapon {
public void Display() { Console.WriteLine("🥊궁수의 활"); }
}
public class ArcherArmor : IArmor {
public void Display() { Console.WriteLine("🧥궁수의 갑옷"); }
}
//.# 전사 아이템 구현
public class WarriorWeapon : IWeapon {
public void Display() { Console.WriteLine("🥊전사의 검"); }
}
public class WarriorArmor : IArmor {
public void Display() { Console.WriteLine("🧥전사의 갑옷"); }
}
//.# 추상 팩토리 인터페이스 : 무기(IWeapon)와 갑옷(IArmor)을 생성하는 메서드를 정의
public interface IItemFactory {
IWeapon CreateWeapon();
IArmor CreateArmor();
}
//.# 궁수 팩토리
public class ArcherFactory : IItemFactory {
public IWeapon CreateWeapon() {
return new ArcherWeapon();
}
public IArmor CreateArmor() {
return new ArcherArmor();
}
}
//.# 전사 팩토리
public class WarriorFactory : IItemFactory {
public IWeapon CreateWeapon() {
return new WarriorWeapon();
}
public IArmor CreateArmor() {
return new WarriorArmor();
}
}
//.# 클라이언트 코드 : IItemFactory를 사용하여 무기와 갑옷을 생성하고, 출력
public class Game {
private IWeapon weapon;
private IArmor armor;
public Game(IItemFactory factory) {
weapon = factory.CreateWeapon();
armor = factory.CreateArmor();
}
public void DisplayItems() {
weapon.Display();
armor.Display();
}
}
class Program {
static void Main(string[] args) {
IItemFactory ArcherFactory = new ArcherFactory();
Game ArcherGame = new Game(ArcherFactory);
ArcherGame.DisplayItems(); // 출력: 🥊궁수의 활, 🧥궁수의 갑옷
IItemFactory WarriorFactory = new WarriorFactory();
Game WarriorGame = new Game(WarriorFactory);
WarriorGame.DisplayItems(); // 출력: 🥊전사의 검, 🧥전사의 갑옷
}
}
2. 빌더 (Builder)
빌더 패턴은 작게 분리된 인스턴스를 건축하듯 조합하여 객체를 생성하는 패턴이다.
복잡한 객체의 생성 과정을 캡슐화하여 객체의 표현 방식을 분리한다.
장점
- 복잡한 객체의 생성 과정을 캡슐화하여 코드의 가독성을 높인다.
- 동일한 생성 과정에서 다양한 표현을 만들 수 있다.
- 생성된 객체의 내부 표현에 대한 자유로운 접근이 가능하다.
- 단일 책임 원칙 (SRP)
- 복잡한 객체의 생성 과정을 캡슐화하여 각 빌더 클래스가 한 가지 객체를 만드는 책임을 가진다.
- 객체의 생성 과정은 빌더에 의해 담당되며, 클라이언트는 구체적인 객체 생성 과정을 몰라도 된다.
- 리스코프 치환 원칙 (LSP)
- 빌더 패턴에서는 서로 다른 빌더 클래스를 사용하여 여러 종류의 객체를 생성할 수 있다.
- 클라이언트는 각 빌더를 교체하여 다양한 객체를 생성할 수 있다.
단점
- 객체의 속성이 많거나 구조가 복잡할 경우, 빌더(Builder) 클래스의 작성이 번거로워진다.
- 생성자 인자가 많아질 경우 설정하기도 어려워진다.
예시 코드 | 빌더 (Builder) 패턴
- 게임에서 복잡한 캐릭터를 생성하는 코드에 적용해 보았다.
- 캐릭터의 이름, 클래스, 무기를 각각 설정하여 생성한다.
//.# 캐릭터 클래스
public class Character {
public string Name { get; set; }
public string Class { get; set; }
public string Weapon { get; set; }
public void Display() { Console.WriteLine($"🔹이름: {Name}, 클래스: {Class}, 무기: {Weapon}"); }
}
//.# 빌더(Builder) 인터페이스 : 캐릭터의 속성을 설정하는 메서드를 정의
public interface ICharacterBuilder {
void SetName(string name);
void SetClass(string className);
void SetWeapon(string weapon);
Character Build();
}
//.# 구체적인 빌더(Builder) 구현 : 캐릭터의 속성을 설정하고, 캐릭터 객체를 생성
public class CharacterBuilder : ICharacterBuilder {
private Character character = new Character();
public void SetName(string name) { character.Name = name; }
public void SetClass(string className) { character.Class = className; }
public void SetWeapon(string weapon) { character.Weapon = weapon; }
public Character Build() {
return character;
}
}
//.# 디렉터 클래스 : 빌더를 사용하여 특정 유형의 캐릭터(전사, 궁수 등)를 생성하는 메서드를 제공
public class CharacterDirector {
public Character ConstructWarrior(ICharacterBuilder builder) {
builder.SetName("전사");
builder.SetClass("근접");
builder.SetWeapon("검");
return builder.Build();
}
public Character ConstructArcher(ICharacterBuilder builder) {
builder.SetName("궁수");
builder.SetClass("원거리");
builder.SetWeapon("활");
return builder.Build();
}
}
class Program {
static void Main(string[] args) {
CharacterDirector director = new CharacterDirector();
ICharacterBuilder builder = new CharacterBuilder();
Character warrior = director.ConstructWarrior(builder);
warrior.Display(); // 출력: 🔹이름: 전사, 클래스: 근접, 무기: 검
Character archer = director.ConstructArcher(builder);
archer.Display(); // 출력: 🔹이름: 궁수, 클래스: 원거리, 무기: 활
}
}
3. 팩토리 메서드 (Factory Method)
팩토리 메서드 패턴은 객체 생성을 서브 클래스로 분리해 처리하도록 캡슐화하는 패턴이다. 즉, 상위 클래스에서 인터페이스만 정의하고, 실제 생성은 서브 클래스에서 구현한다.
이는 클라이언트가 인스턴스화할 클래스를 지정하지 않고, 팩토리 메서드를 통해 객체를 생성할 수 있도록 한다.
장점
- 클라이언트는 구체적인 클래스에 대해 알 필요 없이 팩토리 메서드를 호출하여 객체를 생성할 수 있다.
- 개방-폐쇄 원칙 (OCP)
- 새로운 종류의 객체를 추가하기 위해 수정 없이 확장할 수 있다.
- 예를 들어, 새로운 종류의 몬스터를 추가하려면 새로운 팩토리 메서드를 구현하는 서브 클래스만 필요합니다.
- 의존 역전 원칙 (DIP)
- 클라이언트는 구체적인 클래스 대신, 추상 팩토리나 팩토리 메서드를 통해 객체를 생성하므로, 고수준 모듈이 저수준 모듈에 의존하지 않는다.
단점
- 팩토리 메서드가 많아질 경우 클래스 계층 구조가 복잡해질 수 있다.
- 서브 클래스가 추가될 때마다 새로운 클래스를 정의해야 하므로 파일 개수가 많아질 수 있다.
예시 코드 | 팩토리 메서드 (Factory Method)
- 게임에서 다양한 종류의 몬스터(드래곤, 슬라임)를 생성하는 코드에 적용해 보았다.
- 클라이언트는 특정 몬스터 팩토리를 사용하여 몬스터를 생성하고, 해당 몬스터를 출력한다.
//.# 몬스터 인터페이스 : 몬스터의 행동을 정의
public interface IMonster {
void Display();
}
//.# 몬스터 구현 클래스 : 각 몬스터의 동작을 정의
public class Dragon : IMonster {
public void Display() { Console.WriteLine("🐉드래곤 등장!"); }
}
public class Slime : IMonster {
public void Display() { Console.WriteLine("💀슬라임 등장!"); }
}
//.# 팩토리 메서드(Factory Method) 클래스 : 몬스터를 생성하는 팩토리 메서드(CreateMonster)를 정의
public abstract class MonsterFactory {
public abstract IMonster CreateMonster();
public void ShowMonster() {
IMonster monster = CreateMonster();
monster.Display();
}
}
//.# 구체적인 팩토리 메서드(Factory Method) 클래스 : MonsterFactory를 상속받아 각각 드래곤과 슬라임을 생성
public class DragonFactory : MonsterFactory {
public override IMonster CreateMonster() {
return new Dragon();
}
}
public class SlimeFactory : MonsterFactory {
public override IMonster CreateMonster() {
return new Slime();
}
}
class Program {
static void Main(string[] args) {
MonsterFactory dragonFactory = new DragonFactory();
dragonFactory.ShowMonster(); // 출력: 🐉드래곤 등장!
MonsterFactory SlimeFactory = new SlimeFactory();
SlimeFactory.ShowMonster(); // 출력: 💀슬라임 등장!
}
}
4. 프로토타입 (Prototype)
프로토타입 패턴은 원본 객체를 복제하여 객체를 생성하는 패턴이다.
이 패턴은 객체 생성 과정의 비용이 높을 때 유용하게 쓰인다.
장점
- 객체를 복제하는 것이 생성하는 것보다 효율적일 수 있다.
- 클라이언트는 원형 객체의 구현에 대해 알 필요 없이 복제 메서드를 호출하여 새로운 객체를 생성할 수 있다.
- 단일 책임 원칙 (SRP)
- 프로토타입 패턴은 객체를 복제하는 책임을 담당하는 클래스가 따로 있기 때문에 객체 생성의 단일 책임을 준수한다.
- 개방-폐쇄 원칙 (OCP)
- 새로운 종류의 객체를 추가할 때 기존의 프로토타입 클래스를 수정하지 않고, 새로운 프로토타입 클래스를 추가함으로써 확장할 수 있다.
단점
- 객체가 복잡한 경우, 복제 과정도 복잡해진다.
- 객체의 내부 상태가 변경될 경우 올바른 복제를 보장하기 어렵다.
예시 코드 | 프로토타입 (Prototype)
- 게임에서 아이템을 복제하는 코드에 적용해 보았다.
- 클라이언트는 원본 아이템을 생성하고, 이를 복제하여 새로운 아이템을 생성한다.
- 복제된 아이템은 원본과 동일한 속성을 가진다.
//.# 아이템 프로토타입(Prototype) 인터페이스 : 아이템을 복제하는 Clone 메서드를 정의
public interface IItem {
IItem Clone();
void Display();
}
//.# 구체적인 아이템 구현 프로토타입(Prototype)클래스
public class Sword : IItem {
public string Name { get; set; }
public Sword(string name) {
Name = name;
}
//. 현재 객체의 복사본을 반환
public IItem Clone() {
return new Sword(this.Name);
}
public void Display() { Console.WriteLine($"🔹검: {Name}"); }
}
public class Shield : IItem {
public string Name { get; set; }
public Shield(string name) {
Name = name;
}
//. 현재 객체의 복사본을 반환
public IItem Clone() {
return new Shield(this.Name);
}
public void Display() { Console.WriteLine($"🔹방패: {Name}"); }
}
class Program {
static void Main(string[] args) {
Sword originalSword = new Sword("전설의 검");
Sword clonedSword = (Sword)originalSword.Clone();
Shield originalShield = new Shield("나무 방패");
Shield clonedShield = (Shield)originalShield.Clone();
originalSword.Display(); // 출력: 🔹검: 전설의 검
clonedSword.Display(); // 출력: 🔹검: 전설의 검
originalShield.Display(); // 출력: 🔹방패: 나무 방패
clonedShield.Display(); // 출력: 🔹방패: 나무 방패
}
}
5. 싱글톤 (Singleton)
싱글톤 패턴은 클래스가 하나의 인스턴스만 생성할 수 있도록 보장하는 패턴이다.
생성된 객체는 어디서든 참조할 수 있지만(전역 접근), 여러 프로세스가 동시에 참조할 수는 없다.
주로 자원 공유나 설정 관리 등에서 사용되며, 전역 변수의 남발을 방지하여 자원의 효율적인 사용을 돕는다.
예를 들어, 게임 매니저나 리소스 관리자 등의 인스턴스가 단 하나만 필요할 경우 싱글톤을 사용한다.
장점
- 전역적으로 접근할 수 있는 단일 객체를 보장하여 데이터 공유와 리소스 접근을 쉽게 한다.
- Lazy initialization을 통해 필요할 때만 객체를 생성하므로 자원을 절약할 수 있다.
- 단일 책임 원칙 (SRP)
- 싱글톤 클래스는 인스턴스를 하나만 생성하고 유지하는 책임을 담당한다.
- 애플리케이션에서 단 하나의 객체만 필요한 경우 이 원칙을 따르며, 그 책임을 수행한다.
- 의존 역전 원칙 (DIP)
- 싱글톤 패턴은 전역적으로 접근 가능한 객체를 제공하므로, 다른 객체가 이를 의존할 수 있다.
- 하지만 이 원칙을 지키기 위해 싱글톤을 사용할 때는 주의가 필요하다!
✒ Lazy initialization(지연 초기화)
객체나 변수를 필요할 때까지 생성하거나 초기화를 미루는 기법이다.
이 기법은 자원을 절약하고 성능을 향상시킨다.
단점
- 멀티스레드 환경에서 동기화 문제가 발생할 수 있으므로 주의가 필요하다.
- 객체의 수명 관리가 어려울 수 있으며, 의존성 주입 등의 패턴과 함께 사용할 경우 구현이 복잡해진다.
예시 코드 | 싱글톤 (Singleton)
- 게임 설정을 관리하는 코드에 적용해 보았다.
- instance 변수는 클래스의 유일한 인스턴스를 저장하며, Instance 속성은 이 인스턴스를 반환한다.
- 클라이언트는 GameSettings.Instance를 통해 설정을 가져오거나 변경할 수 있다.
- 모든 클라이언트는 동일한 GameSettings 인스턴스를 사용하므로, 설정이 일관되게 유지된다.
//.# 싱글톤(Singleton) 클래스 : 싱글톤 패턴을 구현하여 게임 설정을 관리
public class GameSettings {
private static GameSettings instance;
public string Difficulty { get; set; }
public int Volume { get; set; }
private GameSettings() {
Difficulty = "Normal";
Volume = 50;
}
public static GameSettings Instance {
get {
if (instance == null) {
instance = new GameSettings();
}
return instance;
}
}
public void DisplaySettings() { Console.WriteLine($"💾난이도: {Difficulty}, 볼륨: {Volume}"); }
}
class Program {
static void Main(string[] args) {
GameSettings settings1 = GameSettings.Instance;
settings1.DisplaySettings(); // 출력: 💾난이도: Normal, 볼륨: 50
GameSettings settings2 = GameSettings.Instance;
settings2.Difficulty = "Hard";
settings2.Volume = 100;
settings1.DisplaySettings(); // 출력: 💾난이도: Hard, 볼륨: 100
settings2.DisplaySettings(); // 출력: 💾난이도: Hard, 볼륨: 100
}
}
참고
https://refactoring.guru/ko/design-patterns/creational-patterns
https://hanbit.co.kr/channel/category/category_view.html?cms_code=CMS8616098823
'💾 Computer Science > Software Engineering' 카테고리의 다른 글
[CS/Design Pattern] GoF 디자인 패턴 | 구조 패턴(Structural Pattern) 7가지 정리 (0) | 2024.07.15 |
---|---|
[CS/Design Pattern] GoF 디자인 패턴 정리 (0) | 2024.07.09 |
[CS/OOP] 객체지향 프로그래밍과 절차지향 프로그래밍의 차이 (0) | 2024.07.08 |