내가 쓰지 않은 코드를 쓰고 싶은데, 호환이 되지 않을 경우 어떻게 해야될까? 코드를 수정하자니, 어떻게 수정할지 막막하고 남의 코드를 건드리자니 이해를 할 수 없다. 이럴 때 쓰는 디자인 패턴이 어댑터 패턴이다

어댑터 패턴

  • 다른 사람이 작성한 코드와 내가 구현한 기능을 서로 연결하기 위해서 쓰는 디자인 패턴
  • 구현 방법이 여러가지 있겠으나, 기본적으로 추가적인 어댑터 스크립트를 작성해서 기존의 코드와 연결하는 방식으로 구현한다

쓰는 이유

  • 내가 쓰지 않은 다른 사람의 코드를 가져다가 쓰려는 경우, 작성한 지 오래 되었거나, 코드를 알아보기 힘들어 수정하기 힘들 경우 사용한다
  • 객체지향의 다형성을 유지하는 용도로도 쓸 수 있다

장점

  • 기존의 코드를 수정하지 않기 때문에, 해당 코드가 변경 불가능하거나 업데이트 될 때에도 대응할 수 있다
  • 최소한의 변경으로 기존 코드를 재사용 할 수 있다

단점

  • 약간의 성능 저하가 발생한다 (물론, 이슈가 되지 않을 만큼 미비한 성능 저하이다)
  • 어댑터가 너무 많으면, 관리가 힘들다. 플레이어의 일반적인 행동들과 관련된 경우만 어댑터 사용을 많이 하는 편이다
  • 해당 스크립트만 봣을 때 무엇으로 상호작용 하는지 파악하는 게 어렵다. 에디터에서 인스펙터 창을 봐야만 확인 가능

주의점
기존 코드를 변환하는 일 없이 받아들일 수 있게, 다른 여러 형식들을 받아 들일 수 있게 어댑터를 만든다. 어댑터가 많을수록 성능저하가 일어나니 최소한으로 만들어야 한다


인터페이스 사용 예시(유니티)

  • 예를 들어 몬스터 개발자와 플레이어 개발자가 서로 다른 사람이라, 공격의 결과 처리 방식이 서로 다르다고 해보자. 이런 경우를 대비해서 미리 인터페이스로 사전에 약속을 하고 구현을 하도록 한다
  • 유니티 에서 가장 많이 사용되는 상호작용 방법이다

IDamagable 인터페이스 스크립트

public interface IDamagable
{
	// 게임 오브젝트와 같이 추가적인 정보를 가져오려고 하는 경우에는 해당 부분을 추가해야 된다
    //  Monster 클래스에는 상위 클래스인 Component에 이미 프로퍼티 get이 구현 되어 있어 생략 가능하다
    // 상위 클래스들에 get이 구현 되어 있지 않다면 추가하자
    public GameObject gameObject { get; }
    public Transform transform { get; }
	public void TakeDamage(GameObject attacker, int attackPoint);
}

Bullet 스크립트

// 공격력
public int attackPoint;

// 충돌 시작 시
OnCollisionEnter(Collision collision)
{
	// collision이 IDamagable 인터페이스를 가지고 있으면, 해당 인터페이스를 가져온다. 즉, Monster 컴포넌트 안에서 구현된 함수를 가져온다
	IDamagable damagable = collision.gameObject.GetComponent<IDamagable>();
    // 컴포넌트나 변수들도 가져올 수 있다
    Rigidbody damagableRig = damagable.GetComponent<Rigidbody>();
    // 요 경우는 깊은 복사라서 데미지 함수 수행을 해도 몬스터에는 적용되지 않는다. 함수를 사용하자. 정보를 가져와야 되는 경우에만 사용한다
    int damagableHP = damagable.HP;
    
    if(damagable != null)
    {
    	Attack(damagable);	
    }
}
void Attack(IDamagable damagable)
{
	// 인터페이스에 포함된 함수를 호출하여 수행. 몬스터에서 수행하는 것처럼 된다	damagable.TakeDamage(gameObject, attackPoint);
}

Monster 스크립트

// 인터페이스 추가
public class Monster : MonoBehaviour, IDamagable
{	
	int HP;
    
	// 데미지를 구현하는 것은 몬스터 컴포넌트 쪽에서 구현한다
	void TakeDamage(GameObject attacker, int attackPoint)
    {
    	HP -= attackPoint;
    	Debug.Log($"{attacker.name}에게 공격받음. {attacker.attackPoint} 데미지");
    }
    
}

결과

  • Bullet을 쐈을 때, Monster의 컴포넌트에 IDamagable 인터페이스가 포함되어 있어서 IDamagable형 변수 damagable로 컴포넌트를 대입 할 수 있다. 그 외의 오브젝트들은 해당 인터페이스 컴포넌트가 없으므로 자연스럽게 걸러진다
  • 기존 코드의 수정 없이 인터페이스 하나만 추가하면 된다(어댑터)
  • IDamagable damagable 에는 인터페이스로 구현된 함수만 포함된다. 참조 타입의 얕은 복사이기 때문에, damagable.TakeDamage를 써도 해당 몬스터의 Monster 컴포넌트 내부에서 수행된다

스위치 컴포넌트

  • IDamagable 인터페이스를 상속받지 않는 컴포넌트를 작성해본다. 상호작용 시 ON / OFF 기능을 수행한다
  • 발판, 스위치 상호작용, 공격 시 반응하는 오브젝트 등에 응용가능
