[Unity] 디자인 패턴

AsiaticRicecake·2025년 4월 22일

1. 📖 Adapter Pattern

Adapter Pattern은 호환되지 않는 인터페이스를 가진 클래스들함께 동작할 수 있도록 징검다리 역할을 하는 객체를 만들어주는 패턴입니다.

기존 클래스를 그대로 사용하고 싶은데 인터페이스가 맞지 않거나,
새 시스템에 맞춰 기존 코드를 재사용하고 싶거나...

개발할 때 기존의 코드를 재사용하거나 수정해야 하는 경우가 발생할 수 있습니다.

이 때 Adapter Pattern을 통해 기존 클래스를 수정하지 않고도,
인터페이스가 다른 클래스를 재사용할 수 있습니다.

1-1 🔖 Adapter Pattern 예시

탱크 게임이 있다고 가정해봅시다

탱크가 포탄을 쏘겠죠?

그 포탄으로 몬스터를 쏜다면 체력감소가 필요할 것입니다.
근데 문을 열 수 있는 스위치를 쏜다면 체력이 감소하는 것이 아닌 문을 열고 닫아야 합니다.

이 두 상황은 탱크가 포탄을 쏴서 맞춘다는 동일하지만 세부적으로 동작하는 로직이 다릅니다.

이 상황에서 Adapter Pattern을 통해 변환이 필요할 수 있습니다.

public interface IHitReceiver
{
    void OnHit(int damage);
}
public class Monster : MonoBehaviour, IHitReceiver
{
    public int hp = 100;

    public void OnHit(int damage)
    {
        hp -= damage;
        Debug.Log("몬스터 맞음! HP: " + hp);
    }
}
public class Switch : MonoBehaviour
{
    [SerializeField] GameObject door;

    private bool IsDoorOpened => door.activeSelf == false;

    public void SwitchAction()
    {
        if (IsDoorOpened)
        {
            Close();
        }
        else
        {            
            Open();
        }
    }

    public void Open()
    {
        door.SetActive(false);
    }

    public void Close()
    {
        door.SetActive(true);
    }

}

자 여기 보시면 Monster의 경우 IHitReceiver 인터페이스를 받아 사용하고 있지만 Switch의 경우 문 여닫는 기능만 구현하기 때문에 IHitReceiver 인터페이스를 사용하기 힘든 것을 알 수 있습니다.

이 때 Adapter를 만들어 연결이 필요합니다!

public class SwitchHitAdapter : MonoBehaviour, IHitReceiver
{
    public Switch targetSwitch;

    public void OnHit(int damage)
    {
        targetSwitch.Activate();   
        Debug.Log("스위치 작동됨!");
    }
}

IHitReceiver 인터페이스를 사용한다면 무조건 데미지가 들어가야 사용할 수 있습니다.
하지만 Switch의 경우 그런 기능이 필요가 없죠..
스위치 때렸는데 데미지 들어가는 건 이상하잖아요?

그래서 SwitchHitAdapter를 따로 만들어서 로직을 바꿔주는 겁니다.

물론 가능하다면 Switch의 기능 자체를 바꾸어서 사용하는 게 좋겠죠
하지만 그건 어디까지나 자기가 개발한 경우에만 해당합니다.

외부에서 받았거나 동료들이 쓴 코드를 수정하는 것은 상당히 어렵습니다....
Switch를 남이 쓴 코드에서 가져왔다고 가정했을 때 섣불리 수정하는 건 위험한 행동입니다.

public class Bullet : MonoBehaviour
{
    public int damage = 10;

    private void OnCollisionEnter(Collision collision)
    {
        IHitReceiver receiver = collision.gameObject.GetComponent<IHitReceiver>();

        if (receiver != null)
        {
            receiver.OnHit(damage);
        }
    }
}

이런 식으로 Switch 구문을 자체를 변경하지 않고도 Adapter를 만들어 쓸 수 있게 만들어 주는 패턴이 Adapter Pattern 입니다.

2. 📖 Singleton Pattern

싱글톤 패턴은 객체 생성을 제한하여 클래스의 인스턴스가
하나만 존재하도록 보장하는 디자인 패턴입니다.

게임에서 GameManager, AudioManager, UIManager 등 매니저라는 이름으로 만들어서
프로젝트 내에서 오직 하나만 만들어 통합관리하도록 만드는 것입니다.

