Menu

C# 추상 클래스: 추상 메서드, 예제, 인터페이스와 비교

C#에서 추상 클래스란 무엇인가: 인스턴스화할 수 없고, 파생 클래스가 반드시 구현해야 하는 추상 멤버를 선언할 수 있는 기본 클래스입니다. 추상 메서드와 속성, 생성자, 템플릿 메서드 패턴, 그리고 대신 인터페이스를 골라야 할 때를 다룹니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

추상 클래스는 상속되기 위해서만 존재하는 클래스입니다. 인스턴스화할 수 없고, 추상 멤버를 선언할 수 있습니다. 모든 구체 파생 클래스가 구현해야 하는, 본문 없는 메서드와 속성입니다. 그 밖의 모든 면에서는 평범한 클래스입니다. 필드, 생성자, 파생 클래스가 상속하는 완전히 구현된 메서드를 가질 수 있습니다.

추상 클래스 선언하기

클래스를 abstract로 표시하고, 파생 클래스가 제공해야 하는 각 멤버를 abstract로 표시한 뒤 본문 대신 세미콜론으로 끝내세요:

출력:

Circle: area 7.07, perimeter 9.42
Rectangle: area 10.00, perimeter 13.00

Describe는 기본 클래스에 한 번 작성되며, Shape에는 전혀 존재하지 않는 Area와 Perimeter를 호출합니다. 이것이 추상 멤버의 핵심입니다. 기본 클래스는 이에 의존할 수 있고, 컴파일러는 모든 구체 도형이 이를 제공함을 보장합니다. 하나를 빠뜨린 파생 클래스는 컴파일되지 않습니다:

class Triangle : Shape
{
    public Triangle() : base("Triangle") { }
    public override double Area() => 6;
}
// error CS0534: 'Triangle' does not implement inherited abstract member 'Shape.Perimeter()'

해결책은 Perimeter를 구현하거나, Triangle이 다른 클래스들을 위한 중간 기본 클래스라면 Triangle 자체를 abstract로 표시하는 것입니다.

추상 멤버의 규칙

  • 추상 멤버는 본문이 없고 암시적으로 가상입니다. 파생 클래스는 override로 구현합니다.
  • 추상 클래스만 추상 멤버를 담을 수 있습니다. 일반 클래스에 두면 컴파일 오류입니다.
  • 추상 멤버는 private일 수 없고(아무도 재정의할 수 없으니까요), 클래스 안에서 static일 수 없습니다.
  • 메서드, 속성, 인덱서, 이벤트는 추상일 수 있습니다. 필드와 생성자는 안 됩니다.
  • 추상 클래스가 추상 멤버를 반드시 담아야 하는 것은 아닙니다. 클래스를 abstract로만 표시하면 인스턴스화만 막으며, 하위 클래스를 통해서만 의미가 있는 기본 클래스에 유용합니다.

추상 속성은 파생 클래스가 제공해야 하는 접근자를 선언합니다:

abstract class Plan
{
    public abstract decimal MonthlyPrice { get; }
    public abstract string Name { get; }
}

class ProPlan : Plan
{
    public override decimal MonthlyPrice => 12.99m;
    public override string Name => "Pro";
}

추상 클래스의 생성자

추상 클래스는 생성자를 가질 수 있습니다. new로 호출할 수는 없지만 모든 파생 생성자가 : base(...)를 통해 하나를 실행하므로, 공유 상태를 초기화하고 검증하는 곳입니다. protected로 선언하세요. "파생 클래스만 이것을 쓸 수 있다"는 뜻을 정확히 말해 줍니다.

템플릿 메서드 패턴

추상 클래스를 작성하는 가장 흔한 이유는 알고리즘의 순서를 기본 클래스에 고정하고 파생 클래스가 개별 단계를 채우게 하는 것입니다. 단계를 실행하는 기본 메서드는 가상이 아니므로, 어떤 하위 클래스도 순서를 바꾸거나 건너뛸 수 없습니다.

출력:

item,qty,price
Coffee beans,2,11.50
Filter papers,1,3.20

| Item | Qty | Price |
| --- | --- | --- |
| Coffee beans | 2 | 11.50 |
| Filter papers | 1 | 3.20 |

Total: 26.20

각 행은 이름 붙은 튜플이고, 출력은 StringBuilder로 만듭니다. 조합에 주목하세요. Header와 Row는 모든 형식이 정해야 하므로 abstract이고, Footer는 대부분의 형식에서 필요 없으므로 빈 기본값을 가진 virtual입니다. 단계들은 protected이므로 외부 코드는 Export만 호출할 수 있습니다. 프레임워크 클래스도 같은 아이디어를 씁니다. Stream과 TextWriter는 추상 기본 클래스이며, 많은 편의 메서드가 파생 클래스가 제공하는 몇 개의 멤버(스트림은 Read와 Write, 텍스트 작성기는 Write(char)) 위에 만들어져 있습니다.

추상 클래스와 인터페이스

둘 다 구체 클래스를 모르고도 타입을 대상으로 코드를 작성하게 해 줍니다. 무엇을 담을 수 있는지와 클래스가 몇 개를 가질 수 있는지가 다릅니다:

