CodeGym /행동 /C# SELF /인터페이스와 추상 클래스 비교

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

C# SELF
레벨 24 , 레슨 2
사용 가능

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를 숨기기에 좋고, 때로는 초보자에겐 헷갈릴 수 있어.

2
과제
C# SELF, 레벨 24, 레슨 2
잠금
기본 구현이 포함된 메서드를 사용하는 인터페이스 구현
기본 구현이 포함된 메서드를 사용하는 인터페이스 구현
2
과제
C# SELF, 레벨 24, 레슨 2
잠금
인터페이스의 static 메서드 사용하기
인터페이스의 static 메서드 사용하기
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION