OOP에서 상속에 관한 키워드 virtual(추상), abstract(가상) 한정자와 interface를 정리했습니다.
세가지 키워드 모두 Override(재정의) 할 수 있다는 공통점이 있지만, 사용 방식과 목적에 차이가 있습니다.
Virtual
메서드나 속성, 어떤 이벤트에 아래 코드처럼 virtual 키워드를 붙여 사용할 수 있습니다.
부모 클래스에서는 virtual로 선언, 자식 클래스에서는 override로 재정의합니다.
자식에서 override 하는게 필수가 아니기도 하며,
override를 사용하지 않을 경우 상속받는 기능 그대로 완전하게 수행합니다.
class Mammal {
public virtual void Cry() {
Console.WriteLine("Mammal Animal makes Noise and Vocalization");
}
}
class Dog : Mammal {
public override void Cry() {
Console.WriteLine("Barks, 멍멍!");
// Dog 클래스는 Cry메서드를 Override 했군요
}
}
class Human : Mammal {
// 암것도 없으니 암것도 굳이 Override 하지 않음
// 부모 클래스 기능 그대로 물려 받아용
}
필요에 따라 자식에서 class에서 override할 수 있습니다. 고로 위 코드의 결과는 아래와 같습니다.
Mammal m1 = new Dog();
m1.Cry(); // 출력 : barks, 멍멍!
Mammal m2 = new Human();
m2.Cry(); // 출력 : Mammal Animal makes Noise and Vocalization
※ 주의
virtual 은 오직 override로만 덮어쓸 수 있습니다.
public class Dog : Mammal {
public new void Speak() { // 이렇게 하면 override가 아님!
Console.WriteLine("Dog says WTF");
}
}
이렇게 new 키워드를 써버리면 override는 안되고 shadowing이 돼,
부모 클래스 타입으로 사용할 경우 여전히 부모의 메서드가 호출됩니다.
Abstract
abstract는 virtual보다 좀 더 강제성이 있는 편입니다. 자식 클래스에서 꼭 override 재정의해야합니다.
사용하려면, 클레스와 메서드 모두 abstract class / abstract void 어쩌구메서드 이어야 합니다.
안스턴트를 직접 만들 수 없으며, 상속을 통해서만 가능합니다.
(Unity용 C#으로 예제 코드를 만들어 봤어유)
public abstract class Enemy : MonoBehaviour
{
public abstract void Attack(); // abstract 자체는 아직 기능 구현 없음
}
public class Skeleton : Enemy
{
public override void Attack()
{
Debug.Log("Shoot an Arrow");
}
}
public class Zombie : Enemy
{
public override void Attack()
{
Debug.Log("zombie Punch!");
}
}
Enemy e1 = new Skeleton(); // ㅇㅇ, 굿 인스턴트 생성
e1.Attack(); // 출력 : Shoot an Arrow
Enemy e2 = new Enemy(); // ㄴㄴ! 추상 클래스는 직접 생성 불가해유
abstract class는 직접 생성 불가하오니, 틀만 제공하고 자식 클래스에서 맡기는 상속 전용이라 생각하면 편합니다.
그렇기 때문에, 자식 클래스에서 (종류별로) 무조건 다르게 기능해야 할 때 사용하게됩니다.
Interface
구현할 기능 전혀 없이 무엇이 있어야 할지만 선언합니다.
Class에서 interface를 implement(구현)하며, 그 안의 모든 멤버를 꼭 구현해야 합니다.
interface의 가장 큰 특징은, 하나의 클래스가 여러 인터페이스를 동시에 구현할 수 있다는 점입니다.
즉, 다중 상속이 가능하며, 여러 클래스에 공통적인 기능을 추가하기 위해 사용합니다.
interface ICry
{
void Cry(); // 구현 ㄴㄴ, 선언만해요
}
class Dog : ICry
{
public void Cry()
{
Console.WriteLine("barks, 멍멍!");
}
}
class Human : ICry
{
public void Cry()
{
Console.WriteLine("T.T, 엉엉");
}
}
ICry dog = new Dog();
dog.Cry(); // 출력 : barks, 멍멍!
다중 상속이기 때문에, 서로 관련 없는 클래스도 interface로 구현 가능하며,
이렇게 interface로 형성된 관계를 다형성이라고 합니다.
접근 제한자로 public 만 허용이 됩니다.
(C#8 이후부터는 default 구현이 가능해졌지만, 일반적으로는 구현 없이 선언만 작성하는 것이 관례라네용)
Unity에서 사용 예를 확인해보겠습니다.
using UnityEngine;
using UnityEngine.EventSystems;
public class ButtonClickHandler : MonoBehaviour, IPointerClickHandler
{
public void OnPointerClick(PointerEventData eventData)
{
Debug.Log("Button clicked!");
}
}
위와 같은 인터페이스 덕분에 Unity에서는 다양한 기능들이 컴포넌트 방식으로 확장되며 제공됩니다.
정리
3가지 특징 별로 표로 정리했습니다.
| virtual | abstract | interface | |
| 설명 | 기본 동작 제공 + 선택적 재정의 허용 | 틀은 제공하고, 구현은 자식에게 강제 | 어떤 기능이 필요하다는 규약(계약) 제공 |
| 기본 기능 구현 | 있음 (선택적 오버라이드) | 없음 (구현 불가) | 없음 (관례적으로 없음) |
| 오버라이드 여부 | 선택 | 필수 | 필수 |
| 클래스 인스턴스화 | 가능 | 불가능 | 인터페이스 자체는 인스턴스화 불가 |
| 상속/구현 대상 | 클래스만 | 클래스만 | 클래스 & 구조체 |
| 다중 상속/구현 | 불가능 | 불가능 | 가능 |
| 키워드 | virtual, override | abstract, override | interface, 구현 시 : 사용 |
| 기본 동작 정의 | 가능 | 불가능 | 불가능 (C#8부터 일부 가능 but ㄴ) |
| 유지보수 성격 | 확장 가능성 중시 | 구조 강제 중심 | 계약 기반 설계 중심 |
갑자기 헷갈려서 정리하게 되었습니다.
그럼 오늘도 좋은 하루 되시길 바라요~ *⋆꒰ঌ(⁎ᴗ͈ˬᴗ͈⁎)໒꒱⋆*
'- C# > C# is 도샵' 카테고리의 다른 글
| [C#] Lambda Expression 람다식 (4) | 2025.08.24 |
|---|---|
| [C#] HashSet<T> (1) | 2025.06.17 |
| [C#] System.Array & Copy (0) | 2025.05.12 |
| [C#] Generic, where (제네릭, 제약조건) Boxing & Unboxing (0) | 2025.05.06 |
| [C#] Async, Await | Asynchronous 비동기 프로그래밍 (0) | 2025.05.05 |