추상 클래스와 인터페이스
덜 만든 틀과, 약속만 적어 둔 것의 차이입니다.
덜 만든 틀과, 약속만 적어 둔 것
앞 단원에서 Animal 에
Speak() 를 두고 "..." 를
찍게 했습니다. 그런데 동물 일반의 울음소리라는 것은 없습니다.
억지로 채워 넣은 그 기본 동작은 아무 뜻이 없고, 자식이 채우기를 잊어도 조용히
"..." 만 나옵니다.
이럴 때 사용하는 것이 추상 클래스와 인터페이스입니다. 둘 다 구현 없이 이름만 적어 두고 채우는 일은 물려받는 쪽에 맡깁니다. 채우지 않으면 컴파일되지 않습니다.
- 추상 클래스 — 덜 만든 클래스입니다. 공통된 값과 동작을 담되, 자식마다 달라야 하는 것은 비워 둡니다. 그 자체로는 만들 수 없습니다.
- 인터페이스 — 무엇을 할 수 있는지만 적은 약속입니다. 담아 둘 값도, 미리 만들어 둔 구현도 없습니다.
고르는 기준은 관계입니다. "~이다" 면 추상 클래스, "~할 수 있다" 면 인터페이스입니다. 개는 동물이지만, 음악과 영상은 서로 남남이면서도 둘 다 재생할 수 있습니다.
남남인 것을 함께 다루기
아래 코드는 브라우저 안에서 실제로 실행됩니다. 고쳐서 눌러 보셔도 됩니다.
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("영상을 재생합니다"); }
코드는 고쳐서 실행해 볼 수 있습니다. 처음 누를 때만 실행기를 내려받느라 잠시 걸립니다. 적은 코드는 서버로 나가지 않습니다.
Music 과 Video 는
공통 부모가 없습니다. 상속으로는 함께 담을 수 없는 둘인데,
"재생할 수 있다"는 약속 하나로 한 배열에 담겨 같은 방식으로 다뤄집니다.
인터페이스 이름을 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.0abstract · interface
- 8.0기본 인터페이스 멤버 — 인터페이스에 구현 두기
추상 클래스와 인터페이스는 C# 1.0부터 있었습니다. 뒤에 더해진 기본 구현(C# 8.0)은 이미 나가 있는 약속을 어떻게 확장하는가에 대한 답이었습니다 — 멤버를 추가해도 기존 클래스가 깨지지 않게 한 것입니다.
인터페이스에 static abstract 를 적어
인스턴스가 아니라 형식 자체가 구현할 것을 정하는 길도
C# 11.0에 열렸습니다. 다만 그것은 제네릭을 알아야 쓸모가 보이므로
3부에서 다룹니다.
직접 해보기
Shape 를 추상 클래스로 만들고 넓이를 돌려주는 메서드를
구현 없이 두세요. Rectangle 과
Circle 이 저마다 채우고, 둘을 한 배열에 담아
넓이를 출력해보세요.
IStorable 이라는 약속을 만들어 저장 이름을 돌려주게
하고, 서로 관계없는 두 클래스(예: 사진과 문서)가 그것을 구현하게 해보세요. 그리고
한 배열에 담아 이름을 출력해보세요.
- "~이다" 면 추상 클래스, "~할 수 있다" 면 인터페이스입니다.
- 추상 클래스는 하나만 물려받지만, 인터페이스는 여럿을 지정할 수 있습니다.
abstract는 반드시 채우라는 뜻입니다. 채우지 않으면 컴파일되지 않습니다.- 추상 클래스는 그 자체로 만들 수 없습니다. 덜 만들어진 것이기 때문입니다.
C# 버전별 변경 이력 에서 각 버전이 무엇을 더했는지 볼 수 있습니다.