C# 중급 문법 (4) : 추상 클래스와 인터페이스

안녕하세요! 객체 지향 프로그래밍의 핵심 원칙인 상속다형성에 대해 알아보면서, '부모 클래스 타입으로 자식 객체를 다룬다'는 개념의 중요성을 깨달으셨을 겁니다. 이번에는 이 다형성을 더욱 강력하게 만들고, 시스템의 유연성과 확장성을 극대화하는 두 가지 중요한 도구인 **추상 클래스(Abstract Class)**와 **인터페이스(Interface)**에 대해 자세히 살펴보겠습니다. 이 둘은 공통적으로 '추상화'를 목표로 하지만, 그 방식과 용도가 다릅니다.


1. 추상 클래스(Abstract Class): 미완성 설계도 

추상 클래스는 이름 그대로 완전하지 않은, 추상적인 클래스를 의미합니다. 일반 클래스처럼 속성과 메서드를 가질 수 있지만, 다음과 같은 중요한 특징이 있습니다.

  • 인스턴스화 불가: 추상 클래스는 그 자체로는 객체를 생성할 수 없습니다. (new AbstractClass() 불가능) 마치 설계도인데 중요한 부품이 빠져있어서 실제 제품을 만들 수 없는 것과 같습니다.
  • 추상 메서드 포함 가능: abstract 키워드를 사용하여 **추상 메서드(Abstract Method)**를 가질 수 있습니다.
    • 추상 메서드선언만 있고 구현(몸통)이 없는 메서드입니다. { } 부분이 없습니다.
    • 추상 메서드는 반드시 자식 클래스에서 override 키워드를 사용하여 구현해야 합니다.
    • 추상 메서드를 하나라도 가지고 있는 클래스는 반드시 abstract 클래스로 선언되어야 합니다.
  • 일반 메서드 및 속성 포함 가능: 추상 클래스는 추상 메서드 외에 일반(구현된) 메서드, 속성, 생성자, 필드 등을 가질 수 있습니다.
  • 상속 전용: 추상 클래스는 오직 다른 클래스에게 상속(inheritance)될 목적으로만 사용됩니다.

추상 클래스 사용 목적:

  • 공통 기능 정의 및 강제 구현: 여러 자식 클래스들이 공유하는 공통적인 속성과 구현된 메서드를 제공하면서도, *반드시 구현해야 할 특정 행동(추상 메서드)*을 강제할 때 사용합니다.
  • IS-A 관계의 계층 구조: 'A는 B이다'라는 논리적 관계를 명확히 하면서, 상위 개념이 하위 개념들의 공통적인 특성과 행동을 정의하는 데 적합합니다.

예시: Shape 추상 클래스

// 추상 클래스 Shape 정의
public abstract class Shape
{
    public string Name { get; set; } // 일반 속성

    public Shape(string name) // 생성자도 가질 수 있음
    {
        Name = name;
    }

    // 일반(구현된) 메서드
    public void DisplayInfo()
    {
        Console.WriteLine($"이 도형은 {Name}입니다.");
    }

    // 추상 메서드: 구현부가 없고, 자식 클래스에서 반드시 재정의해야 함
    public abstract double GetArea(); // 면적 계산은 각 도형마다 다름
    public abstract double GetPerimeter(); // 둘레 계산도 각 도형마다 다름
}

// Circle 클래스: Shape 추상 클래스 상속
public class Circle : Shape
{
    public double Radius { get; set; }

    public Circle(string name, double radius) : base(name)
    {
        Radius = radius;
    }

    // 추상 메서드 GetArea() 구현 (재정의)
    public override double GetArea()
    {
        return Math.PI * Radius * Radius;
    }

    // 추상 메서드 GetPerimeter() 구현 (재정의)
    public override double GetPerimeter()
    {
        return 2 * Math.PI * Radius;
    }
}

// Rectangle 클래스: Shape 추상 클래스 상속
public class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }

    public Rectangle(string name, double width, double height) : base(name)
    {
        Width = width;
        Height = height;
    }

    // 추상 메서드 GetArea() 구현 (재정의)
    public override double GetArea()
    {
        return Width * Height;
    }

    // 추상 메서드 GetPerimeter() 구현 (재정의)
    public override double GetPerimeter()
    {
        return 2 * (Width + Height);
    }
}

추상 클래스 사용 예시:

