C# 추상 그리고 인터페이스

2025. 2. 14. 19:35개발/C#

이 글은 박상현님의 '이것이 C#이다 개정판'을 참고하여 공부한 내용입니다.


처음 추상 클래스를 공부했을때, 저는 피카소의 우는 여인을 떠올렸습니다. 추상이란 단어를 가장 먼저 접한 것이 미술이라 그랬던 것 같습니다. 그래서 객체지향의 추상 클래스를 이해하는데에는 더 어려움이 있었습니다. 제가 생각하기에 개발의 추상은 객체를 이해하기 위해 그 객체가 가지고 있는 여러 특성중 한 부분을 클래스로 나타낸 것이라 생각하고 있습니다. 

 

abstract

abstract class Animal
{
    public abstract void MakeSound();

    public void Sleep()
    {
        Console.WriteLine("Sleeping...");
    }
}

class Dog : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine("Bark! Bark!");
    }
}

추상 클래스는 class 앞에 abstract 키워드를 선언하여 만들 수 있습니다. 일반 클래스와 거의 비슷한 모습을 하고 있지만 추상 멤버를 가질 수 있다는 차이가 있습니다. 추상 멤버도 동일하게 abstract 키워드를 사용하여 나타낼 수 있으며, C++ 순수 가상 메서드와 비슷하게 구현부를 가질 수 없으며, 파생 클래스에서는 이 멤버를 반드시 오버라이드 해야합니다. 하지만 추상 클래스 안의 모든 멤버가 추상 멤버인 것은 아니며, Sleep의 경우 일반 클래스 처럼 사용할 수 있습니다.

Animal myDog = new Dog();
myDog.MakeSound();  // print: Bark! Bark!
myDog.Sleep();  // print: Sleeping...

위 예시는 정상적으로 실행되어 Animal이 일반 클래스처럼 보이기도 합니다. 하지만 객체로 할당할 경우 컴파일러는 오류를 발생시켜 추상 클래스의 인스턴스화를 금지 시킵니다.

Animal animal = new Animal(); 

CS0144 Cannot create an instance of the abstract type or interface 'Animal'

물론 프로퍼티도 추상화가 가능합니다.

abstract class Shape
{
    public abstract double Area { get; }
}

class Circle : Shape
{
    private double radius;

    public Circle(double r)
    {
        radius = r;
    }

    public override double Area
    {
        get { return Math.PI * radius * radius; }
    }
}

 

interface

인터페이스는 정의를 뜻합니다. 추상 클래스와 마찬가지로 객체로 생성할 수 없습니다. 하지만 추상 클래스와 다르게 default 메서드를 제외한 인터페이스 안의 모든 멤버는 구현부를 가지면 안됩니다. 즉 프로토타입의 형태로 선언할 수 있으며, 파생 클래스들은 모든 멤버를 구현해야합니다. 그리고 인터페이스 내의 모든 멤버는 한정자를 사용하지 않고 public으로 취급됩니다.

interface IAnimal
{
    void MakeSound();
    void Sleep();
}

class Dog : IAnimal
{
    public void MakeSound()
    {
        Console.WriteLine("Bark! Bark!");
    }
    
    public void Sleep()
    {
        Console.WriteLine("Sleeping...");
    }
}

인터페이스는 class가 아닌 interface 키워드를 사용하여 선언합니다. 파생 클래스는 상속받은 모든 인터페이스의 멤버를 구현해야 하며, 구현이 없는 경우 컴파일러는 오류를 발생시켜 인터페이스의 규칙을 따르도록 합니다. 그리고 인터페이스의 가장 큰 특징은 다중상속이 가능하는 점입니다. C++에서는 추상이든 일반이든 상관없이 다중 상속이 가능했지만, C#에서는 추상과 일반 클래스는 하나만 상속 받을 수 있습니다. 이것은 죽음의 다이아몬드라는 상속 문제를 방지하기 위함입니다. 

interface IAnimal
{
    void MakeSound();
}

interface IPet
{
    void Play();
}