추상 클래스인터페이스
클래스가 상속 또는 구현할 수 있는 수하나여러 개
인스턴스 필드(상태)가능불가
생성자가능불가
본문이 있는 메서드가능C# 8부터 가능(기본 인터페이스 메서드)
정적 멤버가능C# 8부터 가능, 정적 추상 멤버는 C# 11부터
멤버의 접근 한정자모두기본은 public, C# 8부터 다른 것도 허용
구조체가 쓸 수 있음불가가능
나중에 멤버 추가추상이 아니면 안전기본 본문이 없으면 구현체가 깨짐

기본 인터페이스 메서드(C# 8)가 격차를 줄였지만 핵심적인 차이는 그대로입니다:

public interface ILogger
{
    void Write(string message);
    void Error(string message) => Write("ERROR: " + message);   // default implementation: C# 8, .NET Core 3.0+
}

기본 메서드는 인터페이스에 인스턴스 필드가 없으므로 여전히 이를 건드릴 수 없고, 인터페이스의 다른 멤버만 호출할 수 있습니다.

실용적인 선택 방법:

  • 서로 관련 없는 타입들이 가질 수 있는 능력을 설명할 때(IComparable<T>, IDisposable, IShippingProvider), 구조체도 참여할 수 있어야 할 때, 클래스에 그런 역할이 여러 개 필요할 때는 인터페이스. 대부분의 의존성 주입 코드가 이런 이유로 인터페이스를 대상으로 작성됩니다.
  • 위의 내보내기 클래스들처럼 파생 타입들이 진짜로 한 가지의 변형이고 상태나 고정된 알고리즘을 공유할 때는 추상 클래스.
  • 둘 다 흔합니다. 호출하는 쪽이 의존할 인터페이스와, 이를 구현해 구현자가 공유 부분을 다시 쓰지 않게 해 주는 추상 기본 클래스입니다. 인터페이스 쪽은 인터페이스 페이지에서 자세히 다룹니다.

흔한 실수

  • 기본 클래스를 인스턴스화하려 하기. new Shape(...)는 CS0144입니다. 파생 클래스를 인스턴스화하세요.
  • 구현에 override를 잊기. 파생 클래스에 public double Area()라고 쓰면 추상 멤버를 구현하지 않습니다. 컴파일러는 빠진 구현에 대해 CS0534를, 새 메서드가 상속된 메서드를 숨긴다는 경고를 함께 보고합니다.
  • 추상 클래스의 public 생성자. 해롭지는 않지만 오해를 부릅니다. protected를 쓰세요.
  • 공유 코드가 없는 추상 클래스. 기본 클래스에 추상 멤버만 있고 상태가 없다면, 인터페이스가 같은 것을 말하면서 단 하나뿐인 기본 클래스 자리를 쓰지 않습니다.
  • 기본 생성자에서 추상 멤버 호출하기. 파생 구현이 파생 생성자의 본문보다 먼저 실행되므로, 그 본문이 대입하는 필드는 여전히 0이나 null입니다.

자주 묻는 질문

C#에서 추상 클래스란 무엇인가요?

abstract로 표시한 클래스는 기본 클래스로만 쓰기 위한 것입니다. new로 인스턴스를 만들 수 없습니다(CS0144 오류). 일반 필드, 생성자, 메서드에 더해, 본문이 없고 모든 비추상 파생 클래스가 구현해야 하는 abstract 멤버를 담을 수 있습니다.

C#에서 추상 클래스와 인터페이스의 차이는 무엇인가요?

클래스는 추상 클래스 하나만 상속할 수 있지만 인터페이스는 여러 개 구현할 수 있습니다. 추상 클래스는 상태(인스턴스 필드), 생성자, 모든 접근 수준의 멤버를 가질 수 있으며, 공유 구현을 두기에 자연스러운 곳입니다. 인터페이스는 인스턴스 상태 없이 능력을 설명합니다. C# 8부터 기본 메서드 본문을 가질 수 있지만, 여전히 인스턴스 필드나 생성자는 가질 수 없습니다.

C#에서 abstract 메서드와 virtual 메서드의 차이는 무엇인가요?

virtual 메서드는 파생 클래스가 재정의해도 되는 본문을 가집니다. abstract 메서드는 본문이 없으며 파생 클래스가 반드시 재정의해야 하고, 그렇지 않으면 파생 클래스도 추상이 됩니다(구체 클래스라면 CS0534 오류). 추상 메서드는 추상 클래스 안에만 둘 수 있습니다.

C#에서 추상 클래스가 생성자를 가질 수 있나요?

네. 파생 클래스가 생성될 때 : base(...)를 통해 실행되며, 보통 공유 필드를 초기화합니다. protected로 선언하세요. 추상 클래스의 public 생성자는 호출하는 쪽이 쓸 수 있을 것처럼 보이지만 실제로는 쓸 수 없습니다.

추상 클래스의 인스턴스를 만들 수 없는 이유는 무엇인가요?

구현이 없는 추상 멤버가 있을 수 있기 때문입니다. new Shape().Area()를 호출하면 실행할 코드가 없습니다. 컴파일러는 CS0144("Cannot create an instance of the abstract type or interface")를 보고합니다. 대신 구체 파생 클래스의 인스턴스를 만들고, 원하면 추상 타입의 변수에 담으세요: Shape s = new Circle(2);.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기