C#LAB
C# 2.5 · 2부. 객체지향

추상 클래스와 인터페이스

덜 만든 틀과, 약속만 적어 둔 것의 차이입니다.

예상 학습 시간 16분 실행 단추가 있는 예제는 고쳐서 실행해 볼 수 있습니다 난이도 기초
개념 설명

덜 만든 틀과, 약속만 적어 둔 것

앞 단원에서 AnimalSpeak() 를 두고 "..." 를 찍게 했습니다. 그런데 동물 일반의 울음소리라는 것은 없습니다. 억지로 채워 넣은 그 기본 동작은 아무 뜻이 없고, 자식이 채우기를 잊어도 조용히 "..." 만 나옵니다.

이럴 때 사용하는 것이 추상 클래스인터페이스입니다. 둘 다 구현 없이 이름만 적어 두고 채우는 일은 물려받는 쪽에 맡깁니다. 채우지 않으면 컴파일되지 않습니다.

  • 추상 클래스덜 만든 클래스입니다. 공통된 값과 동작을 담되, 자식마다 달라야 하는 것은 비워 둡니다. 그 자체로는 만들 수 없습니다.
  • 인터페이스무엇을 할 수 있는지만 적은 약속입니다. 담아 둘 값도, 미리 만들어 둔 구현도 없습니다.

고르는 기준은 관계입니다. "~이다" 면 추상 클래스, "~할 수 있다" 면 인터페이스입니다. 개는 동물이지만, 음악과 영상은 서로 남남이면서도 둘 다 재생할 수 있습니다.

최소 예제 · 편집 가능

남남인 것을 함께 다루기

아래 코드는 브라우저 안에서 실제로 실행됩니다. 고쳐서 눌러 보셔도 됩니다.

Program.cs
IPlayable[] items = { new Music(), new Video() };

foreach (IPlayable item in items) {
    item.Play();
}

// 할 수 있는 일만 적어 둔 약속입니다
interface IPlayable {
    void Play();
}

// 둘은 서로 아무 관계가 없습니다
class Music : IPlayable {
    public void Play() => Console.WriteLine("음악을 재생합니다");
}

class Video : IPlayable {
    public void Play() => Console.WriteLine("영상을 재생합니다");
}
출력
음악을 재생합니다 영상을 재생합니다

코드는 고쳐서 실행해 볼 수 있습니다. 처음 누를 때만 실행기를 내려받느라 잠시 걸립니다. 적은 코드는 서버로 나가지 않습니다.

MusicVideo공통 부모가 없습니다. 상속으로는 함께 담을 수 없는 둘인데, "재생할 수 있다"는 약속 하나로 한 배열에 담겨 같은 방식으로 다뤄집니다.

인터페이스 이름을 I 로 시작하는 것은 문법이 아니라 널리 쓰이는 관례입니다. 클래스인지 인터페이스인지를 이름만 보고 알기 위한 것입니다.

상세 사용법

둘은 어떻게 다른가

추상 클래스 — 덜 만든 클래스
abstract class Animal {
    // 값을 담아 둘 수 있습니다
    protected string Name { get; }

    public Animal(string name) {
        Name = name;
    }

    // 구현이 없습니다 — 자식이 채웁니다
    public abstract void Speak();

    // 구현해 둔 메서드도 함께 둡니다
    public void Sleep() => Console.WriteLine("쿨쿨");
}
인터페이스 — 약속만
interface IPlayable {
    // 담아 둘 값이 없습니다

    // public 을 적지 않아도
    // 밖에서 보입니다
    void Play();
    void Stop();
}

// 여럿을 함께 지정할 수 있습니다
class Music : IPlayable, ISearchable {
    // ...
}
추상 클래스인터페이스
몇 개나하나만 물려받습니다여럿을 지정할 수 있습니다
값 담기필드·프로퍼티를 가집니다담아 둘 수 없습니다
생성자가집니다없습니다
관계~이다~할 수 있다

둘 중 하나를 선택해야 하는 것은 아닙니다. 추상 클래스를 물려받으면서 인터페이스를 여럿 지정할 수 있습니다. 실제 코드에서 흔한 모양입니다.

abstract 와 virtual 의 차이

앞 단원의 virtual기본 동작을 두고 바꾸고 싶으면 바꾸라는 뜻이었습니다. abstract기본 동작이 없고 반드시 채우라는 뜻입니다. 채우지 않으면 컴파일되지 않으므로, 잊어버릴 수가 없습니다.

abstract class Animal {
    public abstract void Speak();          // 반드시 채워야 합니다
    public virtual void Move() => Console.WriteLine("걷습니다");   // 그대로 두어도 됩니다
}

var a = new Animal("이름");   // 컴파일 오류 — 덜 만든 클래스는 만들 수 없습니다

추상 클래스는 그 자체로 만들 수 없습니다. 덜 만들어진 것이므로 당연합니다. 2.4의 예제에서 new Animal() 이 되던 것과 달라지는 지점입니다.

인터페이스에 구현을 두는 일

C# 8.0부터는 인터페이스에도 기본 구현을 둘 수 있습니다. 이미 널리 쓰이는 인터페이스에 멤버를 하나 더할 때, 그것을 구현하던 모든 클래스가 한꺼번에 깨지는 일을 막으려고 마련된 것입니다.

interface IPlayable {
    void Play();

    // 채우지 않은 클래스는 이것을 그대로 사용합니다(C# 8.0+)
    void Pause() => Console.WriteLine("멈춤을 지원하지 않습니다");
}

