디자인 패턴 7가지

이준호·2023년 12월 10일
0

📌 게임 프로그래밍에서 자주 사용되는 7개의 디자인 패턴

1. 싱글톤 패턴 (Singleton Pattern)

  • 특정 클래스의 인스턴스가 단 하나만 존재하도록! 보장하는 디자인 패턴

  • 클래스의 인스턴스를 전역적으로 접근이 가능
    - 싱글톤 패턴에서 생성된 객체는 프로그램 전체에서 공유되고 관리됨

  • 프로그램의 생명주기동안 이 인스턴스는 한 번만 생성이 됨

  • 전역변수보다 안전함!

  • 게임의 설정, 오디오 매니저, UI매니저 같이 전역적으로 하나만 존재해야 하는 객체에 사용됨

public class AudioManager
{
	// 이 인스턴스는 프로그램의 실행 동안 단 한 번만 생성됨
    private static AudioManager _instance;
    
    // 싱글톤 인스턴스에 대한 접근자
    public static AudioManager Instance
    {
    	get
        {
        	if (_instance == null)
            {
            	_instance = new AudioManager();
            }
            return _instance;
        }
    }
}

	// 생성자를 private으로 설정하여 외부에서 인스턴스를 생성하는 것을 방지
    private AudioManager()
    {
    	// 초기화 코드
	}
    
    // 오디오 관련 메소드
    public void PlaySound(string soundName)
    {
    	// 소리 재생
	}
    
    public void StopSound(string soundName)
    {
    	// 소리 중지
	}
}

// 메모리 사용을 최적화하고 코드를 깔금하게 사용할 수 있게 해주는 싱글톤 패턴
AudioManager.Instance.PlaySound("backgroundMusic");

2. 상태 패턴 (State Pattern)

  • 객체의 상태에 따라 객체의 행동을 변경하게 해주는 디자인 패턴

  • 조건문으로 나누는 것이 아니라 객체의 상태를 별도의 클래스로 캡슐화

  • 객체는 runtime에 자신의 행동을 변경함

  • 상태에 따른 행동이 복잡할 때 유용함

  • 쉽게 말해 게임 캐릭터의 상태(대기, 이동, 공격)에 따라 행동이 달라지게 구현하는 것

  • 상태 추가하기가 쉬움

// 상태 인터페이스
public interface IState
{
	void HandleInput(Player player, string input);
    void Update(Player player);
}

// 상태에 따른 클래스들, 대기 상태 클래스
public class StandingState : IState
{
	public void HandleInput(Player player, string input)
    {
    	if (input == "SPACE")
        {
        	player.SetState(new JumpingState());
        }
    }
    
    public void Update(Player player)
    {
    	// 대기 상태에서 할 행동
    }
}

// 점프 상태 클래스
public class JumpingState : IState
{
	public void HandleInput(Player player, string input)
    {
    	// 점프 상태에서의 입력 처리
    }
    
    public void Update(Player player)
    {
    	// 점프 상태에서 할 행동
	}
}

// 플레이어 클래스
public class Player
{
	private IState currentState;
    
    public Player()
    {
    	currentState = new StandingState();
    }
    
    public void SetState(IState newState)
    {
    	currentState = newState;
    }
    
    public void HandleInput(string input)
    {
    	currentState.HandleInput(this, input);
	}
    
    public void Update()
    {
    	currentState.Update(this);
    }
}

// 상태 패턴 사용 예시
Player player = new Player();
player.HandleInput("SPACE");	// 점프 상태로 변경
player.Update();	// 현재 상태(점프)에 맞는 행동 실행

3. 관찰자 패턴 (Observer Pattern)

  • 한 객체 (주인공, 주체)의 상태 변화를 관찰하는 다수의 객체(관찰자들)

  • 주인공 객체가 변하면 관찰자들에게도 자동으로 이를 알려줌

  • 즉, 상태 변화에 따른 자동 업데이트 기능 (구독 시스템과 유사함)

  • 객체들 사이의 의존성을 줄이면서도 중요한 정보를 적절히 공유하는 형태

  • 플레이어의 체력 변화, 점수 휙득 같은 이벤트에 반응하는 시스템을 구현할 때 유용

// 관찰자 인터페이스
public interface IObserver
{
	void Update(int health);
}

// 주인공 인터페이스
public interface ISubject
{
	void Attach(IObserver observer);
    void Detach(IObserver observer);
    void Notify();
}