public class Program
{
    public static void Main(string[] args)
    {
        // Shape shape = new Shape("도형"); // 에러! 추상 클래스는 인스턴스화 불가

        Shape circle = new Circle("원", 5.0);
        Shape rectangle = new Rectangle("직사각형", 4.0, 6.0);

        // 다형성 활용: 부모 타입(Shape)으로 자식 객체를 다룸
        List<Shape> shapes = new List<Shape> { circle, rectangle };

        foreach (Shape s in shapes)
        {
            s.DisplayInfo(); // 일반 메서드 호출
            Console.WriteLine($"{s.Name}의 면적: {s.GetArea():F2}"); // 재정의된 추상 메서드 호출
            Console.WriteLine($"{s.Name}의 둘레: {s.GetPerimeter():F2}"); // 재정의된 추상 메서드 호출
            Console.WriteLine("--------------------");
        }
        /*
        출력:
        이 도형은 원입니다.
        원의 면적: 78.54
        원의 둘레: 31.42
        --------------------
        이 도형은 직사각형입니다.
        직사각형의 면적: 24.00
        직사각형의 둘레: 20.00
        --------------------
        */
    }
}

Shape 추상 클래스는 모든 도형이 공통적으로 가질 수 있는 Name 속성과 DisplayInfo() 메서드를 제공합니다. 하지만 도형마다 면적과 둘레를 계산하는 방식이 다르므로, GetArea()와 GetPerimeter()는 추상 메서드로 선언하여 자식 클래스들이 반드시 자신에게 맞는 방식으로 구현하도록 강제합니다.


2. 인터페이스(Interface): '무엇을 할 수 있는가'에 대한 계약서 

