1. 인터페이스란?
추상 클래스가 일종의 "반제품" 템플릿이라면, 인터페이스는 그냥 요구사항 리스트야. 어떤 존재(클래스)가 어떤 걸 할 수 있어야 하는지, 그걸로 추상적인 상황에서 쓸 수 있는지 결정하는 거지.
프로그래밍에서 인터페이스는 일종의 "계약서" 또는 "요구사항 리스트" 같은 거야. 객체가 반드시 제공해야 하는 public 메서드, 프로퍼티, 인덱서, 이벤트 목록을 정의하지. 인터페이스는 이렇게 말하는 거지: "너가 예를 들어 IDriveable (운전 가능)이라고 불리고 싶으면, Drive()랑 Stop() 메서드는 꼭 만들어라."
인터페이스를 직원 요구사항 리스트라고 생각해봐. 예를 들어 "요리사"를 뽑고 싶으면, 인터페이스에는 "요리할 줄 알아야 함, 음식 내놓을 줄 알아야 함, 손 씻을 줄 알아야 함" 이런 게 적혀있겠지. 실제로 그 요리사가 어떻게 할지는 그 사람 마음이야. 중요한 건 겉으로 봤을 때 계약대로만 하면 된다는 거지.
중요: 인터페이스는 무엇을 제공해야 하는지만 정의하고, 어떻게 하는지는 신경 안 써.
OOP 용어로 간단히
- 인터페이스는 객체의 외부 "모습"(API), 즉 어떤 동작을 할 수 있는지 정의해.
- 상태(필드)는 없음. 전통적으로 인터페이스는 구현도 없지만, 최신 C#에서는 예외(예: 기본 메서드)도 있어. 이건 나중에 자세히 다룰게.
- 클래스는 인터페이스를 몇 개든 구현할 수 있어(클래스 상속과 다르게!).
실생활 비유
USB 포트 — 다들 알지? 마우스, 키보드, USB, 이어폰, 심지어 커피머신도 연결할 수 있어(진짜야!). 마우스 내부가 어떻게 생겼는지는 상관없고, USB 포트랑 프로토콜만 맞으면 돼. 이게 바로 인터페이스야!
인터페이스가 왜 필요할까?
- 결합도 낮추기. 코드는 인터페이스랑만 일하고, 구체적인 클래스는 몰라도 돼. "인터페이스 수준"에서 프로그래밍하는 거지.
- 여러 동작 상속 가능: 클래스가 여러 인터페이스를 구현할 수 있어.
- 표준화: 예를 들어, 비교 가능한 모든 객체는 IComparable 인터페이스를 구현하게 할 수 있어.
- 테스트 용이: 실제 구현 대신 "가짜"로 쉽게 바꿔서 테스트할 수 있어.
- 플러그인 확장: 기존 코드 안 건드리고 새 "모듈"을 추가할 수 있어.
2. 인터페이스 선언 문법
이제 코드로 가보자! C#에서 인터페이스는 interface 키워드로 선언하고, 이름은 보통 I로 시작해 :
// 인터페이스 선언
public interface IPrintable
{
// 추상 메서드 — 계약
void Print();
// 프로퍼티도 선언 가능
string Name { get; set; }
}
기억해!: 인터페이스의 메서드와 프로퍼티는 구현이 없어(메서드 몸체 없이 시그니처만 — 추상 메서드랑 똑같이).
인터페이스의 주요 특징
- 인터페이스 안에 필드(멤버 변수) 선언 불가.
- 모든 메서드, 프로퍼티, 이벤트, 인덱서는 기본적으로 public (구현할 때도 마찬가지).
- 인터페이스에는 생성자 못 넣어(상태가 없으니까).
- 인터페이스를 구현하는 클래스가 추상이든 구체적이든 상관없어.
인터페이스 실전 예시
우리 예제 앱에 인터페이스를 넣어보자. 이제 "출력 가능한" (IPrintable) 인터페이스가 있고, 이걸 Report 클래스랑 새로 만든 Invoice 클래스가 구현한다고 해보자.
public interface IPrintable
{
void Print();
string Name { get; set; }
}
이제 이 인터페이스를 구현하는 클래스를 정의해보자:
public class Report : IPrintable
{
public string Name { get; set; }
public Report(string name)
{
Name = name;
}
// 인터페이스 메서드 구현
public void Print()
{
Console.WriteLine($"보고서 출력: {Name}");
}
}
이번엔 완전히 다른 클래스지만 같은 인터페이스:
public class Invoice : IPrintable
{
public string Name { get; set; }
public Invoice(string name)
{
Name = name;
}
public void Print()
{
Console.WriteLine($"계산서 출력: {Name}");
}
}
이제 어떤 출력 가능한 객체든 처리하는 메서드를 쓸 수 있어:
public static void PrintAnything(IPrintable printable)
{
printable.Print(); // 끝! 보고서든 계산서든 출력만 할 수 있으면 돼.
}
사용 예시도 볼까:
var report = new Report("월간 보고서");
var invoice = new Invoice("계산서 #12345");
PrintAnything(report); // 보고서 출력: 월간 보고서
PrintAnything(invoice); // 계산서 출력: 계산서 #12345
이렇게 인터페이스 덕분에 범용적이고 확장성 좋은, 예쁜 코드를 쓸 수 있어.
3. 인터페이스 구현
클래스에 인터페이스 붙이기
클래스에서 인터페이스 구현은 콜론(:)으로 해(상속이랑 똑같아):
public class Ticket : IPrintable
{
public string Name { get; set; }
public void Print()
{
Console.WriteLine($"티켓 출력: {Name}");
}
}
중요: 클래스는 인터페이스의 모든 멤버를 반드시 구현해야 해. 그리고 구현한 멤버는 public이어야 해.
하나라도 안 구현하면:
public class BrokenTicket : IPrintable
{
// Print() 구현 빠짐
public string Name { get; set; }
}
// 컴파일 에러: 'BrokenTicket'은(는) 인터페이스 멤버 'IPrintable.Print()'를 구현하지 않음
여러 인터페이스
클래스는 인터페이스 여러 개를 콤마로 구현할 수 있어:
public interface IStorable
{
void Store();
}
public class MultiPurposeDoc : IPrintable, IStorable
{
public string Name { get; set; }
public void Print()
{
Console.WriteLine("문서 출력");
}
public void Store()
{
Console.WriteLine("문서 저장");
}
}
4. 인터페이스가 왜 필요할까?
지금 머릿속에 "음, 문법은 알겠는데, 이게 실제로 왜 필요하지? 그냥 내 인생만 복잡하게 만드는 거 아냐?" 이런 생각 들 수도 있어. 근데 진짜야, 인터페이스는 실무에서 제일 많이 쓰는 도구 중 하나야.
책임 분리와 낮은 결합도(Decoupling / Loose Coupling):
- 음악 플레이어를 만든다고 해보자. 음악이 어디서 오든(로컬 파일, 인터넷, CD 등) 상관없고, 중요한 건 음악 소스가 오디오 스트림만 제공하면 돼.
- IAudioSource 인터페이스에 GetAudioStream() 메서드를 정의할 수 있지.
- 그럼 FileAudioSource, InternetAudioSource, CDAudioSource 같은 클래스들이 이 인터페이스를 구현할 수 있어.
- 플레이어는 IAudioSource만 알면 되고, 구체적인 타입은 몰라도 돼. 내일 BluetoothAudioSource 같은 새 소스가 생겨도 플레이어 코드는 안 바꿔도 돼! 그냥 IAudioSource 구현하는 새 클래스를 만들면 끝. 이렇게 하면 시스템이 훨씬 유연하고 확장성 좋아져. 이게 바로 낮은 결합도 — 컴포넌트가 구체적인 구현이 아니라 추상(인터페이스)에만 의존하는 거야.
다형성과 일관된 처리:
아까 PrintAnything 예시처럼, 타입이 달라도 인터페이스로 공통 동작을 묶을 수 있어. 그럼 Print() 메서드를 어떤 객체든 똑같이 호출할 수 있지. 이게 바로 간결하고 범용적인 코드를 만드는 방법이야.
테스트(Unit Testing):
이건 인터페이스의 제일 중요한 용도 중 하나야. 어떤 컴포넌트를 테스트할 때, 보통 다른 컴포넌트가 필요하지(예: 데이터 저장 클래스가 DB 작업 클래스에 의존).
진짜 DatabaseSaver 클래스를 넘기면(테스트하려면 진짜 DB가 필요하니까!) 불편하지. 대신 IDataSaver 인터페이스만 구현한 "가짜"(혹은 "mock") 객체를 넘길 수 있어. 이 mock 객체는 실제 DB에 접근 안 하고, 저장하는 척만 해. 이렇게 하면 컴포넌트를 독립적으로, 빠르고 외부 의존성 없이 테스트할 수 있어.
API와 프레임워크 개발:
라이브러리나 프레임워크를 만들 때, 개발자에게 "확장 포인트"를 주고 싶잖아. 인터페이스가 딱이야. "내 시스템이랑 연동하려면 이 인터페이스만 구현해!"라고 할 수 있지. .NET 표준 라이브러리에도 IEnumerable<T>, IDisposable, IComparable<T> 같은 인터페이스가 엄청 많아 — 대표적인 계약서들이지.
인터페이스 수준 프로그래밍(Programming to an Interface):
경험 많은 개발자들은 항상 이렇게 말해: "구현이 아니라 인터페이스에 맞춰 프로그래밍해라." 즉, 변수나 메서드 파라미터 타입을 구체적인 클래스(Car) 대신 인터페이스(IDriveable)로 쓰라는 거지. 이렇게 하면 코드가 더 유연하고, 구현 바꿀 때도 쉽게 바꿀 수 있어.
5. 인터페이스 작업 시 흔한 실수
실수 1: 인터페이스 인스턴스 생성 시도.
Cat murzik = new Cat("무르지크", 3); 이렇게는 돼. 왜냐면 Cat은 구체적인 클래스니까. 하지만 ITalkable talker = new ITalkable(); 이렇게는 절대 안 돼. 인터페이스는 계약서, 템플릿일 뿐이야. 구현이 없으니 직접 만들 수 없어. 설계도지, 완성된 집이 아니야.
실수 2: 인터페이스 멤버 구현 누락.
클래스가 IMyInterface를 구현한다고 했으면, 모든 메서드를 꼭 구현해야 해. 하나라도 빠지면 컴파일 에러: MyClass가 IMyInterface.TheMissingMethod()를 구현하지 않았다고 나와.
실수 3: 접근 제한자 잘못 사용.
인터페이스 메서드는 암묵적으로 public이야. 구현할 때도 public이어야 해. private이나 protected로 하면 컴파일 에러. 약속했으면 공개적으로 구현해야지.
실수 4: 인터페이스에 필드나 생성자 추가 시도.
인터페이스는 동작만 정의하고, 상태는 안 다뤄. 그래서 필드나 생성자 추가하면 안 돼. 시도하면 컴파일 에러. 프로퍼티는 가능하지만, getter/setter만 정의하는 거야.
실수 5: override와 인터페이스 구현 혼동.
override는 부모 클래스 메서드 재정의할 때 쓰는 거야. 인터페이스 구현할 때는 필요 없어 — 그냥 public 메서드로 시그니처만 맞추면 돼. 이거 헷갈리기 쉬우니까 꼭 기억해.
GO TO FULL VERSION