// 플레이어 클래스 (주인공)
public class Player : ISubject
{
	private int health;
    private List<IObserver> observers = new List<IObserver>();
    
    public int Health
    {
    	 get { return health; }
         set
         {
         	health = value;
            Notify();
         }
    }
    
    public void Attach(IOserver observer)
    {
    	observers.Add(observer);
    }
    
    public void Detach(IObserver observer)
    {
    	observer.Remove(observer);
    }
    
    public void Notify()
    {
    	foreach (var observer in observer)
        {
        	observer.Update(health);
        }
    }
}

// UI 클래스 (관찰자)
public class HealthDisplay : IObserver
{
	public void Update(int health)
    {
    	Console.WriteLine("Health updated to : " + health);
        // UI 업데이트 로직
    }
}

// 옵저버 패턴 사용 예시
Player player = new Player();
HealthDisplay display = new HealthDisplay();

player.Attach(display);
player.Health = 90;	// 플레이어의 체력 변경. 자동으로 관찰자들에게 알림이 감.

4. 커맨드 패턴 (Command Pattern)

  • 주로 게임 유저의 입력을 다룰 때 많이 사용하는 패턴.

  • '요청'을 객체의 형태로 캡슐화를 함.(게임 유저가 이동, 공격, 점프 등의 행동을 요청)
    - 일종의 TV의 리모컨. 각 버튼이 요청

  • 사용자의 요청을 발생시킨 코드와 요청을 수행하는 코드 사이의 의존성을 줄이는 것.
    - 명령을 수행하는 부분은 TV

  • 입력 처리를 깔끔하고 유연하게 관리하고, 명령을 추가하거나 재사용하기 쉬워짐.

// 커맨드 인터페이스
public interface ICommand
{
	void Execute();
}

// 구체적인 커맨드 클래스들.
// 이동 요청 클래스
public class MoveCommand : ICommand
{
	private Player palyer;
    private float x, y;
    
    public MoveCommand(Player player, float x, float y)
    {
    	this.player = player;
        this.x = x;
        this.y = y;
    }
    
    public void Excute()
    {
    	player.Move(x, y);
    }
}

// 점프 요청 클래스
public class JumpCommand : ICommand
{
	private Player player;
    
    public JumpCommand(Player player)
    {
    	this.player = player;
    }
    
    public void Execute()
    {
    	player.Jump();
	}
}

// 플레이어 클래스
public class Player
{
	public void Move(float x, float y)
    {
    	// 이동 로직
	}
    
    public void Jump()
    {
    	// 점프 로직
    }
}

// 커맨드 사용
Player player = new Player();
ICommand moveCommand = new MoveCommand(player, 1.0f, 0.0f);
ICommand jumpCommand = new JumpCommand(player);

moveCommand.Execute();	// 플레이어 이동
jumpCommand.Execute();	// 플레이어 점프

5. 컴포넌트 패턴 (Component Pattern)

  • 객체의 행동을 작은 부품(컴포넌트) 으로 분리

  • 부품을 조합하여 복잡한 동작을 구현하는 방식

  • 게임 캐릭터를 레고 조립하듯이 구성

  • 개별 컴포넌트를 재사용하여 개발 시간도 줄이고, 유지보수가 쉬워지는 패턴.

  • 게임 개발, 게임 엔진에서 많이 쓰이는 방식

// 컴포넌트 인터페이스
public interface IComponent
{
	void Update();
}

// 구체적인 컴포넌트 클래스들
public class MovementComponent : IComponent
{
	public void Update()
    {
    	// 이동 관련 로직
    }
}

public class JumpComponent : IComponent
{
	public void Update()
    {
    	// 점프 관련 로직
    }
}

// 게임 오브젝트 클래스
public class GameObject
{
	private List<IComponent> components = new List<IComponent>();
    
    public void AddComponent(IComponent component)
    {
    	components.Add(component);
    }
    
    public void update()
    {
    	foreach (var component in components)
        {
        	component.Update();
        }
    }
}

// 사용 예시
GameObject player = new GameObject();
player.AddComponent(new MovementComponent());
player.AddCompinent(new JumpComponent());

player.Update();

6. 빌더 패턴 (Builder Pattern)

  • 복잡한 객체의 생성 과정을 단계별로 분리

  • 객체의 생성과 표현을 분리하는 것

  • 생성하는 과정에서 서로 다른 표현을 가진 객체를 만드는 것
    - 샌드위치를 만들 때, 재료를 단계별로 다르게 추가하고 다른 형태의 샌드위치가 생성되는 형태

  • 복잡한 게임 오브젝트나 레벨을 생성할 때 사용 됨. 게임 내의 캐릭터나 맵을 생성하는 과정