인터페이스클래스가 특정 기능을 제공할 것임을 명시하는 '계약서' 또는 '규약'과 같은 역할을 합니다. 추상 클래스보다 더 높은 수준의 추상화를 제공하며, 다음과 같은 특징이 있습니다.

  • 구현 없음: 인터페이스는 메서드의 선언(시그니처)만 포함하며, 어떠한 구현(몸통)도 가지지 않습니다. 속성도 get; set;처럼 선언만 가능하며, 필드를 가질 수 없습니다. (C# 8.0부터는 default 구현 가능하지만, 기본적인 개념은 '구현 없음'입니다.)
  • 다중 구현 가능: 클래스는 하나의 추상 클래스만 상속받을 수 있지만, **여러 개의 인터페이스를 구현(Implement)**할 수 있습니다.
  • IS-A 관계 (능력): 'A는 B이다'보다는 'A는 B를 할 수 있다' (HAS-A-CAPABILITY)는 관계를 표현하는 데 적합합니다. 예를 들어, '비행기는 날 수 있다(예시 인터페이스 이름 : IFlyable)', '강아지는 짖을 수 있다(예시 인터페이스 이름 : IBarkable)'.
  • 접근 제한자: 인터페이스의 모든 멤버는 기본적으로 public이며, 명시적으로 public을 붙이지 않습니다.

인터페이스 사용 목적:

  • 역할 정의 및 강제 구현: 특정 역할을 수행하는 데 필요한 행동들을 정의하고, 이 인터페이스를 구현하는 클래스들이 해당 행동들을 반드시 구현하도록 강제할 때 사용합니다.
  • 느슨한 결합(Loose Coupling) 촉진: 클래스들 간의 의존성을 줄여 코드를 더 유연하고 확장 가능하게 만듭니다. 특정 구현에 묶이지 않고, 인터페이스라는 추상적인 계약에 의존하게 합니다.
  • 다중 상속의 대안: C#의 단일 상속 제한을 극복하고, 여러 상위 타입의 '능력'을 한 클래스에 부여할 수 있도록 합니다.

예시: IPlayable 인터페이스

// 인터페이스 IPlayable 정의 (접근 제한자 public은 붙이지 않음)
public interface IPlayable
{
    // 메서드 선언 (구현 없음)
    void Play();
    void Pause();
    void Stop();

    // 속성 선언 (구현 없음)
    string CurrentTrack { get; set; }
}

// MusicPlayer 클래스: IPlayable 인터페이스 구현
public class MusicPlayer : IPlayable
{
    public string CurrentTrack { get; set; }

    public MusicPlayer(string track)
    {
        CurrentTrack = track;
    }

    public void Play()
    {
        Console.WriteLine($"{CurrentTrack}을(를) 재생합니다.");
    }

    public void Pause()
    {
        Console.WriteLine($"{CurrentTrack}을(를) 일시 정지합니다.");
    }

    public void Stop()
    {
        Console.WriteLine($"{CurrentTrack}을(를) 정지합니다.");
    }
}

// VideoPlayer 클래스: IPlayable 인터페이스 구현
public class VideoPlayer : IPlayable
{
    public string CurrentTrack { get; set; } // 비디오 파일 이름 등

    public VideoPlayer(string videoName)
    {
        CurrentTrack = videoName;
    }

    public void Play()
    {
        Console.WriteLine($"비디오 {CurrentTrack}을(를) 재생합니다.");
    }

    public void Pause()
    {
        Console.WriteLine($"비디오 {CurrentTrack}을(를) 일시 정지합니다.");
    }

    public void Stop()
    {
        Console.WriteLine($"비디오 {CurrentTrack}을(를) 정지합니다.");
    }
}

인터페이스 사용 예시: 

public class Program
{
    public static void Main(string[] args)
    {
        // 인터페이스 타입으로 객체를 다룸 (다형성)
        IPlayable player1 = new MusicPlayer("봄날의 곰");
        IPlayable player2 = new VideoPlayer("바다 탐험");

        // 인터페이스를 구현하는 모든 객체는 Play, Pause, Stop을 호출할 수 있음
        player1.Play();
        player1.Pause();
        Console.WriteLine($"현재 재생 중: {player1.CurrentTrack}");
        player1.Stop();
        Console.WriteLine("--------------------");

        player2.Play();
        player2.Stop();
        Console.WriteLine($"현재 재생 중: {player2.CurrentTrack}");
        player2.Pause(); // 비디오 플레이어의 일시 정지

        Console.WriteLine("\n--- 여러 플레이어를 한 번에 제어 ---");
        List<IPlayable> playableItems = new List<IPlayable> { player1, player2 };

        foreach (IPlayable item in playableItems)
        {
            item.Play();
        }
        // 출력:
        // 봄날의 곰을(를) 재생합니다.
        // 비디오 바다 탐험을(를) 재생합니다.
    }
}

IPlayable 인터페이스는 '재생 가능한' 객체가 가져야 할 공통적인 행동(Play(), Pause(), Stop())을 정의합니다. MusicPlayer와 VideoPlayer는 서로 다른 종류의 객체이지만, IPlayable을 구현함으로써 모두 '재생 가능한' 객체로 취급될 수 있습니다. 이를 통해 서로 다른 구현체를 동일한 인터페이스로 다루는 강력한 다형성을 얻을 수 있습니다.


3. 추상 클래스와 인터페이스 비교 

특징 추상 클래스(Abstract Class) 인터페이스(Interface)
인스턴스화 불가능 불가능
구현 포함 가능 (일반 메서드/속성) + 추상 메서드 기본적으로 불가능 (선언만 가능), C# 8.0+ 기본 구현 가능
필드 가질 수 있음 가질 수 없음
생성자 가질 수 있음 가질 수 없음
상속/구현 단일 상속만 가능 (클래스 -> 추상 클래스) 다중 구현 가능 (클래스 -> 여러 인터페이스)
접근 제한자 일반 클래스와 동일 (public, private, protected 등) 모든 멤버는 기본적으로 public
목적 IS-A 관계 (종류): 공통 구현과 강제 구현의 균형 HAS-A-CAPABILITY 관계 (능력): 역할 정의 및 강제 구현
예시 Animal, Shape, Vehicle IComparable, IEnumerable, IDisposable, IPlayable

언제 무엇을 사용할까?

  • 추상 클래스:
    • 여러 클래스들이 공통된 기반(base)을 공유하며, 일부 기능은 공통적으로 구현하고 일부는 각자 다르게 구현해야 할 때 사용합니다. (예: Animal의 Sleep()은 공통, MakeSound()는 각자 다르게)
    • 'A는 B이다'라는 강한 계층적 관계가 있을 때 적합합니다.
    • 버전 변경 시 유연성이 필요한 경우 (새로운 일반 메서드를 추가해도 기존 구현체에 영향 X)
  • 인터페이스:
    • 클래스들이 서로 다른 상속 계층에 속하더라도 특정 기능을 반드시 수행해야 한다고 규정하고 싶을 때 사용합니다.
    • 'A는 B를 할 수 있다'라는 능력을 부여하고 싶을 때 적합합니다.
    • 다중 상속의 대안으로 여러 '능력'을 한 클래스에 부여해야 할 때 사용합니다.
    • 느슨한 결합을 통해 시스템의 유연성과 확장성을 극대화할 때 중요합니다.

마무리하며: 추상 클래스와 인터페이스로 완성하는 추상화! ✨

추상 클래스인터페이스는 C#에서 추상화를 구현하고 다형성을 활용하는 데 필수적인 도구입니다. 둘 다 코드의 유연성과 확장성을 높이고, 특정 기능을 강제하여 견고한 시스템을 만드는 데 기여하지만, 그 접근 방식과 목적에 차이가 있습니다.

  • 추상 클래스는 공통된 특징을 가진 '그룹'에 대한 미완성 설계도이며,
  • 인터페이스는 다양한 객체들이 공통적으로 가질 수 있는 '능력'에 대한 계약서입니다.

이 두 가지를 적절하게 사용하여 여러분의 C# 프로그램을 더욱 객체 지향적으로, 그리고 더욱 강력하게 만들어 보세요!

추상 클래스와 인터페이스의 특정 구현 방식이나 더 깊은 개념에 대해 궁금한 점이 있으신가요? 언제든지 질문해주세요!