1. 소개
예전엔 인터페이스가 사립학교 규칙처럼 엄격했어: 시그니처만 있고, 필드도 없고, 구현도 없고, static 멤버도 없었지! 하지만 .NET은 계속 발전하고, 프로그래밍 언어도 살아있는 생물처럼 새로운 도전에 맞춰 진화해야 해.
새로운 C# 버전이 나오면서 인터페이스도 새로운 트릭을 배웠어. 그 중 가장 눈에 띄는 게 바로 static 멤버야. 이제 인터페이스에도 static 메서드, 프로퍼티, 이벤트를 넣을 수 있어. 그리고 C# 11부터는 static abstract 메서드도 선언할 수 있어서, 이 인터페이스를 구현하는 타입이 반드시 static 메서드를 구현해야 해.
이건 진짜 패러다임의 큰 변화야. 제네릭이나 객체지향 프로그래밍을 다루는 방식이 달라졌거든.
쉽게 말하면: 인터페이스의 static 멤버는 "공용" 멤버야. 객체 인스턴스가 아니라 인터페이스 타입(혹은 구현 타입) 자체로 접근하는 거지.
예전엔 클래스, 구조체, enum만 static 메서드나 프로퍼티를 가질 수 있었는데, 이제 인터페이스도 가능해졌어.
어떻게 생겼냐고? 문법 예시
public interface IMyMath
{
static int Add(int a, int b) => a + b; // static 메서드 (기본 구현)
static abstract int Multiply(int a, int b); // 구현 타입에서 반드시 구현해야 함
}
- static — 타입 레벨에서 접근, 인스턴스 아님.
- static abstract — "반드시" static 메서드를 구현해야 한다는 계약.
- 이제 인터페이스(클래스처럼)에도 static 메서드, 프로퍼티, 이벤트 선언 가능. 상수도 만들 수 있어. 하지만 여전히 인스턴스 필드나 static 필드는 못 넣어(상수 제외).
static 멤버의 역할은 범용적인 "연산"을 선언하는 거야. 예를 들어, 객체 컬렉션이 있는데 어떤 타입이 인터페이스를 구현하는지 몰라도 "비교" 같은 걸 호출하고 싶을 때, static abstract 멤버로 가능해.
2. 인터페이스에 구현이 있는 static 메서드
이게 왜 필요해?
고전적인 문제: 인스턴스 메서드(객체로 뭔가 하는 것)뿐 아니라 static 메서드(예: 문자열에서 객체 만들기, 두 객체를 static하게 비교하기)도 선언하고 싶을 때가 있어. 예전엔 Factory, Comparer, Helper 같은 패턴으로 우회했는데, 이제 인터페이스에서 바로 표현 가능해.
특히 Generics나 임의 타입을 다루는 알고리즘에서 엄청 중요해졌어:
- 범용 연산자 구현 (예: 더하기, 비교 등).
- 제네릭 코드 제한: "static 메서드나 연산자가 있는 타입만…" 같은 조건.
- 직렬화/역직렬화: 코드 작성 시 타입을 몰라도 문자열에서 객체를 만들고 싶을 때.
구현이 있는 static 메서드
C# 8부터 인터페이스에 구현이 있는 static 메서드를 넣을 수 있게 됐어. 클래스의 static 메서드랑 비슷해.
public interface IUtility
{
static void PrintHello()
{
Console.WriteLine("인터페이스에서 Hello!");
}
}
이런 메서드는 이렇게 호출해: IUtility.PrintHello();
인터페이스에 논리적으로 속하지만 특정 구현과는 상관없는 유틸 함수에 딱 좋아. 예: 타입 전체 통계, CreateDefault 같은 팩토리 메서드, 공통 유효성 검사 등.
특징: 인터페이스의 static 멤버는 클래스에서 "오버라이드" 안 됨
인터페이스에 static void Method() { ... }를 선언하면, 구현 클래스에서 똑같은 static 메서드를 선언해도 오버라이드가 아냐! 그냥 이름만 같고 완전히 별개인 두 메서드야. "가상 static 메서드"가 아니란 뜻.
3. static abstract 멤버
C# 11부터 인터페이스에 static abstract 메서드를 선언할 수 있어. 이건 "이 인터페이스를 구현하는 클래스나 구조체는 반드시 이 시그니처의 static 멤버를 구현해야 한다"는 뜻이야.
예시:
public interface IParsable<T>
{
static abstract T Parse(string s);
}
이 인터페이스를 구현하는 타입은 static 메서드 Parse(string s)를 반드시 구현해야 해.
클래스에서 이런 인터페이스 구현하기
public class Temperature : IParsable<Temperature>
{
public int Value { get; set; }
// static 구현!
public static Temperature Parse(string s)
{
var temp = new Temperature();
temp.Value = int.Parse(s);
return temp;
}
}
어떻게 동작하냐면?
이게 제네릭 코드에서 특히 재밌어져:
public static T ParseFromString<T>(string s) where T : IParsable<T>
{
return T.Parse(s);
}
// 사용 예시:
var temp = ParseFromString<Temperature>("42");
이제 "static" 동작을 구현한 아무 타입이나 받아서 진짜 범용 코드를 쓸 수 있어!
4. 인터페이스의 static 멤버 vs. 일반 클래스 static 멤버
| 특징 | 클래스 static 멤버 | 인터페이스 static 멤버 |
|---|---|---|
| 상속됨 | 아니오 | 아니오, 하지만 인터페이스 계약 내에서 구현됨 |
| 구현 필수? | 아니오 | static abstract일 때만 필수 |
| Generics에서 사용 | 아니오 (C# 11 전까지) | 가능 (static abstract부터) |
| 기본 구현 가능? | 가능 | 가능 |
| 오버라이드 | 아니오 | 아니오, 반드시 구현만 필요 |
| 호출 시 가시성 | 타입 이름으로 | 인터페이스 타입이나 구현 타입으로 |
7. 실제 예시
이제 실제로 우리 작은 학습용 앱에 이 기능을 어떻게 적용할 수 있는지 보자.
"IPrintable" 인터페이스가 있다고 해보자:
public interface IPrintable
{
void Print();
static void PrintAll(IEnumerable<IPrintable> items)
{
foreach (var item in items)
{
item.Print();
}
}
}
이제 이렇게 쉽게 호출할 수 있어:
var documents = new List<IPrintable>
{
new Invoice { Number = "INV-001" },
new Receipt { Number = "RC-007" }
};
IPrintable.PrintAll(documents); // 인터페이스의 static 메서드!
이런 구조는 인터페이스 구현 전체에 "그룹 작업" 할 때 완전 좋아.
좀 더 고급 예시: 숫자 타입의 범용 더하기
인터페이스가 있다고 해보자:
public interface IAddable<T>
{
static abstract T Add(T left, T right);
}
정수용(래퍼 클래스) 구현:
public struct MyInt : IAddable<MyInt>
{
public int Value { get; }
public MyInt(int val) => Value = val;
public static MyInt Add(MyInt left, MyInt right) => new MyInt(left.Value + right.Value);
}
그리고, 두 숫자 타입 T를 더하는 범용 함수:
public static T Sum<T>(T a, T b) where T : IAddable<T>
{
return T.Add(a, b);
}
// 사용 예시:
var x = new MyInt(5);
var y = new MyInt(6);
var z = Sum(x, y); // z.Value == 11
이런 범용성을 위해서 인터페이스 static 멤버 지원이 생긴 거야!
8. 흔한 실수와 특징
새로운 기능이 항상 예제처럼 쉽진 않아. 초보가 헷갈릴 만한 포인트 몇 개 정리해줄게:
인터페이스 static 메서드는 구현 클래스에 "상속"되지 않아. 인터페이스에 static void Foo()가 있으면, MyClass.Foo()와 IMyInterface.Foo()는 완전히 별개 메서드야.
static abstract는 반드시 구현해야 해. 까먹으면 컴파일러가 "인터페이스 완전히 구현 안 됐음"이라고 알려줄 거야.
Generic 제약: static abstract 멤버를 쓰려면 generic 파라미터에 인터페이스로 제한 걸어야 해 (where T : IMyInterface).
아직 모든 툴이 이 기능을 완벽히 지원하진 않아. 예를 들어 Rider, VS Code, 구버전 Roslyn 분석기는 .NET이 C# 11+를 지원하지 않으면 static abstract 멤버를 제대로 못 보여줄 수도 있어.
인터페이스 extension 메서드랑 헷갈리지 마: extension 메서드는 따로 구현되고, static 멤버처럼 동작하지 않아.
GO TO FULL VERSION