// 빌더 인터페이스
public interface IBuilder
{
	void BuildPartA();
    void BuildPartB();
    void BuildPartC();
    GameObject GetResult();
}

// 빌더 클래스
public class ConcreteBuilder : IBuilder
{
	private GameObject gameObject = new GameObject();
    
    public void BuildPartA()
    {
    	// 객체의 일부분 A를 구축 (예 : 캐릭터 모델 추가)
    }
    
    public void BuildPartB()
    {
    	// 객체의 일부분 B를 구축 (예 : 캐릭터 애니메이션 설정)
	}
    
    public void BuildPartC()
    {
    	// 객체의 일부분 C를 구축 (예 : 캐릭터 능력치 설정)
    }
    
    public GameObject GetResult()
    {
    	return gameObject;
    }
}

// 게임 오브젝트 클래스
public class GameObject
{
	// 게임 오브젝트 관련 속성 및 메소드
}

// 빌더 사용
IBuilder builder = new ConcreteBuilder();
builder.BuildPartA();
builder.BuildPartB();
builder.BuildPartC();
GameObject player = builder.GetResult();

7. 플라이웨이트 패턴 (Flyweight Pattern)

  • 메모리 사용을 최적화하기 위한 패턴

  • 객체의 공유를 촉진하는 형태

  • 플라이웨이트 패턴의 상태 종류
    - 변경 불가능한 '공유 상태'

    • 객체마다 다를 수 있는 '개별 상태'
  • 공유 상태는 객체 내부에 저장. 개별 상태는 클라이언트에 의해 관리됨.

  • 도서관에서 책을 빌리는 것.
    - 책은 공유 객체.

    • 각 사람이 책을 보며 개별적으로 필기하면서 정보를 관리할 수 있음.
  • 대규모 게임 환경에서 반복되는 아이템 같은 것을 관리할 때 유용함. 모바일 게임에서도 유용.

// 플라이웨이트 클래스, 모든 나무가 공유하는 정보.
public class TreeModel
{
	// 공유되는 상태 (예 : 모델, 텍스처)
    public string Model { get; set; }
    public string Texture { get; set; }
}

// 공유되지 않는 상태를 관리하는 클래스, 각 나무의 고유한 정보를 저장. 위치를 관리함.
public class Tree
{
	private TreeModel model;
    private float x, y;	// 나무의 위치, 고유 상태
    
    public Tree(TreeModel model, float x, float y)
    {
    	this.model = model;
        this.x = x;
        this.y = y;
    }
    
    public void Deaw()
    {
    	// 나무를 그리는 로직, model의 정보와 위치 정보를 사용
	}
}

// 플라이웨이트 팩토리, TreeModel 클래스 인스턴스를 생성하고 관리함.
// 같은 종류의 나무가 여러번 필요할 때 매번 TreeModel 클래스가 생성하지 않도록 하는게 핵심
public class TreeFactory
{
	private Dictionary<string, TreeModel> models = new Dictionary<string, TreeModel>();
    
    public TreeModel GetTreeModel(string model, string texture)
    {
    	if (!models.ContainsKey(model))
        {
        	models[model] = new TreeModel { Model = model, Texture = texture };
        }
        return models[model];
    }
}

// 사용 예시
TreeFactory factory = new TreeFactory();

var oakModel = factory.GetTreeModel("oak", "oakTexture");
var pineModel = factory.GetTreeModel("pine", "pineTexture");

List<Tree> trees = new List<Tree>();
trees.Add(new Tree(oakModel, 10, 20));
trees.Add(new Tree(pineModel, 50, 60));

foreach (var tree in trees)
{
	tree.Draw();
}

📌 디자인 패턴의 적절한 사용

  • 디자인 패턴은 애초에 특정 문제를 해결하기 위해 고려된 것임.

    • 성능과 메모리 사용을 고려
    • 재사용성과 확장성을 고려
  • 디자인 패턴은 좋은 도구이지만, 모든 상황에 억지로 사용하려는 것은 금물. 오히려 코드가 복잡해지는 역효과.

  • 팀과의 소통이 중요함. 디자인 패턴은 모든 팀원이 공유하면서 작성해나가는 것이 중요.

profile
No Easy Day

0개의 댓글