1. 추상 클래스 (Abstract Classes)
정의: 클래스 선언부에
abstract
키워드를 사용하여 정의하는 특수한 형태의 클래스입니다. 추상 클래스는 그 자체로 인스턴스화될 수 없으며(즉,new
키워드로 객체를 직접 생성할 수 없음), 다른 클래스가 상속받아 구현을 완성하기 위한 기반(Base) 클래스로 사용됩니다.특징:
- 미완성 설계도: 추상 클래스는 완성된 구현(일반 메서드, 필드, 프로퍼티 등)과 미완성된 부분(추상 멤버)을 모두 포함할 수 있습니다.
- 추상 멤버 포함 가능:
abstract
키워드로 선언된 멤버(주로 메서드나 프로퍼티)를 가질 수 있습니다. 추상 멤버는 구현부({}
)가 없으며, 오직 시그니처만 정의합니다. - 구현 강제: 추상 클래스를 상속받는 자식 클래스는 부모 클래스에 정의된 모든 추상 멤버를 반드시
override
키워드를 사용하여 구현해야 합니다. 이는 자식 클래스가 특정 기능을 반드시 제공하도록 강제하는 역할을 합니다. - 단일 상속: C#의 클래스 상속 규칙에 따라, 클래스는 오직 하나의 추상 클래스(또는 일반 클래스)만 상속받을 수 있습니다.
주요 용도: 여러 관련 클래스들 간의 공통적인 상태(필드)와 구현(메서드) 을 공유하면서, 동시에 각 자식 클래스가 반드시 구현해야 하는 특정 기능(추상 메서드) 의 규약을 정의하고 싶을 때 사용됩니다. "is-a" 관계(예: '고블린은 몬스터이다')를 표현하며, 기본적인 뼈대와 일부 구현을 제공하는 데 적합합니다.
예시:
public abstract class Character : MonoBehaviour // 추상 클래스 정의 { public float health = 100f; // 공통 상태 (구현 포함) public virtual void Move(Vector3 direction) // 가상 메서드 (기본 구현 제공, 재정의 가능) { transform.Translate(direction * Time.deltaTime); } public abstract void Attack(); // 추상 메서드 (구현 없음, 자식 클래스에서 반드시 구현해야 함) public void TakeDamage(float amount) // 일반 메서드 (공통 구현 제공) { health -= amount; if (health <= 0) Die(); } protected abstract void Die(); // protected 추상 메서드 } public class PlayerCharacter : Character // 추상 클래스 상속 { public override void Attack() // 추상 메서드 구현 (필수) { Debug.Log("Player attacks with sword!"); } protected override void Die() // protected 추상 메서드 구현 (필수) { Debug.Log("Player died."); // 게임 오버 로직 등 } }
2. 인터페이스 (Interfaces)
정의: 클래스 또는 구조체가 반드시 구현해야 하는 관련 멤버(메서드, 프로퍼티, 이벤트, 인덱서)들의 집합, 즉 규약(Contract)만을 정의하는 완전한 추상 타입입니다.
interface
키워드를 사용하여 선언합니다.특징:
- 순수 규약: 인터페이스는 (C# 8.0 이전까지는) 멤버의 구현(코드 본문)을 포함할 수 없으며, 오직 시그니처(이름, 매개변수, 반환 타입 등)만을 정의합니다. (C# 8.0부터는 인터페이스에 기본 구현(Default Implementation)을 포함할 수 있게 되었지만, 인터페이스의 주된 목적은 여전히 규약 정의입니다.)
- 구현 강제: 인터페이스를 구현(Implement)하는 클래스나 구조체는 해당 인터페이스에 정의된 모든 멤버를 반드시 구현해야 합니다.
- 다중 구현 가능: 클래스는 여러 개의 인터페이스를 동시에 구현할 수 있습니다. 이는 C#의 단일 클래스 상속 제한을 보완하여 다중 상속과 유사한 효과를 낼 수 있게 합니다.
- 상태(필드) 없음: 인터페이스는 인스턴스 필드(멤버 변수)를 가질 수 없습니다. 오직 기능의 명세만을 정의합니다.
- 암묵적 public: 인터페이스의 모든 멤버는 기본적으로
public
접근 수준을 가집니다 (명시적으로 지정하지 않음).
주요 용도: 서로 다른 상속 계층에 속하거나 관련이 없는 클래스들에게 공통된 기능(Capability) 또는 역할(Role) 을 부여하고 싶을 때 사용됩니다. 클래스 간의 느슨한 결합(Loose Coupling) 을 달성하는 핵심적인 수단입니다. "can-do"(예: '날 수 있다') 또는 "has-a"(예: '체력 속성을 가진다'는 아니지만 '피해를 받을 수 있다'는 기능) 관계의 측면을 표현하는 데 적합합니다.
예시:
// 피해를 받을 수 있는 모든 객체를 위한 인터페이스 public interface IDamageable { float CurrentHealth { get; } // 읽기 전용 프로퍼티 규약 void TakeDamage(float amount); // 메서드 규약 } // 상호작용 가능한 모든 객체를 위한 인터페이스 public interface IInteractable { string InteractionPrompt { get; } // 상호작용 문구 프로퍼티 규약 void Interact(GameObject interactor); // 상호작용 메서드 규약 } // Player 클래스가 두 인터페이스를 모두 구현 public class Player : MonoBehaviour, IDamageable, IInteractable { public float CurrentHealth { get; private set; } = 100f; public string InteractionPrompt => "Talk to Player"; // 프로퍼티 구현 public void TakeDamage(float amount) // IDamageable 구현 { CurrentHealth -= amount; Debug.Log($"Player took {amount} damage. Health: {CurrentHealth}"); } public void Interact(GameObject interactor) // IInteractable 구현 { Debug.Log("Player is interacting with " + interactor.name); } } // 파괴 가능한 상자도 IDamageable 구현 가능 public class DestructibleBox : MonoBehaviour, IDamageable { public float CurrentHealth { get; private set; } = 20f; public void TakeDamage(float amount) { CurrentHealth -= amount; if (CurrentHealth <= 0) Destroy(gameObject); } }
3. 가상 메서드 (Virtual Methods)
정의: 부모 클래스에서
virtual
키워드를 사용하여 선언된 메서드입니다. 가상 메서드는 기본 구현(Default Implementation) 을 가지며, 자식 클래스에서override
키워드를 사용하여 선택적으로 재정의(Override) 될 수 있습니다.특징:
- 기본 구현 제공:
virtual
메서드는 부모 클래스에서 완전한 구현을 가집니다. - 선택적 재정의: 자식 클래스는 부모의 가상 메서드를 재정의하여 자신만의 동작을 구현할 수도 있고, 재정의하지 않고 부모의 기본 동작을 그대로 상속받아 사용할 수도 있습니다.
- 다형성 지원: 부모 클래스 타입의 참조 변수를 통해 가상 메서드를 호출하면, 런타임 시 해당 변수가 실제로 가리키는 객체의 타입에 따라 재정의된 메서드(재정의된 경우) 또는 부모의 기본 메서드(재정의되지 않은 경우)가 호출됩니다.
- 기본 구현 제공:
주요 용도: 상속 계층 구조 내에서 대부분의 자식 클래스에게는 유용한 기본 동작을 제공하면서, 일부 자식 클래스에게는 그 동작을 변경하거나 확장할 수 있는 유연성을 부여하고 싶을 때 사용합니다.
예시: (위
Character
추상 클래스 예시의Move
메서드 참고) 또는,public class Weapon : MonoBehaviour { public virtual void Reload() // 가상 메서드 (기본 재장전 로직) { Debug.Log("Reloading weapon..."); // 기본 재장전 애니메이션, 사운드 등 } } public class Shotgun : Weapon { public override void Reload() // 가상 메서드 재정의 { Debug.Log("Reloading shotgun (shell by shell)..."); // 샷건 고유의 재장전 애니메이션, 사운드 등 // base.Reload(); // 필요하다면 부모의 기본 로직 호출 가능 } } public class Pistol : Weapon { // Reload 메서드를 재정의하지 않음 -> 부모의 기본 Reload() 동작 사용 }
4. 추상 메서드 vs. 가상 메서드
구분 | 추상 메서드 (abstract ) |
가상 메서드 (virtual ) |
---|---|---|
구현부 | 없음 ({} 없음) |
있음 ({} 있음, 기본 동작 정의) |
선언 위치 | abstract 클래스 내에서만 가능 |
abstract 또는 일반 클래스 내 가능 |
구현 강제성 | 자식 클래스에서 반드시 재정의 | 자식 클래스에서 선택적 재정의 |
목적 | 구현 없이 규약만 정의, 구현 강제 | 기본 구현 제공, 선택적 변경 허용 |
5. 추상 클래스 vs. 인터페이스 비교
구분 | 추상 클래스 (Abstract Class) | 인터페이스 (Interface) |
---|---|---|
핵심 역할 | 관련 클래스들의 공통 기반 (상태+행동) 제공 | 클래스의 기능/역할(규약) 정의 |
인스턴스화 | 불가능 | 불가능 |
상속/구현 | 단일 상속 (클래스 하나만 상속 가능) | 다중 구현 가능 (여러 인터페이스 구현 가능) |
멤버 구현 | 구현된 멤버(필드, 메서드) 가질 수 있음 | 구현된 멤버 가질 수 없음 (C# 8.0 이전 기준) |
필드(상태) | 가질 수 있음 | 가질 수 없음 |
생성자 | 가질 수 있음 | 가질 수 없음 |
접근 제한자 | 멤버별 지정 가능 (public , protected 등) |
기본 public (제한적) |
관계 표현 | "is-a" 관계 (예: Dog is an Animal ) |
"can-do" 관계 (예: Player can TakeDamage ) |
주요 사용 시점 | 공통 구현과 상태를 공유하는 밀접한 관련 클래스 계층 | 서로 다른 클래스에 공통 기능 부여, 느슨한 결합 |
6. Unity에서의 활용 사례
- 추상 클래스:
- 공통 로직을 가진 상태 머신의
BaseState
정의. - 다양한 종류의 캐릭터(
Player
,NPC
,Monster
)가 공유하는 기본 속성 및 메서드를 포함하는BaseCharacter
정의. - 스킬이나 능력의 기본 구조를 정의하는
AbilityBase
정의.
- 공통 로직을 가진 상태 머신의
- 인터페이스:
- 피해를 받을 수 있는 모든 객체(
Player
,Enemy
,DestructibleObject
)를 위한IDamageable
. - 플레이어와 상호작용 가능한 모든 객체(
NPC
,Chest
,Lever
)를 위한IInteractable
. - 오브젝트 풀링 시스템에서 재사용 가능한 객체를 위한
IPoolable
(예:OnSpawn()
,OnDespawn()
메서드 정의). - 이동 가능한 모든 유닛을 위한
IMovable
. - 시스템 간의 의존성을 낮추기 위해 서비스 로케이터나 의존성 주입 패턴에서 서비스의 계약으로 사용.
- 피해를 받을 수 있는 모든 객체(
- 가상 메서드:
MonoBehaviour
의 생명주기 메서드(Awake
,Start
,Update
등)는 개념적으로 우리가 재정의하여 사용하는 가상 메서드와 유사합니다.- 기본 클래스에서 공통적인 초기화(
Initialize
)나 정리(Cleanup
) 로직을virtual
메서드로 제공하고, 자식 클래스에서 필요에 따라override
하여 추가적인 초기화/정리 작업을 수행하도록 합니다. Enemy
클래스의virtual Die()
메서드처럼, 기본 소멸 효과를 제공하되 특정 적 타입은 특별한 효과를 내도록 재정의할 수 있게 합니다.
7. 선택 가이드 (When to Use Which)
- "이 객체들은 ~라는 공통 기반에서 나왔고, 일부 상태와 기본 구현을 공유한다" 면 추상 클래스를 고려합니다. 자식들에게 특정 메서드의 구현을 강제하고 싶다면 추상 메서드를 사용합니다.
- "이 객체들은 서로 상속 관계는 없지만, ~라는 기능을 수행할 수 있어야 한다" 면 인터페이스를 고려합니다. 최대한의 유연성과 느슨한 결합이 필요할 때, 또는 다중 상속의 효과가 필요할 때 적합합니다.
- "기본적인 동작 방식은 있지만, 특정 자식 타입에서는 다르게 동작해야 할 수도 있다" 면 (추상 클래스 또는 일반 클래스 내에서) 가상 메서드를 고려합니다. 구현을 강제하지 않고 선택적인 재정의를 허용합니다.
종종 추상 클래스와 인터페이스를 함께 사용하기도 합니다. 예를 들어, 추상 클래스가 인터페이스를 구현하면서 일부 메서드는 추상 메서드로 남겨두어 자식 클래스가 구현하도록 할 수 있습니다.
결론
추상 클래스, 인터페이스, 그리고 가상 메서드(및 추상 메서드)는 C#에서 추상화와 다형성을 구현하는 핵심적인 도구들입니다. 추상 클래스는 공통된 기반과 구현 일부를 제공하며 관련 클래스들의 뼈대를 잡는 데 유용하고, 인터페이스는 클래스의 상속 계층과 관계없이 공통된 기능이나 역할에 대한 순수한 계약을 정의하여 시스템의 유연성과 확장성을 극대화합니다. 가상 메서드는 기본 구현을 제공하면서도 파생 클래스에서 동작을 변경할 수 있는 다형적 유연성을 부여합니다. Unity C# 개발에서 이러한 기능들을 언제, 왜 사용해야 하는지 명확히 이해하고 상황에 맞게 적절히 선택하여 활용하는 것은 객체 지향 원칙에 부합하는 견고하고 유지보수하기 쉬우며 확장 가능한 게임 시스템을 설계하는 데 매우 중요합니다. 이들을 효과적으로 사용함으로써 코드의 복잡성을 관리하고, 변화에 유연하게 대처하며, 협업을 용이하게 만들 수 있습니다.