class Dog : IAnimal, IPet
{
    public void MakeSound()
    {
        Console.WriteLine("Bark! Bark!");
    }

    public void Play()
    {
        Console.WriteLine("Playing with a ball.");
    }
}

위와 같이 다중상속이 가능하며, 당연하게도 상속받은 인터페이스의 모든 멤버를 구현해야 합니다. 다중상속은 코드의 유지보수에서 큰 장점을 발휘합니다.

abstract class Animal
{
    public abstract void MakeSound();
}

class Dog : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine("Bark! Bark!");
    }
}

class Lion : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine("Roar! Roar!");
    }
}

만약 위 예시와 같이 Animal 클래스로 파생 클래스를 관리하고 있는 상황에서 애완동물로 취급할 수 있는 클래스에만 Play를 추가해야한다고 생각해봅시다. 그러면 Dog 클래스에 Play 메서드를 새로 작성하는 방법을 떠올릴 것 입니다. 하지만 수정해야 할 클래스가 많아진다면 이 방법은 유지보수하기 굉장히 힘들것 입니다. 협업중이라면 다른 개발자와 따로 애완동물 리스트를 작성해서 공유하며, 애완동물의 멤버가 추가 될때마다 일일히 확인하여 코드를 수정해야 할 것입니다.

interface IAnimal
{
    void MakeSound();
}

interface IPet
{
    void Play();
}

class Dog : IAnimal, IPet
{
    public void MakeSound()
    {
        Console.WriteLine("Bark! Bark!");
    }

    public void Play()
    {
        Console.WriteLine("Playing with a ball.");
    }
}

class Lion : IAnimal
{
    public void MakeSound()
    {
        Console.WriteLine("Roar! Roar!");
    }
}

인터페이스를 사용하면 위와 같은 문제를 방지 할 수 있습니다. 애완동물 인터페이스를 생성하여 애완동물의 기능을 정의하고 파생 클래스에 추가합니다. 이 과정을 통해 파생 클래스를 관리할 수 있으며, 인터페이스가 수정되면 컴파일러가 오류를 발생시켜 인터페이스의 수정을 개발자에게 알릴 것 입니다.

 

default 메서드

C# 8.0이 되면서 인터페이스에도 메서드를 구현할 수 있게 되었습니다. 이것을 default 메서드라 부르며 파생 클래스들은 영향을 받지 않습니다.

interface IAnimal
{
    void MakeSound();

    public void Sleep()
    {
        Console.WriteLine("Sleeping...");
    }
}

class Dog : IAnimal
{
    public void MakeSound()
    {
        Console.WriteLine("Bark!");
    }
}

위 예시와 같이 선언할 수 있으며, 파생 클래스는 해당 메서드를 강제로 구현하지 않아도 됩니다.

IAnimal myDog = new Dog();
myDog.MakeSound();  // print: Bark!
myDog.Sleep();  // print: Sleeping...

만약 루트 오브젝트가 Dog이되면 Sleep은 사용할 수 없습니다. 메서드가 상속되지 않는 것입니다. 이것은 위에도 말했던 죽음의 다이아몬드 상속 문제를 회피하기 위한 것입니다.

interface IAnimal
{
    void MakeSound();

    public void Sleep()
    {
        Console.WriteLine("Sleeping...");
    }
}

class Dog : IAnimal
{
    public void MakeSound()
    {
        Console.WriteLine("Bark!");
    }

    public void Sleep()
    {
        Console.WriteLine("Grrrrr....");
    }
}

그래서 파생클래스에서 메서드를 오버라이딩 해도 문제가 되지 않습니다.

IAnimal myDog = new Dog();
myDog.MakeSound();  // print: Bark!
myDog.Sleep();  // print: Grrrrr....

 

'개발 > C#' 카테고리의 다른 글

C# 이벤트  (0) 2025.02.20
C# 대리자, 익명 메서드 그리고 람다식  (0) 2025.02.18
C# 클래스  (0) 2025.02.13
C# 프로퍼티  (0) 2025.02.12
C# 종료자, Finalize, IDisposable  (0) 2025.02.12