일단 유니티는 주로 C#으로 스크립팅을 한다 즉 C#에 있는 편리한 기능을
마음것 사용할수도있는거기도하다.
물론 유니티는 신규버전의 닷넷을 지원안하기에 최신기술을쓸수없지만,
전통적인 유용한 기능, 바로 인터페이스를 사용할수있다.
왜 이 이야기가 나왔냐면 바로 이 C#으로 유니티에서 게임을만들텐데 처음에 하나하나 만들땐쉽다.
다만.. 이것들이쌓인다면? 전부다 부모공통화에 별의별거 다낀다면? 바로 대참사가 나버린다. 하지만 공통부모개념에서 상속으로 자식한테 뻗어나갈필요가없다. 바로 그러라고있는게 인터페이스다.
👉 이번 글에서 다룰 내용들
*1. 그래서 인터페이스가 뭔데
• 인터페이스 개요2. 상속만 사용할시 문제점
• 다양한 클래스와 상호할때의 문제 if - else의 한계3. 인터페이스 활용하기
• TryGetComponent와 IUse로 간단한 연결구현4. 마무리 요약
• 이걸 쓰면 어떤게 좋은지

그래서 인터페이스가 무엇인가하면 상속과 서로 보완하는 개념이라볼수있다.
"클래스가 어떤 행동을 할 수 있는지 정의해 두는 약속입니다."
뭐 다들 인터넷이나 책에선 이 클래스가 어떤 행동을 할 수 있는지 정의해 두는 약속이라곤 하는데 사실 말만들어선 굳이? 랑 왜
제약을 걸지 이생각이 날꺼다.
하지만 사실 이방식을쓰는건 단순한 족쇄의 역할이 아니다. 사실 말만들어선
족쇄가맞다. 다만 이 족쇄로 무언갈 할수있기에 OOP에서 아주유용한 기능이기도하고.
일단 인터페이스를정확히 말하기전 다른형제인 상속에대해서도 알아야편하다.
상속은 한마디로 Is-A (~는 ~이다), 즉 유전적인 '혈통'과 같다.
부모의 기능과 변수를 그대로 가져와 재사용할 수 있다는 강력한 장점이 있죠. 적당히 쓰면 참 좋은데, 유니티에서는 프로젝트가 조금만 커져도 조금씩 스노우볼이 굴러간다는게 문제다.
C#에서는 단 하나의 부모만 상속받을 수 있기 때문에 클래스 구조가 점차 딱딱하게 굳어버린다 즉 이게 어떤문제를 야기하냐면.
예를 들어 나중에 단순한 선풍기 하나를 추가하려고 했는데...
전자제품➔가전제품➔금속오브젝트➔선풍기
이딴 식으로 거대한 부모 상속 피라미드가 쌓여버릴 수 있습니다.
이걸 구현하려고 버트하나 누르자고 조건문으로 누르는 대상이 자식 클래스하나하나대입하는 하드코딩을 하거나, 부모한테 공통 로직을 밀어 넣으려다 보면 부모와 연관된 온갖 자식 클래스들이 다 꼬여버리는 개판오분전이 생긴다.
여기서 관건은 바로 굳이 상속을써야하는가이다 그냥 단지 버튼을 누른다. 우린이거만필요할텐데. 그냥 자식한테 버튼누르고싶은데말이지.
상속 체계(Is-A) 대신, 행위의 자격만 부여해 주는 것. 이것이 바로 인터페이스의 핵심이다.
상속의 경우 자식에게 강제로 죄다 박아버린다면,
인터페이스는 부모가 누구든 상관이딱히업고 그냥 너 이거해야해 이거가져야만해 이정도다.
public interface IUse
{
void Use(); // " 이 인터페이스를쓸려면 반드시 넣어야하는기능"
}
// 2. 클래스에 인터페이스 장착
public class Fan : MonoBehaviour, IUse
{
// IUse를 붙였기 때문에 Use()를 만들지 않으면 에러가 난다. 그것이 약속이니깐.
public void Use()
{
Debug.Log("위이잉");
}
}
이처럼 빈 껍데기 메서드 하나 달랑 던져주고 구현하라고 강제하는 게 전부이다.
근데 여기까지만 보면 강제한다..이게끝이다 솔직히 왜 만들어야할지 의문일테고말이지?
이제 아까 이야기했던 족쇄라는 개념으로보자면.
인터페이스를 만들면 구현부 클래스에 메서드를 강제로 만들라고 오류를 만들어내는 꼬라지가 말 그대로 족쇄다.
하지만 이 족쇄가 유용한게 하나가있다 바로 공통된다는거다
선풍기든, 전등 스위치든, 폭탄 스위치든, 보물상자든 상관없이 IUse 라는 인터페이스를 가진다면 외부에서는 얘가 선풍기인지 폭탄인지 몰라도 그냥 Use() 버튼을 누를 수 있게 되는 겁니다.
즉 이놈부모가누군지 클래스가 누군지 알필요도없다 클래스를통한 참조와통신이아닌 인터페이스가 통신창구가될수있다.
즉 부모나 각 자식클래스 찾을생각도없이 그저 인터페이스를 대상으로 연결하면그만이다.
인터페이스의 진짜 가치는 상호작용해야 할 객체가 여러 개로 늘어날 때 비로소 드러난다.
인터페이스 없이, 플레이어가 눈앞의 오브젝트와 버튼누르기를 구현한다고 쳐보자.
플레이어 눈앞에 선풍기(Fan), 문(Door), 폭탄(Bomb)이 있다.
인터페이스가 없다면 플레이어의 상호작용 스크립트는 보통 이런 식으로 짜여진다
// 플레이어가 E키를 눌렀을 때 실행되는 코드
void Interact()
{
// Raycast로 감지한 오브젝트
GameObject hitObject = GetLookAtObject();
// 1. 선풍기인가?
Fan fan = hitObject.GetComponent<Fan>();
if (fan != null)
{
fan.Use();
return;
}
// 2. 문인가?
Door door = hitObject.GetComponent<Door>();
if (door != null)
{
door.Open(); // 심지어 메서드 이름도 제각각!
return;
}
// 3. 폭탄인가?
Bomb bomb = hitObject.GetComponent<Bomb>();
if (bomb != null)
{
bomb.Explode();
return;
}
}
뭐 부모를통해서 공통상속을 만들수도있지만 문제는 각 기능별 결합이 커져버려서 그것조차도 문제가 생겨버린다 TryGetComponent를 사용해도 하나하나 검수해야하기에 여전히 자원을 미친듯이 갉아먹는다.
플레이어가 칼을 휘둘러 데미지를 주는 상황입니다. 몬스터뿐만 아니라 '파괴 가능한 나무상자'나 '폭발하는 드럼통'에도 데미지를 주고 싶다.
공통 부모 클래스인 LivingEntity나 UnitBase를 만들어 처리하려고 하면 바로 벽에 부딪힙니다.
// 몬스터는 LivingEntity를 상속받아 HP와 TakeDamage()를 가짐
public class Monster : LivingEntity { ... }
// 하지만 나무상자는 이미 'PropBase'라는 환경 오브젝트 부모를 상속받고 있음!
// C#은 다중 상속이 불가능하므로 PropBase와 LivingEntity를 동시에 상속받을 수 없다.
public class WoodenBox : PropBase, LivingEntity // ❌ 에러 발생! (C# 단일 상속 제한)
{
}
나무상자에 데미지 기능을 넣겠다고 부모를 LivingEntity로 바꾸면, 상자에 필요도 없는 'AI 이동 로직', '마나(MP)', '시야 감지' 같은 뚱뚱한 부모 코드가 억지로 추가된다.
결국 상속 구조가 꼬여서 모든 클래스에 HP 관련 코드를 복사-붙여넣기(하드코딩)하거나 코드 전체를 엎어야 하는 대참사가 발생한다.
이렇게 2가지 예시가있기에 상속만으로는 해결이 안되는 문제가있는거다.
앞서 겪었던 대참사를 해결하는 방법은 간단하다. 각 클래스에 인터페이스를 던져주고, 호출하는 쪽에선 상대의 클래스 대신 인터페이스로 접근하는것이다.
먼저 상호작용 가능한 객체들이 공유할 규격인 IUse 인터페이스를 선언후 쓰는놈한테 다 넣어준다. 심지어 여러개도넣을수있기에 유용하다
// 1. 공통 인터페이스 선언
public interface IUse
{
void Use();
}
// 2. 부모가 완전히 다른 각 클래스에 IUse 장착
public class Fan : MonoBehaviour, IUse
{
public void Use() => Debug.Log("선풍기가 회전합니다.");
}
public class Door : MonoBehaviour, IUse
{
public void Use() => Debug.Log("문이 열립니다.");
}
public class Bomb : MonoBehaviour, IUse
{
public void Use() => Debug.Log("폭탄이 터집니다!");
}
부모 클래스가 무엇이든, 심지어 서로 아무런 상속 관계가 없어도 상관없다. 그저 IUse만 붙여주면 준비 끝.
이제 플레이어의 상호작용 스크립트로 돌아온후. 복잡했던 조건문 덩어리가 어떻게 바뀌는지 확인할 수 있다.
void Interact()
{
GameObject hitObject = GetLookAtObject();
if (hitObject.TryGetComponent<IUse>(out var interactable))
{
interactable.Use(); // 1줄로 끝
}
}
무엇이 달라졌는가? 상대를 몰라도 찌를 수 있다는거다
플레이어 스크립트는 대상이 Fan인지 Door인지 알 필요가 없다. 그저 IUse 인터페이스를 가지고 있는지만 확인하고 Use()를 호출하면끝이기에 클래스를 전부 확인할필요가없다.
나중에 게임에 '보물상자', '엘리베이터', 'NPC'가 100개 추가되어도 플레이어의 Interact() 코드는 단 한 줄도 수정할 필요가 없다. 새로운 클래스에 IUse만 붙여주면 즉각 연동되기때문이다.
| 구분 | 적용 전 (상속 & if-else 분기) | 적용 후 (인터페이스 & TryGetComponent) |
|---|---|---|
| 관점 | Is-A (혈통): 정체성 중심 | Has-Do (자격): 가능 행위 중심 |
| 의존성 | 상대의 진짜 클래스 타입(Fan, Door)을 알아야 함 | 상대의 정체와 상관없이 인터페이스 자격증만 확인 |
| 확장성 | 오브젝트 추가 시마다 호출부(if-else) 수정 필요 | 오브젝트가 100개 추가되어도 호출부 코드 수정 0줄 |
| 클래스 구조 | 공통 부모 상속 구조 꼬임 (단일 상속 제한) | 부모 혈통 상관없이 필요한 자격만 뗐다 붙여 조립 |
| 호출 방식 | 클래스마다 GetComponent 후 null 체크 반복 | hit.TryGetComponent<IUse>(out var target) 1줄로 완료 |
| 자원 효율 | 불필요한 연쇄 탐색으로 CPU 오버헤드 발생 | 단일 검색 및 가비지 할당(GC Alloc) 최소화 |
처음보면 이게뭔지 이해가 잘안갈수도있다. 하지만 충분히 배울만하고 유용한 가치있는 개념이자 차후 도움이될것이기에 한번쯤은 잘안되도 사용해가면서 감을잡아나간다면 아주 유용한 기술이될겄이다.