Notice
Recent Posts
Recent Comments
Link
«   2026/05   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
Archives
Today
Total
관리 메뉴

I'm FanJae.

[20260507] C# ( 추상 클래스, 인터페이스 ) 본문

Unity/Unity 초격차캠프

[20260507] C# ( 추상 클래스, 인터페이스 )

FanJae 2026. 5. 7. 18:40

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("드래곤이 날아오른다.");
    }

- 위와 같이 인터페이스를 쪼개면, 필요한 기능한 선택해서 구현할 수 있다.

- 물론, 인터페이스를 너무 잘게 쪼개는 것도 좋지 않다.

- 따라서, 인터페이스는 하나의 명확한 역할 단위로 나누는 것이 중요하다

 
 
Comments