Adapter Pattern은 호환되지 않는 인터페이스를 가진 클래스들이 함께 동작할 수 있도록 징검다리 역할을 하는 객체를 만들어주는 패턴입니다.
기존 클래스를 그대로 사용하고 싶은데 인터페이스가 맞지 않거나,
새 시스템에 맞춰 기존 코드를 재사용하고 싶거나...
개발할 때 기존의 코드를 재사용하거나 수정해야 하는 경우가 발생할 수 있습니다.
이 때 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 입니다.
싱글톤 패턴은 객체 생성을 제한하여 클래스의 인스턴스가
하나만 존재하도록 보장하는 디자인 패턴입니다.
게임에서 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(); // 호출
마지막으로 볼 것은 Observer Pattern 입니다.
Observer Pattern은 객체 간 1:N 관계를 정의해서, 하나의 객체 상태가 바뀌면 의존하고 있는 다른 객체들한테 자동으로 알림을 주는 디자인 패턴입니다.
사람이 1:N으로 있다고 합시다.
1에 포함된 사람이 일정 시간주기로 계속해서 옷을 다르게 입고 왔다고 가정해봅시다.
그럴 때마다 맞춰보라고 N명의 사람에게 말한다면
그것을 맞추는 데 관찰하고 생각하고 쓸데없는 에너지만 소비할 것입니다.
옵저버 패턴은 1이 되는 객체가 자신의 데이터 변경 시
등록된 N개의 객체들에게 직접 알려주는 디자인 패턴입니다.
주기적으로 확인하지 않아도 데이터 변화에 대응할 수 있기 때문에
게임 최적화를 위해 많이 사용되는 패턴입니다.
유니티에서는 보통 이벤트나 델리게이트(delegate)를 이용해서 구현합니다!
플레이어의 체력이 줄어들면 옵저버에게 바로 가도록 이벤트로 구현합니다.
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);
}
}
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;
}
}