I'm FanJae.
[20260507] C# ( 추상 클래스, 인터페이스 ) 본문
1. 추상 클래스 (Abstract Class)
(1) 정의
- 추상 클래스는 클래스의 한 종류지만, 그 자체로 객체를 직접 생성할 수 없는 클래스다.
- 대신, 공통 기능이나 구조를 정의하고 자식 클래스가 구체적인 동작을 구현하도록 설계할 때 사용한다.
abstract class Animal
{
public abstract void MakeSound();
}
Animal animal = new Animal(); // Compile Error
- 위와 같이 추상 클래스는 자기 자신에 대한 객체를 생성하는 것이 불가능하다.
(2) 특징
- abstract 키워드를 이용하여 선언할 수 있다.
- new Animal() 처럼 직접 인스턴스를 만드는 행위가 불가능하다. (즉, 본인에 대한 객체를 직접 생성할 수 없다.)
- 자식 클래스가 공통으로 사용하는 필드, 프로퍼티, 일반 메서드를 가질 수 있다.
- 자식 클래스가 반드시 구현해야 하는 추상 메서드를 가질 수 있다.
abstract class Character
{
private string name; // 캐릭터의 이름
public void Move() // 자식 클래스가 사용할 이동하는 메서드
{
Console.WriteLine("이동한다");
}
public abstract void Attack(); // 상속 받은 자식 클래스가 반드시 구현해야 하는 추상 메서드
}
- 추상 메서드는 정의만 할 수 있다. 본문을 가질 수 없다.
- 추상 메서드는 자식 클래스에서 override 로 구현해야 한다.
class Warrior : Character
{
public override void Attack() // Character를 상속 받았기 때문에 Attack()을 반드시 구현해야 한다.
{
Console.WriteLine("강한 검으로 내려친다.");
}
}
(3) 추상 클래스가 필요한 이유
① 공통 기능을 중복 없이 관리하기 위해서
abstract class Animal
{
protected string Name { get; set; }
public void Eat()
{
Console.WriteLine($"{Name}이 음식을 아주 맛있게 먹습니다.");
}
}
- 부모 클래스인 Animal 에 구현되어 있는 Eat() 는 자식 클래스(Dog , Cat )에서 상속 받아 사용 가능하다.
② 자식 클래스가 반드시 가져야 하는 특정 기능을 강제하기 위해서
abstract class Animal
{
protected string Name { get; set; }
public void Eat()
{
Console.WriteLine($"{Name}이 음식을 아주 맛있게 먹습니다.");
}
public void MakeSound() // 어딘가 잘못된 울음 소리를 내는 메서드
{
Console.WriteLine("야옹? 멍멍?");
}
}
- 개와 고양이의 울음소리는 다르다. (개와 고양이뿐만 아니라 모든 동물의 울음소리가 그렇다.)
- 따라서 동물이라면 소리를 내는 방식은 다르지만, 울음을 내는 ‘기능’은 필요하다고 할 수 있다.
public abstract void MakeSound(); // 추상 메서드화 된 MakeSound
- 메서드를 추상 메서드로 선언하면, Dog 와 Cat 같은 자식 클래스에서 이 메서드를 반드시 구현해야 한다.
(1) Animal에 MakeSound()를 추상 메서드로 제공한 예제
abstract class Animal
{
protected string Name { get; set; }
public void Eat()
{
Console.WriteLine($"{Name}이 음식을 아주 맛있게 먹습니다.");
}
public abstract void MakeSound() // 어딘가 잘못된 울음 소리를 내는 메서드
{
Console.WriteLine("야옹? 멍멍?");
}
}
class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("멍멍!");
}
}
class Cat : Animal
{
public override void MakeSound()
{
Console.WriteLine("야옹!");
}
}
- 이와 같이 Cat과 Dog에서 MakeSound()를 자신에게 맞게 필수적으로 재정의하고 있다.
(4) 추상 클래스는 언제 사용하면 좋은가?
- 추상 클래스는 여러 클래스가 공통 기능을 공유하면서 일부 동작은 각 클래스마다 서로 다르게 구현해야 할 때 사용하면 유용하다.
Animal
- 공통 기능 : Eat()
- 강제 구현 기능 : MakeSound()
Character
- 공통 기능 : Move()
- 강제 구현 기능 : Attack()
- 즉, 추상 클래스는 공통 기능을 제공하면서 특정 기능에 대한 구현을 강제할 수 있다.
- 이를 이용하여, 객체의 일관성은 유지하면서 유연성을 확장할 수 있다는 이점이 생긴다.
(5) virtual 과 abstract 의 차이점
- 지난번 메서드 오버라이딩(Method Overriding)에 대해서 다룰때, virtual을 사용해 메서드를 재정의하였다.
- virtual 과 abstract 는 둘 다 자식 클래스에서 override 할 수 있다는 공통점이 있다.
- 하지만, 두 키워드의 역할은 조금 다른데, 아래와 같은 부분에서 차이가 존재한다.
① 부모 클래스에서 기본 구현을 제공하고 있는가?
abstract class Animal
{
public virtual void Eat() // 자식 클래스에서 재정의가 가능한 virtual 메서드
{
Console.WriteLine("동물이 음식을 먹습니다.");
}
public abstract void MakeSound(); // 자식 클래스에서 반드시 재정의해야 하는 abstract 메서드
}
(1) virtual
- Eat() 는 virtual 로 선언되어 있기 때문에, 기본 동작이 존재한다.
(2) abstract
- 이에 반면, MakeSound()는 abstract 로 선언되어 있기 때문에, 기본 동작이 없다.
② 자식 클래스가 재정의를 해야 하는가?
class Dog : Animal
{
public override void MakeSound() // 필수 재정의
{
Console.WriteLine("멍멍!");
}
}
class Cat : Animal
{
public override void MakeSound() // 필수 재정의
{
Console.WriteLine("야옹!");
}
public override void Eat() // 선택적 재정의
{
Console.WriteLine("고양이가 생선을 아주 맛있게 먹습니다.");
}
}
(1) virtual
- Eat() 는 virtual 로 선언되어 있기 때문에, 자식 클래스가 필요하면 재정의한다.
- 만약 재정의할 필요가 없으면, 부모 클래스의 구현체를 그대로 사용한다.
(2) abstract
- MakeSound() 는 abstract 로 선언되어 있기 때문에, 부모 클래스에 기본 동작이 없다.
-따라서 자식 클래스가 반드시 이를 재정의 해야한다.
(6 추상 클래스와 다형성
- 추상 클래스는 자기 자신으로 객체를 직접 생성할 수는 없지만, 타입으로 사용 가능하다.
- 즉, 자식 클래스의 객체를 부모 추상 클래스 타입의 변수에 담을 수 있다.
Animal[] animals =
{
new Dog("바우");
new Cat("나비");
}
foreach (Animal animal in animals)
{
animal.MakeSound();
}
- 위와 같은 형태로 부모 타입을 이용해 여러 자식 객체를 다룰 수 있는 것이 다형성이다.
① 가능한 이유
Animal dog = new Dog("바우");
- 위 코드에서 변수 dog 의 타입은 Animal 이다.
- 이 타입은 컴파일 타임에 결정되는 정적 타입이다.
- 하지만 실제 생성되는 객체는 Dog 객체이며, 이를 런타임 객체(실제 객체)라고 한다.
※ 즉, 컴파일 시점에는 Animal 타입으로 동작을 확인하고, 실행 시점에는 실제 객체인 Dog의 메서드가 호출되는 원리다. (이것에 대한 원리는 메서드 오버라이딩 파트에서 다뤘다.)
2. 인터페이스 (Interface)
(1) 정의
- 인터페이스는 클래스가 반드시 구현해야 하는 메서드의 규약을 정의한 것이다.
- 즉, 객체의 ‘어떤 행동을 수행할 수 있는가’를 정의하는 것이다.
interface IAttackable
{
void Attack();
}
- 일반적으로 인터페이스 이름의 시작은 알파벳 대문자 I를 앞에 넣어 구분한다. (규칙)
- 여기서 IAttackable() 이라는 것은 공격할 수 있는 객체다.
- 하지만, 실제 공격 방식은 인터페이스가 정의하지 않고, 이를 구현하는 클래스가 결정한다.
class Warrior : IAttackable
{
public void Attack()
{
Console.WriteLine("전사가 검으로 강하게 찌른다");
}
}
- 위와 같이 상속과 같은 방식으로 클래스 명 : 구현할 클래스 명 형태로 처리한다.
- 상속과 구분하기 위해서 인터페이스를 받아오는 경우, ‘구현한다’라고 칭한다.
(2) 특징
- 메서드, 속성, 이벤트 등의 규약을 정의 할 수 있다.
- 인터페이스를 구현한 클래스는 해당 멤버를 반드시 구현해야 한다.
- 인터페이스 자체로 객체를 직접 생성할 수 없다. (추상 클래스와 동일)
- 클래스는 여러 개의 인터페이스를 구현할 수 있다.
// 불가능 (클래스에 대한 다중 상속)
class Warrior : Character, Player
{
}
// 가능 (여러 개의 인터페이스 구현 가능)
class Warrior : IAttackable, IMovable
{
}
- 인터페이스의 타입으로 객체를 참조할 수 있다. (추상 클래스와 동일)
(3) 인터페이스가 필요한 이유
① 구체적인 클래스에 직접 의존하지 않도록 만들기 위해서
abstract class Character
{
public void Attack();
}
class Warrior : Character
class Mage : Character
class Archer : Character
- 위와 같이 캐릭터의 단위로 묶으면, 추상 클래스로 묶을 수 있다.
- 설계의 관점에 따라서 캐릭터가 아닌 물체도 공격할 수 있다.
class Trap : Character
- 함정도 공격할 수 있다. 하지만, 함정이 Character 에 분류되면 개념이 모호해진다.
(1) 인터페이스를 이용한 개선 (같은 기능을 묶어준다.)
interface IAttackable
{
void Attack();
}
- 위와 같이 사용할 경우 캐릭터가 아니어도 공격 가능이라는 인터페이스로 묶을 수 있다.
class Warrior : IAttackable { }
class Mage : IAttackable{ }
class Archer : IAttackable { }
class Trap : IAttackable { }
class Monster : IAttackable { }
- 이처럼 인터페이스는 코드가 구체적인 클래스에 직접 의존하지 않도록 만들어 줄 수 있다.
(4) 인터페이스는 언제 사용하면 좋은가?
① 객체의 종류보다 행동이 중요한 경우
interface IAttackable
{
void Attack();
}
interface IDamageable
{
void TakeDamage(int damage);
}
interface IMovable()
{
void Move();
}
- 인터페이스는 객체의 행동을 정의하는데 초점이 맞춰져 있다.
- 즉, 이 객체가 ‘무엇을 할 수 있는가?’를 정의하는 것이다.
class Warrior : IAttackable, IDamageable
{
public void Attack()
{
Console.WriteLine("전사가 검으로 강하게 찌른다.");
}
public void TakeDamage(int damage)
{
Console.WriteLine($"전사가 {damage} 피해를 받았다.");
}
}
class Trap : IMovable
{
public void Move()
{
Console.WriteLine("이동형 함정 트랩이 이동했다.");
}
}
- Warrior 와 Trap 은 서로 다른 클래스다. 하지만, Interface를 통해 같은 방식으로 다룰 수 있다.
- 또, 이들의 특정 기능 구현을 강제하지만, 특정 클래스에 의존하지 않고 있다.
- 즉, ~이다. (is-a 관계)가 아닌 ~할 수 있는 존재다에 초점을 맞출 때 적합하다.
※ 특정 기능만 강제하고 싶은 경우 인터페이스를 사용한다. (~할 수 있는 존재다.)
※ 공통된 기능을 제공하면서 일부 기능만 강제하고 싶은 경우 추상 클래스를 사용한다. (~이다.)
(5) 인터페이스와 다형성
- 인터페이스도 자신을 직접 객체로 생성할 수 없지만, 타입으로 사용할 수 있다.
interface IAttackable
{
void Attack();
}
class Warrior : IAttackable
{
public void Attack()
{
Console.WriteLine("기사가 검으로 공격한다.");
}
}
class Archer : IAttackable
{
public void Attack()
{
Console.WriteLine("궁수가 화살을 발사한다.");
}
}
class Mage : IAttackable
{
public void Attack()
{
Console.WriteLine("마법사가 마법을 사용한다.");
}
}
- 위 클래스를 사용할 때는 모두 같은 방식으로 호출할 수 있다.
IAttackable[] attackers =
{
new Warrior(),
new Archer(),
new Mage()
};
foreach (IAttackable attacker in attackers)
{
attacker.Attack();
}
- Attack() 을 호출 하는 것은 동일하지만 실행 결과는 실제 객체에 따라 달라진다.
- 핵심은 컴파일 타임 타입과 런타임 객체 타입의 차이에 의해 발생한다.
(6) 인터페이스 설계 시 주의할 점
- 인터페이스는 너무 많은 기능을 한 번에 넣으면 안된다.
interface ICharacter
{
void Attack();
void Jump();
void Fly();
void Swim();
void Trade();
void UseMagic();
}
- 이 인터페이스를 만약 슬라임이 사용한다고 하면 아래와 같은 문제가 발생한다.
class Slime : ICharacter
{
public void Attack() { }
public void Jump() { }
public void Fly() { } // 슬라임에게 필요 없는 기능
public void Swim() { } // 필요 없을 수 있음
public void Trade() { } // 필요 없음
public void UseMagic() { } // 필요 없음
}
- 인터페이스를 구현하면 모든 멤버를 구현해야 한다.
- 따라서 슬라임에게 필요 없는 기능까지 억지로 구현하게 된다.
- 위와 같은 상황을 객체 지향 5대 원칙 SOLID 원칙 중 ISP에 위배 된다고 한다.
① ISP (Interface Sergregation Principle, 인터페이스 분리 원칙)
- 객체는 자신이 사용하는 메서드에만 의존해야 한다.
- 객체는 사용하지 않는 메서드를 의존해서는 안된다.
※ 정리하면, 인터페이스는 사용하는 객체를 기준으로 잘게 분리해야 한다.
interface IAttackable
{
void Attack();
}
interface IFlyable
{
void Fly();
}
interface ISwimmable
{
void Swim();
}
interface ITradable
{
void Trade();
}
class Slime : IAttackable
{
public void Attack()
{
Console.WriteLine("슬라임이 몸통 박치기를 한다.");
}
}
class Dragon : IAttackable, IFlyable
{
public void Attack()
{
Console.WriteLine("드래곤이 브레스를 사용한다.");
}
public void Fly()
{
Console.WriteLine("드래곤이 날아오른다.");
}
- 위와 같이 인터페이스를 쪼개면, 필요한 기능한 선택해서 구현할 수 있다.
- 물론, 인터페이스를 너무 잘게 쪼개는 것도 좋지 않다.
- 따라서, 인터페이스는 하나의 명확한 역할 단위로 나누는 것이 중요하다
'Unity > Unity 초격차캠프' 카테고리의 다른 글
| [20260507] C# ( namespace ) (0) | 2026.05.07 |
|---|---|
| [20260507] C# ( 추상 클래스 & 인터페이스의 차이점 ) (0) | 2026.05.07 |
| [20260506] C# ( 오버로딩 & 오버라이딩 차이점 ) (0) | 2026.05.06 |
| [20260506] C# (캡슐화, 상속, 다형성) (0) | 2026.05.06 |
| [20260504] C# (프로퍼티, 생성자, 메서드 II) (0) | 2026.05.04 |