메서드만이 아닙니다. 프로퍼티에도 기본 구현을 둘 수 있습니다. 다만 담아 둘 필드가 없다는 제약이 그대로라, 대개 이미 있는 것으로 셈해서 돌려주는 모양이 됩니다.

interface IPlayable {
    // ① 구현 없이 적기 — 구현하는 쪽이 채웁니다(예전부터 됩니다)
    string Title { get; set; }

    // ② 계산해서 돌려주기 — 채우지 않아도 됩니다(C# 8.0+)
    string Label => $"[{Title}]";

    // ③ 컴파일 오류 — 담아 둘 자리가 없습니다
    // string Memo { get; set; } = "";
}

①과 ②의 차이가 인터페이스의 성격을 그대로 보여 줍니다. ①은 담을 자리를 구현하는 쪽에 맡기는 것이고, ②는 이미 있는 것으로 셈해서 내주는 것입니다. 어느 쪽도 인터페이스 자신은 값을 들고 있지 않습니다.

set 에도 기본 구현을 둘 수 있습니다. 다만 받은 값을 담아 둘 자리가 없으니, 아래처럼 다른 멤버로 넘기는 것 말고는 할 수 있는 일이 없습니다. 그래서 실제로는 ②처럼 get 만 두는 모양이 훨씬 흔합니다.

string Label {
    get => $"[{Title}]";
    set => Title = value.Trim('[', ']');   // 담지 못하고 Title 에 넘깁니다
}

이 제약은 무엇을 추가할 수 있는지도 정합니다. 이미 담겨 있는 값으로 셈하거나 그것을 바꾸는 멤버는 얼마든지 추가할 수 있지만, 자기만의 새 값을 담아야 하는 멤버는 추가할 수 없습니다 — 담을 자리가 인터페이스에도, 이미 나가 있는 클래스에도 없기 때문입니다.

그래서 이것이 있다고 인터페이스가 추상 클래스가 되는 것은 아닙니다. 여전히 값을 담아 둘 수 없고 생성자도 없습니다. 새로 만드는 인터페이스에 기본 구현을 잔뜩 두기보다, 이미 나가 있는 것을 확장할 때 사용하는 도구로 여기는 편이 맞습니다.

버전 배지

이 문법, 몇 버전부터 사용할 수 있나요

1.0
2.0
3.0
6.0
7.0
8.0
9.0
11.0
14.0
  • 1.0abstract · interface
  • 8.0기본 인터페이스 멤버 — 인터페이스에 구현 두기
C# 1.0+

추상 클래스와 인터페이스는 C# 1.0부터 있었습니다. 뒤에 더해진 기본 구현(C# 8.0)은 이미 나가 있는 약속을 어떻게 확장하는가에 대한 답이었습니다 — 멤버를 추가해도 기존 클래스가 깨지지 않게 한 것입니다.

인터페이스에 static abstract 를 적어 인스턴스가 아니라 형식 자체가 구현할 것을 정하는 길도 C# 11.0에 열렸습니다. 다만 그것은 제네릭을 알아야 쓸모가 보이므로 3부에서 다룹니다.

실습 문제

직접 해보기

1. 반드시 채우게 하기 난이도 하

Shape 를 추상 클래스로 만들고 넓이를 돌려주는 메서드를 구현 없이 두세요. RectangleCircle 이 저마다 채우고, 둘을 한 배열에 담아 넓이를 출력해보세요.

2.4에서는 virtual 로 두고 0을 돌려주게 했습니다. 이번에는 abstract 라 구현을 적지 않으며, 채우는 쪽은 그대로 override 입니다.
Shape[] shapes = { new Rectangle(3, 4), new Circle(5) }; foreach (Shape shape in shapes) { Console.WriteLine(shape.Area()); } abstract class Shape { // 도형 일반의 넓이라는 것은 없습니다 public abstract double Area(); } class Rectangle(double w, double h) : Shape { public override double Area() => w * h; } class Circle(double r) : Shape { public override double Area() => 3.14 * r * r; }
2. 남남인 것에 같은 약속 지우기 난이도 중

IStorable 이라는 약속을 만들어 저장 이름을 돌려주게 하고, 서로 관계없는 두 클래스(예: 사진과 문서)가 그것을 구현하게 해보세요. 그리고 한 배열에 담아 이름을 출력해보세요.

인터페이스를 구현하는 메서드에는 override 를 적지 않습니다. 부모의 구현을 바꾸는 것이 아니라 인터페이스를 채우는 것이기 때문입니다.
IStorable[] items = { new Photo("바다"), new Document("보고서") }; foreach (IStorable item in items) { Console.WriteLine(item.FileName()); } interface IStorable { string FileName(); } class Photo(string title) : IStorable { public string FileName() => $"{title}.jpg"; } class Document(string title) : IStorable { public string FileName() => $"{title}.pdf"; }
요약
  • "~이다" 면 추상 클래스, "~할 수 있다" 면 인터페이스입니다.
  • 추상 클래스는 하나만 물려받지만, 인터페이스는 여럿을 지정할 수 있습니다.
  • abstract반드시 채우라는 뜻입니다. 채우지 않으면 컴파일되지 않습니다.
  • 추상 클래스는 그 자체로 만들 수 없습니다. 덜 만들어진 것이기 때문입니다.

C# 버전별 변경 이력 에서 각 버전이 무엇을 더했는지 볼 수 있습니다.