1. 주요 인터페이스 분류
혹시 인터페이스가 아키텍트랑 "클린 코드" 좋아하는 사람들만 쓰는 장난감이라고 생각한다면, 놀라지 마! 진지하게 C# 프로젝트를 하다 보면 인터페이스에 푹 빠지게 돼. 왜냐면 .NET 표준 라이브러리의 거의 모든 부분이 인터페이스로 만들어져 있거든! 인터페이스 없으면 컬렉션도 못 쓰고, 파일도 못 읽고, LINQ로 필터링도 못 해.
인터페이스는 .NET에서 다형성과 확장성의 기본이야. 프레임워크의 모든 "블록"이 서로 잘 맞물리게 해주는 게 바로 인터페이스지.
.NET 표준 라이브러리는 진짜 인터페이스 투성이야. 이 바다에서 안 빠지려면, 아래처럼 분류해볼게 (물론 이게 다는 아니야 — 다 외우려면 인생이 모자라!):
| 카테고리 | 인터페이스 | 뭐에 쓰는 거야? |
|---|---|---|
| 컬렉션 | , , , , |
순회, 수정, 인덱스로 접근, 키-값 쌍 작업 |
| 리소스 작업 | |
리소스 해제 (파일, 연결, 스트림) |
| 비교 | , , |
정렬, 비교, 객체의 유일성 |
| 직렬화 | |
객체를 바이트 스트림으로 변환 (그리고 다시) |
| LINQ와 쿼리 | , |
복잡한 쿼리 지원 (예: DB 쿼리) |
| 비동기 | , |
비동기 순회 및 리소스 해제 |
| 이벤트와 알림 | , |
속성/컬렉션 변경에 반응 |
| 날짜와 시간 | |
커스텀 문자열 포맷팅 |
| 자료구조 | , |
컬렉션과 튜플의 깊은 비교 |
| 스트림/입출력 | , , , |
스트림 작업, push/pull 알림 |
중요! 위에 나온 많은 인터페이스들은 타입 파라미터를 써: List<string>. 보통 컬렉션이나 컬렉션 인터페이스에서 많이 써 Int[] → List<int>. 자세한 건 컬렉션 배울 때 다시 볼 거야.
2. 컬렉션 인터페이스
IEnumerable와 IEnumerator — foreach의 입장권
.NET의 거의 모든 컬렉션은 IEnumerable 또는 그 제네릭 버전 IEnumerable<T>을 구현해. 이 인터페이스 덕분에 foreach라는 마법 같은 문법을 쓸 수 있지:
List<int> numbers = new List<int> { 1, 2, 3 };
foreach (int n in numbers) // List<int>가 IEnumerable<int>를 구현해서 동작함
{
Console.WriteLine(n);
}
이 인터페이스의 최소 버전은 이렇게 생겼어:
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
IEnumerator는 실제로 컬렉션을 돌아다니는 "순회자" 인터페이스야:
public interface IEnumerator
{
bool MoveNext();
object Current { get; }
void Reset();
}
실제로는 진짜 자주 IEnumerable<T> 타입으로 파라미터를 넘기거나 값을 반환하게 될 거야. 예를 들어, 배열에서 짝수만 반환하는 메서드:
public IEnumerable<int> GetEvenNumbers(int[] array)
{
foreach (var x in array)
if (x % 2 == 0) yield return x;
}
맞아, yield 키워드가 마법을 부려서 인터페이스 구현을 대신 해줘. 이건 나중에 더 자세히 볼 거야.
ICollection과 IList — 인덱스 접근과 수정 가능한 컬렉션
단순히 순회만 하는 게 아니라, 요소를 추가/삭제하거나 인덱스로 접근하고 싶으면 좀 더 특화된 인터페이스가 필요해:
- ICollection<T> — Add, Remove 메서드랑 Count 속성이 추가됨.
- IList<T> — 인덱스 접근 (this[int index]) 등 더 많은 기능이 추가됨.
public void PrintFirstItem(IList<string> list)
{
if (list.Count > 0)
Console.WriteLine(list[0]);
}
List<T>는 IEnumerable<T>, ICollection<T>, IList<T>를 다 구현해. 그래서 단순히 순회만 하거나, 다양한 메서드를 다 쓸 수도 있어서 엄청 편해.
IDictionary<TKey, TValue> — "키-값" 쌍
딕셔너리 작업 좋아한다면 (진짜 자주 쓰지!), IDictionary<TKey, TValue> 인터페이스를 써. 이걸로 키로 값을 찾거나, 반대로도 가능해.
public void PrintAllPairs(IDictionary<string, int> ages)
{
foreach (var pair in ages)
Console.WriteLine($"{pair.Key}: {pair.Value}");
}
여기서 ages는 Dictionary<string, int>일 수도 있고, SortedDictionary<string, int>일 수도 있어 — 중요한 건 둘 다 이 인터페이스를 구현한다는 거!
3. IDisposable 인터페이스: 리소스 제대로 다루기
모든 입출력, 파일 작업, 네트워크 연결, DB 작업은 .NET에서 IDisposable 인터페이스에 달려 있어. 이 인터페이스는 진짜 중요한 약속을 정해: 객체에 관리되지 않는 리소스가 있으면, 사용 후 "정리"해야 해. 맞아, 이 친구 덕분에 using 문법이 가능해진 거지:
using (StreamReader reader = new StreamReader("file.txt"))
{
// 파일 작업, using 끝나면 파일이 무조건 닫힘!
string line = reader.ReadLine();
}
인터페이스 자체는 엄청 간단해:
public interface IDisposable
{
void Dispose();
}
근데 "진짜" 라이브러리들은 다 이걸 구현해! 더 좋은 사용법은 공식 문서 참고해봐.
4. 비교와 유일성을 위한 인터페이스
IComparable<T>와 IComparer<T>
컬렉션의 객체들을 정렬하고 싶으면, 객체끼리 서로 비교할 수 있어야 해. 그럴 때 IComparable<T> 인터페이스를 쓰는 거지:
public class Student : IComparable<Student>
{
public string Name { get; set; }
public int Score { get; set; }
public int CompareTo(Student? other)
{
// 점수 내림차순 정렬
if (other == null) return 1;
return other.Score.CompareTo(this.Score);
}
}
// 이제 학생들을 정렬할 수 있음:
var students = new List<Student> { ... };
students.Sort();
IComparer<T>는 클래스 밖에서 비교 기준을 정할 수 있게 해줘 — 예를 들어, 학생을 이름으로 정렬하거나 점수로 정렬하고 싶을 때:
public class NameComparer : IComparer<Student>
{
public int Compare(Student? x, Student? y)
{
return string.Compare(x?.Name, y?.Name);
}
}
IEquatable<T> — 동등성 비교
객체가 HashSet<T>에 들어가서 (즉, 컬렉션에서 "유일"하게 취급되길) 원하면, IEquatable<T>를 구현해:
public class Person : IEquatable<Person>
{
public string Name { get; set; }
public bool Equals(Person? other)
{
if (other == null) return false;
return this.Name == other.Name;
}
}
5. 이벤트와 알림용 인터페이스
프로그램이 객체의 속성이 바뀌었는지 "알고" 싶었던 적 있어? 예를 들어, 사용자가 성을 바꾸면 UI가 자동으로 갱신되길 바란다면? 바로 그럴 때 INotifyPropertyChanged 인터페이스가 있어. 이건 WPF나 Xamarin 같은 GUI 앱에서 엄청 많이 써.
인터페이스 시그니처는 간단해:
public interface INotifyPropertyChanged
{
event PropertyChangedEventHandler? PropertyChanged;
}
구현 예시:
public class User : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
private string name;
public string Name
{
get => name;
set
{
if (name != value)
{
name = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
}
}
}
}
이제 이 객체에 바인딩된 UI는 Name이 바뀔 때마다 자동으로 갱신돼. 더 궁금하면 공식 문서 참고!
6. 그 외 유용한 인터페이스
IFormattable 인터페이스: 유연한 포맷팅
객체를 예쁘게, 다양한 방식으로 문자열로 출력하고 싶으면 (예: 소수점 자리수 다르게), IFormattable 인터페이스를 구현해:
public class Temperature : IFormattable
{
public double Celsius { get; }
public Temperature(double celsius) => Celsius = celsius;
public string ToString(string? format, IFormatProvider? formatProvider)
{
// 복잡하게 안 하고, 포맷에 따라 'C' 또는 'F'로 보여줌
if (format == "F")
return $"{Celsius * 9 / 5 + 32} F";
return $"{Celsius} C";
}
}
이제 temp.ToString("F", null) 또는 temp.ToString("C", null)을 부를 수 있어. 그래서 DateTime, double, decimal 같은 내장 타입들이 유연한 포맷팅을 지원하는 거야.
비동기용 인터페이스
C#에 비동기가 도입되면서, 비동기 세계에서 쓸 새로운 인터페이스가 필요해졌어. 예를 들어, 객체가 비동기로 리소스를 해제해야 한다면 IAsyncDisposable을 구현해:
public interface IAsyncDisposable
{
ValueTask DisposeAsync();
}
이제 using 대신 await using을 쓰면 돼!
비동기 열거 (await foreach)는 IAsyncEnumerable<T> 인터페이스로 동작해. 이걸로 메인 스레드를 막지 않고 요소를 순회할 수 있어. 예를 들어, 큰 파일을 부분적으로 읽거나, 인터넷에서 데이터 스트림을 받을 때 써. 자세한 건 58레벨에서 :P
직렬화 인터페이스: ISerializable
객체를 바이트 스트림으로 만들어 저장/전송(예: 네트워크로)해야 한다면, .NET은 ISerializable 인터페이스를 제공해. 요즘은 더 편한 방법(속성이나 내장 직렬화기)이 많아서 자주 쓰진 않지만, 언급할 가치는 있어. 시그니처 예시:
public interface ISerializable
{
void GetObjectData(SerializationInfo info, StreamingContext context);
}
7. 인터페이스 실제 적용: 진짜 앱 예시
예를 들어, 도서관 책 관리 콘솔 앱을 만든다고 해보자 (누군가는 해야지!). 책 목록 출력, 정렬, 필터, 데이터 로드/저장 기능을 넣고 싶어. .NET 인터페이스를 알면 이렇게 할 수 있어:
- 다양한 컬렉션 타입 반환 — IEnumerable<Book>로 반환하면, 사용자는 배열인지 리스트인지 신경 안 써도 돼.
- 여러 기준으로 정렬 추가: IComparable<Book>로 제목 정렬, IComparer<Book>로 저자 정렬 구현.
- 파일 작업 시 리소스 효율적 해제 — IDisposable로 처리.
- 장르나 저자별로 책 필터링 — LINQ로, IEnumerable<T>만 구현하면 다 됨.
- 앱 확장성 확보 — 예를 들어, 파일 저장소 대신 DB 연결로 바꿀 때, 클래스가 인터페이스(IBookStorage)로 동작하면 구현만 바꾸면 됨.
8. 흔한 실수와 주의점
초보들이 제일 많이 하는 실수 중 하나는 변수나 메서드 인자 선언할 때 구체적인 구현체("List")를 쓰는 거야. 인터페이스("IEnumerable", "IList")로 선언해야 코드가 유연하고 확장하기 쉬워. 기억해: "인터페이스로 프로그래밍"하면 코드가 훨씬 유연해져.
어떤 수준의 인터페이스를 쓸지 고를 때도 신경 써야 해 — 예를 들어, 단순히 순회만 필요하면 IEnumerable<T>만 쓰고, IList<T>까지 요구하지 않는 게 좋아. 괜히 제한을 늘리지 말자.
또 한 가지: 클래스에 인터페이스가 많고, 그 사이에 메서드나 속성이 겹치면, 명시적 인터페이스 구현(explicit implementation)을 써야 해. 안 그러면 컴파일러가 헷갈려.
GO TO FULL VERSION