1. 고전적인 차이점 간단 정리
누가 "인터페이스는 그냥 시그니처 모음이야"라고 하면, "너 C# 몇 버전 써?"라고 물어봐. C# 8부터 인터페이스가 훨씬 강력해졌거든. 이제는 .NET 플랫폼의 새로운 기능까지 포함해서, 고전적인 특징뿐 아니라 최신 기능까지 추상 클래스와 비교해볼 때가 됐어.
몇 년 전, C# 7 시절로 돌아가보면 — 진짜 단순했지. 추상 클래스는 필드랑 부분적으로 구현된 메서드를 가질 수 있었고, 인터페이스는 오직 시그니처(메서드, 프로퍼티, 이벤트, 인덱서)만 가능했어.
추상 클래스 상속은 "is-a" (Is-a) 관계로, 인터페이스는 다중 상속("can-do", can-do)으로 행동을 구현하지.
| 특징 | 추상 클래스 | 인터페이스 (C# 8 이전) |
|---|---|---|
| 관계 | is-a | can-do |
| 상속 | 하나만 가능 | 다중 가능 |
| 필드 | 가질 수 있음 | 가질 수 없음 |
| 메서드 구현 | 가능 | 불가능 |
| 생성자 | 가능 | 불가능 |
| 접근 제한자 | 여러 가지 (public, protected, ...) | 암시적으로 public만 |
보다시피, 예전엔 추상 클래스가 인터페이스보다 훨씬 "형님" 느낌이었지 — 더 강력하고 유연했어. 근데 이제는 달라졌어!
2. Default 구현이 있는 인터페이스
C# 8부터 (그리고 C# 14, .NET 9에서는 더더욱) 인터페이스에 default 구현 메서드라는 슈퍼파워가 생겼어. 이걸 "Default Interface Methods" (DIM)라고 불러.
어떻게 생겼냐면?
public interface IAnimal
{
void SayHello();
// Default 구현 메서드!
void Walk()
{
Console.WriteLine("나 걷고 있어...");
}
}
와우! 이제 인터페이스도 메서드 구현을 가질 수 있게 된 거야. 그것도 여러 개! 단, 이런 메서드는 반드시 본문이 있어야 하고, 필드, private 메서드, 생성자는 여전히 안 돼.
3. 최신 인터페이스 기능
요즘 .NET 개발자라면 알아야 할 새로운 기능들:
- Default 구현 메서드.
- 인터페이스 내부의 private 메서드 (보조용, 같은 인터페이스 내에서만 사용 가능).
- static 메서드 (C# 8부터).
- Default 구현이 있는 프로퍼티.
- static 필드 (C# 14부터 — "static interface members").
- 추상 static 멤버 ("abstract static members" — 이제 인터페이스가 구현체에 특정 static 메서드를 요구할 수 있음!).
최신 인터페이스 예시:
public interface ILogger
{
static int LoggerCount { get; set; } // C# 14
void Log(string message); // 시그니처 (계약)
// Default 구현
void LogWarning(string warning)
{
Log("[WARNING]: " + warning);
}
// 인터페이스 내 private 보조 메서드 (C# 8+)
private void FormatAndLog(string level, string msg)
{
Log($"{level}: {msg}");
}
// 인터페이스 내 static 메서드 (C# 8+)
static void PrintLoggerInfo()
{
Console.WriteLine("ILogger 인터페이스 — 최고의 도우미!");
}
}
상상해봐 — 예전엔 이런 게 불가능했어, 마치 고양이가 서버 경비원 하는 것처럼 말이지.
4. 추상 클래스: 뭐가 달라졌나?
추상 클래스는... 뭐랄까... 지난 10년간 크게 진화하진 않았어. 여전히 다음을 가질 수 있지:
- 필드 (private, protected, static 포함).
- 구현된 메서드와 추상 메서드.
- 생성자 (초기화 로직 넣을 수 있음).
- 프로퍼티, 이벤트, 인덱서.
- static 및 인스턴스 멤버.
추상 클래스 예시:
public abstract class Animal
{
public string Name { get; set; }
public abstract void Speak();
public virtual void Walk()
{
Console.WriteLine($"{Name} 발로 걷는다!");
}
protected void Eat()
{
Console.WriteLine($"{Name} 사료 먹는다.");
}
}
추상 클래스는 여전히 공통 로직, 상태, 행동을 클래스 계층에 저장하기에 딱 좋은 곳이야.
5. 최신 비교: 새로운 기능까지 포함한 표
| 특징 | 추상 클래스 | 인터페이스 (C# 14+, .NET 9) |
|---|---|---|
| 관계 | is-a (이다) | can-do (할 수 있다) |
| 상속 | 하나만 가능 | 다중 가능 |
| 필드 | 네, 아무거나 | static만* (C# 14+) |
| 생성자 | 네 | 아니오 |
| 메서드 구현 | 네 (virtual/abstract) | 네 (default, static, abstract static) |
| 구현된 프로퍼티 | 네 | 네 (default 구현) |
| private 멤버 | 네 | 네 (메서드만, C# 8+) |
| static 멤버 | 네 | 네 (C# 8+, 제한 있음) |
| static 필드 | 네 | 네 * (C# 14+) |
| 접근 제한자 | 아무거나 | 기본적으로 public 또는 private |
* — 인터페이스의 static 필드는 특별한 경우에만 쓰이고, 아주 최신 기능이야.
6. 언제 인터페이스, 언제 추상 클래스? 최신 팁
인터페이스 (이제 default 구현도 있음)는 컴포넌트 간 계약을 만드는 도구야. 핵심은 다중성. 네 클래스가 인터페이스를 10개든 20개든 구현할 수 있어서 진짜 만능병사가 될 수 있지.
추상 클래스는 이런 경우에 골라:
- 공통 상태(필드), 로직, 행동을 상속받는 클래스에 주고 싶을 때.
- 기본이지만 오버라이드 가능한 로직이 필요할 때 (virtual 사용).
- 생성자로 초기화 로직을 중앙에서 관리하고 싶을 때.
실제 프로젝트에서는 이런 패턴이 많아: "순수 계약"은 인터페이스로, 공통 코드나 인프라가 필요하면 추상 베이스 클래스를 만들어.
. ┌────────────────────────┐
│ 인터페이스 │
│ (계약: 뭘 할 수 있나) │
└─────────┬──────────────┘
│
┌──────────────┼──────────────┐
│ │ │
구현 1 구현 2 ... 구현 N
MyLogger CloudLogger FileLogger
(추상 클래스 상속과 조합도 가능)
7. 시나리오 — 언제 뭐가 이득?
다중 구현:
예를 들어, IDrivable 인터페이스와 Vehicle 추상 클래스가 있다고 해보자. Car 클래스는 Vehicle을 상속받으면서 동시에 여러 인터페이스(IDrivable, IRepairable, IInsurable)를 구현할 수 있어. 만약 Repairable이 추상 클래스였다면 Vehicle이랑 Repairable 중 하나만 골라야 했겠지! 이럴 땐 인터페이스가 확실히 이득이야.
공통 로직과 상태:
모든 "자동차"에 "번호" 필드가 있다고 해보자. 이건 추상 클래스의 필드여야 해. 인터페이스엔 (static 빼고) 필드 못 넣어.
API 진화:
Default Interface Methods 덕분에, 이제 인터페이스를 기존 사용자 코드 안 깨고 진화시킬 수 있어.
예를 들어, 인터페이스에 default 구현이 있는 새 메서드를 추가해도 — 다 잘 돌아가고, 기존 구현체도 안 깨져! 예전엔 이게 진짜 아팠거나, 아예 불가능했지.
8. 실전 예제
우리 학습 앱에 점점 로깅이 추가되고 있어. 직접 ILogger 인터페이스를 default 구현까지 만들어보자:
public interface ILogger
{
void Log(string message);
// 모든 구현체에서 쓸 수 있는 default 구현!
void LogInfo(string info)
{
Log("[INFO] " + info);
}
// 인터페이스 static 메서드
static void PrintHelp()
{
Console.WriteLine("이벤트 로깅엔 ILogger를 쓰세요");
}
}
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine(message);
}
}
// 코드 어딘가에서:
ILogger logger = new ConsoleLogger();
logger.LogInfo("시스템 시작!"); // default 구현 덕분에 동작함
// 인터페이스 static 메서드 호출
ILogger.PrintHelp();
만약 인터페이스에 default 구현이 있는 새 메서드를 추가하면, 기존 구현체(예: ConsoleLogger)도 자동으로 이 메서드를 쓸 수 있어 — 코드 깨질 걱정 없음.
9. 실전에서의 실수와 주의점
겉보기엔 다 좋아 보여도, 실제로는 주의할 점이 있어. 예를 들어, 인터페이스에 default 구현이 있어도, 객체를 클래스 타입으로 다루면 default 구현은 인터페이스 타입에서만 쓸 수 있어.
ConsoleLogger log = new ConsoleLogger();
log.LogInfo("Hello"); // 컴파일 안 됨: LogInfo는 클래스에 없음!
ILogger log2 = log;
log2.LogInfo("Hello"); // 잘 동작!
이건 인터페이스 명시적 구현의 특수 케이스랑 비슷해. 때로는 "불필요한" API를 숨기기에 좋고, 때로는 초보자에겐 헷갈릴 수 있어.
GO TO FULL VERSION