그리고 외부에서 쉽게 접근할 수 있도록 전역적으로 접근 가능한 메서드를 제공하고
전환될 때 파괴되지 않도록 만들어줍니다.

단일의 인스턴스와 전역적 접근이 더해지기 때문에
오브젝트들이 서로를 참조하고 있는 결합도를 낮출 수 있습니다.

결합도는 다른 것이 아니고 오브젝트가 여러가지로 엉켜있는 것을 말합니다.
결합도가 높을수록 결합한 오브젝트가 많다는 뜻이므로 관리하기 상당히 어렵습니다.

물론 단점도 있습니다.

너무 많이 남용하면 코드가 복잡해질 수 있고
객체들이 싱글톤 객체의 데이터에 의존하게 되는 현상이 발생할 수 있습니다.

필요한 경우에만 사용하도록 해야합니다.

public class GameManager : MonoBehaviour
{
	/* 게임 매니저 클래스가 private으로 하나만 존재하도록 하고 
     public으로 어디서든 접근 가능하도록 함 */
  	private static GameManager instance;
	public static GameManager Instance // 없는 경우 만들어 주기
	{
    	get
    	{
        	if (instance == null)
        	{
           	 	GameObject gameObject = new GameObject("GameManger");
           	 	instance = gameObject.AddComponent<GameManager>();
        	}

        	return instance;
    	}
	}

    void Awake()
    {
        if (instance == null)
        {
            // 인스턴스 할당
            instance = this;
            // 씬이 전환되어도 파괴되지 않게 설정
            DontDestroyOnLoad(gameObject);
        }
        else
        {   
        	// 인스턴스가 하나만 나타나도록 중복 제거 
            Destroy(gameObject);
        }
    }
    
    public void StartGame()
    {
        Debug.Log("Game Start");
    }    
}
GameManager.Instance.StartGame(); // 호출

3. 📖 Observer Pattern

마지막으로 볼 것은 Observer Pattern 입니다.

Observer Pattern은 객체 간 1:N 관계를 정의해서, 하나의 객체 상태가 바뀌면 의존하고 있는 다른 객체들한테 자동으로 알림을 주는 디자인 패턴입니다.

사람이 1:N으로 있다고 합시다.
1에 포함된 사람이 일정 시간주기로 계속해서 옷을 다르게 입고 왔다고 가정해봅시다.
그럴 때마다 맞춰보라고 N명의 사람에게 말한다면
그것을 맞추는 데 관찰하고 생각하고 쓸데없는 에너지만 소비할 것입니다.

옵저버 패턴은 1이 되는 객체가 자신의 데이터 변경 시
등록된 N개의 객체들에게 직접 알려주는 디자인 패턴입니다.

주기적으로 확인하지 않아도 데이터 변화에 대응할 수 있기 때문에
게임 최적화를 위해 많이 사용되는 패턴입니다.

유니티에서는 보통 이벤트델리게이트(delegate)를 이용해서 구현합니다!

3-1 🔖 Observer Pattern 예시

3-1-1 ✔️ Subject (1이 되는 객체)

플레이어의 체력이 줄어들면 옵저버에게 바로 가도록 이벤트로 구현합니다.

using System;
using UnityEngine;

public class Player : MonoBehaviour
{
    public static Action<int> OnHealthChanged; // 옵저버에게 전달할 이벤트

    private int health = 100;

    public void TakeDamage(int damage)
    {
        health -= damage;
        Debug.Log("플레이어 피해: " + damage);

        // 옵저버들에게 알림
        OnHealthChanged?.Invoke(health);
    }
}

3-1-2 ✔️ Observer (N이 되는 객체, 관찰자)

Subject에서 플레이어의 체력이 줄어들면 이벤트가 발생하도록 만들면 Observer에서 그 이벤트를 받아 내부로직을 수행하는 구조입니다. 간단하죠?

using UnityEngine;
using UnityEngine.UI;

public class HealthUI : MonoBehaviour
{
    public Text healthText;

    private void OnEnable()
    {
        Player.OnHealthChanged += UpdateHealthUI;
    }

    private void OnDisable()
    {
        Player.OnHealthChanged -= UpdateHealthUI;
    }

    private void UpdateHealthUI(int newHealth)
    {
        healthText.text = "HP: " + newHealth;
    }
}

0개의 댓글