public class Switch : MonoBehaviour
{
    [SerializeField] GameObject door;


    private bool IsDoorOpened => door.activeSelf == false;
    
    // 이 어트리뷰트를 사용하면 에디터 상에서 함수를 실행 시켜볼 수 있다
    [ContextMenu("TestDoorAction")]

    public void SwitchAction()
    {
        // if(door.activeSelf)
        if(IsDoorOpened)
        {
            Close();
        }
        else
        {
            Open();
        }
    }
    public void Open()
    {
        door.SetActive(false);
    }
    public void Close()
    {
        door.SetActive(true);
    }
}

에디터에서 함수 실행해보기

  • 컴포넌트에서 오른쪽 클릭하면 실행 가능한 함수를 에디터 상에서 실행 해볼 수 있다

스위치의 기능 추가

  • 만약, 스위치를 포탄을 쏴서 맞추는 걸로 기능을 수행할 수 있게 구현하려면 어떻게 해야 할까? IDamagable 인터페이스를 추가 해야할까? 그러기에는 뭔가 이상하다. 이럴때 어댑터가 등장한다

유니티에서 어댑터 구현

  • 어댑터로 쓸 수 있는 하나의 방법으로, 유니티 이벤트로 구현한다. 해당 방법 말고 원리만 알고 있으면 다른 방식으로 구현해도 된다
  • 어댑터 이벤트에 수행하고자 하는 컴포넌트의 함수를 추가한다

어댑터로 스위치 구현

  • 플레이어 공격으로 구현 하려면, 어댑터를 추가해서 기존에 만들어둔 IDamagable 인터페이스를 통해 여닫기 기능으로 바꿔주면 된다

어댑터 스크립트

public class SwitchAdapter : MonoBehaviour, IDamagable
{
    // 이벤트의 매개변수는 인터페이스 함수의 매개변수에 맞춘다
    public UnityEvent<GameObject, int> OnDamaged;
    public void TakeDamage(GameObject attacker , int damage)
    {
        // if(OnDamaged != null) {OnDamaged.Invoke()} 의 간략화
        OnDamaged?.Invoke(attacker,damage);
    }
}

  • 사진과 같이 스위치에 어댑터 스크립트를 컴포넌트로 추가하고, 스위치 게임 오브젝트를 이벤트에 드래그&드롭 한다
  • 실행시키는 함수는 SwitchDoor()를 선택하면 어댑터 구현이 완료된다
  • 이렇게 되면 Bullet 스크립트의 OnCollisionEnter() 에서 SwitchIDamagable 인터페이스를 가져와서 수행하는 TakeDamage() 함수가 어댑터의 TakeDamage()로 대체된다. 유니티 에디터 상에서 OnDamaged 이벤트에 SwitchDoor() 함수를 등록 했으므로, 해당 함수가 실행되며 스위치 기능이 수행된다

어댑터로 상호작용 구현

  • 레이캐스트로 일정 거리 안에 상호작용 가능한 물체가 있으면 G키를 눌러 상호작용하는 기능을 구현해보자

플레이어(탱크)에 상호작용 기능 구현

  • 구현하고자 하는 기능을 스크립트로 작성한다
void update()
{
	if(Input.GetKeyDown(KeyCode.G))
	{
	    TryInteract();
	}
}

void TryInteract()
{
    if (Physics.Raycast(muzzlePoint.position, muzzlePoint.forward, out RaycastHit hitInfo, 3f))
    {
        Debug.DrawLine(muzzlePoint.position, hitInfo.point, Color.green, 0.5f);
        IInteractable interactObject = hitInfo.collider.gameObject.GetComponent<IInteractable>();
        if (interactObject != null)
        {
            interactObject.Interact();
        }
    }
    else
    {
        Debug.DrawLine(muzzlePoint.position, muzzlePoint.position + muzzlePoint.forward * 5f, Color.red, 0.5f);
    }
}
  • G 키를 누르면 TryInteract() 함수로 상호작용을 시도한다

인터페이스 IInteractable

  • 상호작용을 구현할 인터페이스의 스크립트를 작성한다
public interface IInteractable
{
    public void Interact();
}
  • 인터페이스는 어댑터에 추가한다

어댑터 InteractAdapter

  • 스위치인터페이스를 연결할 어댑터의 스크립트 작성한다
public class InteractAdapter : MonoBehaviour, IInteractable
{
    public UnityEvent OnInteraction;

    public void Interact()
    {
        OnInteraction?.Invoke();
    }
}
  • 이후, 에디터 상에서 스위치에 어댑터를 추가하고, 어댑터의 함수에 SwitchDoor() 함수를 추가하면 된다

결과

  • 다음과 같이 G 키를 눌렀을 때, 상호작용 거리가 기즈모로 표시된다. 현재는 닿은 물체가 없어 빨간 선이 나온다
  • 스위치와 상호작용 거리 내에서 상호작용을 시도하자, 스위치가 제대로 작동하는 것을 볼 수 있다

TIP
해당 기능으로 NPC 와 상호작용, 보물 상자와 상호작용, 스위치, 문 등과 상호작용 등 다양하게 사용할 수 있다

주의사항

  • 일단 인터페이스로 최대한 기능들을 구성원들과 소통하면서 구현하는 것이 베스트다. 다른 곳에서 코드를 가져오거나, 레거시 코드들을 사용해야 될 경우에 사용하는 것이 어댑터 패턴이다

참고

profile
개발 박살내자

0개의 댓글