1. 비즈니스 로직에서의 인터페이스: 버튼과 액션
이건 인터페이스에 대한 마지막 강의라서 실전 예제 엄청 많아. 실제로 인터페이스가 어떻게 쓰이는지 감 잡을 수 있을 거야.
인터페이스는 특히 큰 앱에서 진짜 쓸모 있어. 그래서 아래에는 좀 복잡한 케이스도 있어, 가끔은 커리큘럼보다 앞서나갈 수도 있음. 흥미 있으면 쭉 봐! 아니면 다음 레벨로 넘어가도 돼 :P
제일 쉬운 예시 — 버튼과 클릭
우리 콘솔 학습 앱을 계속 발전시켜보자 — 간단한 사용자 UI 시스템(버튼, 텍스트 필드, 토글 등)이 콘솔 메뉴에 있다고 상상해봐.
어떤 요소는 클릭에 반응해야 하고(버튼), 어떤 건 반응 안 해도 돼(예: 고정된 제목). 인터페이스로 이런 시나리오를 깔끔하게 구현할 수 있어.
1단계: 인터페이스 만들기
// "클릭"할 수 있는 컴포넌트:
public interface IClickable
{
void Click();
}
2단계: 다양한 요소에 인터페이스 적용하기
// 모든 메뉴 요소의 기본 클래스
public class MenuItem
{
public string Title { get; set; }
public MenuItem(string title)
{
Title = title;
}
public virtual void Display()
{
Console.WriteLine(Title);
}
}
// 버튼: "클릭" 가능
public class Button : MenuItem, IClickable
{
public Button(string title) : base(title) {}
public void Click()
{
Console.WriteLine($"[버튼] {Title} 눌렸어!");
}
}
// 그냥 라벨: 클릭 불가
public class Label : MenuItem
{
public Label(string title) : base(title) {}
}
3단계: 인터페이스 다형성 사용하기
IClickable[] clickableItems = new IClickable[]
{
new Button("저장"),
new Button("종료")
// Label은 여기 못 들어감 — 얘는 IClickable 아님!
};
foreach (var item in clickableItems)
{
item.Click();
}
이거 신기하지? 리스트에는 “Click” 할 줄 아는 애들만 들어가니까, 잘못된 객체에 “클릭” 시도해도 런타임 에러 안 나.
2. 인터페이스와 “전략” 패턴: 알고리즘을 런타임에 선택하기
예를 들어, 보고서를 여러 방식으로 저장할 수 있는 앱을 만든다고 해보자: 파일, 데이터베이스, “클라우드”, 아니면 Telegram으로 사장님한테 보내는 것도 가능(묻지 마, 별일 다 있음).
1단계: 문제 정의
ReportGenerator 클래스가 저장 방식의 세부 구현을 몰라도 동작해야 해.
2단계: 전략 인터페이스 정의
public interface IDataSaver
{
void Save(string reportData);
}
3단계: 다양한 구현체 만들기
public class FileDataSaver : IDataSaver
{
public void Save(string reportData)
{
Console.WriteLine("[FileDataSaver] 파일에 저장 중...\n" + reportData);
// 여기에 File.WriteAllText(...) 코드 들어갈 수 있음
}
}
public class DatabaseDataSaver : IDataSaver
{
public void Save(string reportData)
{
Console.WriteLine("[DatabaseDataSaver] 데이터베이스에 저장 중...\n" + reportData);
// 여기에 DB 저장 코드 들어갈 수 있음
}
}
4단계: 저장 전략 바꾸기 위해 인터페이스 사용
public class ReportGenerator
{
private readonly IDataSaver _dataSaver;
public ReportGenerator(IDataSaver dataSaver)
{
_dataSaver = dataSaver;
}
public void GenerateReport()
{
string report = "이거 중요한 보고서야!";
Console.WriteLine("보고서 생성 중...");
_dataSaver.Save(report);
}
}
유연성 시연
// ReportGenerator 코드를 안 바꿔도 저장 방식만 바꿀 수 있음!
IDataSaver fileSaver = new FileDataSaver();
ReportGenerator fileReport = new ReportGenerator(fileSaver);
fileReport.GenerateReport();
IDataSaver dbSaver = new DatabaseDataSaver();
ReportGenerator dbReport = new ReportGenerator(dbSaver);
dbReport.GenerateReport();
실전에서 유용한 점: 내일 갑자기 새로운 초현대식 서비스에 보고서 저장해야 한다면, 그냥 인터페이스 구현한 새 클래스만 만들고 객체만 바꿔주면 돼. 기존 코드는 한 줄도 안 건드려도 됨.
3. .NET에서의 인터페이스: IDisposable과 using
.NET에서 제일 많이 쓰는 인터페이스 중 하나가 IDisposable이야. 파일, 스트림, 네트워크 연결 등 관리되지 않는 리소스 다루는 모든 클래스가 이걸 구현해.
IDisposable이 왜 필요할까?
꼭 해제해야 하는 리소스(예: 파일 닫기)를 쓸 때 IDisposable 구현하고 Dispose 메서드 작성해. 그러면 using 구문에 객체를 넘길 수 있고, 블록을 벗어나면 Dispose가 자동 호출돼.
예시: 파일 작업 시뮬레이션
public class FakeFile : IDisposable
{
public string FileName { get; }
public FakeFile(string fileName)
{
FileName = fileName;
Console.WriteLine($"파일 열림: {fileName}");
}
public void Dispose()
{
Console.WriteLine($"파일 닫힘: {FileName}");
}
}
// 메인 프로그램에서:
using (var file = new FakeFile("otchet.txt"))
{
Console.WriteLine("파일에 쓰는 중...");
// using 끝나면 파일이 자동으로 "닫힘"
}
출력:
파일 열림: otchet.txt
파일에 쓰는 중...
파일 닫힘: otchet.txt
진짜 유용함: 파일 닫는 거 까먹을 일 없음 — 인터페이스랑 언어 인프라가 알아서 해줌.
4. 컬렉션과 LINQ에서의 인터페이스
리스트, 배열, 딕셔너리 쓸 때 이미 인터페이스를 쓰고 있는 거야, 의식 못 해도.
List<int> list = new List<int> { 1, 2, 3 };
IEnumerable<int> enumerable = list; // 문제 없음!
// 이제 이런 식으로 요소 순회 가능:
foreach(var x in enumerable)
{
Console.WriteLine(x);
}
LINQ 메서드 대부분이 IEnumerable<T> 인터페이스로 컬렉션을 다뤄. 그래서 컬렉션 타입에 상관없이 코드를 쓸 수 있어.
이게 왜 필요할까?
List<T> 대신 T[], HashSet<T> 또는 네가 만든 컬렉션으로 바꿔도 코드가 계속 잘 돌아감!
5. 테스트용 인터페이스(Mock 객체)
테스트에서는 외부 의존성에서 코드를 분리하는 게 중요해: 진짜 DB나 파일 건드리지 않기. 인터페이스 덕분에 mock 객체(가짜 객체)로 테스트할 수 있어서 실제 DB나 파일 안 건드려도 돼.
public class FakeDataSaver : IDataSaver
{
public bool WasCalled { get; private set; } = false;
public void Save(string reportData)
{
WasCalled = true;
Console.WriteLine("mock 객체에 데이터 저장됨!");
}
}
// 테스트에서
FakeDataSaver saver = new FakeDataSaver();
ReportGenerator generator = new ReportGenerator(saver);
generator.GenerateReport();
Console.WriteLine($"Save 호출됨? {saver.WasCalled}");
결과: 테스트가 실제 환경에 의존하지 않고, 필요한 메서드가 호출됐는지만 확인할 수 있어!
6. 충돌 해결을 위한 명시적 인터페이스 구현
가끔 클래스가 서로 다른 인터페이스에서 같은 이름의 메서드를 구현해야 할 때가 있어. 근데 그 메서드 로직은 다를 수 있지.
public interface IFlyable
{
void Move();
}
public interface ISwimmable
{
void Move();
}
public class Duck : IFlyable, ISwimmable
{
// 명시적 구현
void IFlyable.Move()
{
Console.WriteLine("오리가 날아!");
}
void ISwimmable.Move()
{
Console.WriteLine("오리가 헤엄쳐!");
}
}
Duck duck = new Duck();
// duck.Move(); // 에러: 그런 메서드 없음!
((IFlyable)duck).Move(); // "오리가 날아!"
((ISwimmable)duck).Move(); // "오리가 헤엄쳐!"
좀 빡세 보일 수 있지만, 가끔은 이렇게 해야 할 때도 있어!
7. 이벤트 모델에서의 인터페이스: INotifyPropertyChanged
.NET에는 이벤트 지원용 표준 인터페이스가 있어 — 예를 들어, 데이터 모델에서 뭔가 바뀌면 UI가 그걸 알아야 할 때(특히 WPF, MAUI, 기타 GUI에서 자주 씀).
using System.ComponentModel;
public class Person : INotifyPropertyChanged
{
private string name;
public string Name
{
get => name;
set
{
if (name != value)
{
name = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
}
}
}
public event PropertyChangedEventHandler? PropertyChanged;
}
의미: 데이터 바인딩 지원하는 프레임워크는 네 클래스가 이 인터페이스 구현하길 기대해 — 그러면 속성 바뀔 때마다 UI가 자동으로 갱신됨.
8. “팩토리” 패턴에서의 인터페이스
인터페이스는 팩토리(여러 종류의, 서로 대체 가능한 객체를 만들어주는 클래스) 만들 때도 딱이야.
public interface ITransport
{
void Move();
}
public class Bicycle : ITransport
{
public void Move() => Console.WriteLine("자전거 타고 가!");
}
public class Car : ITransport
{
public void Move() => Console.WriteLine("자동차 타고 가!");
}
public class TransportFactory
{
public static ITransport Create(string type)
{
return type switch
{
"bike" => new Bicycle(),
"car" => new Car(),
_ => throw new ArgumentException("알 수 없는 교통수단 타입")
};
}
}
ITransport transport = TransportFactory.Create("bike");
transport.Move(); // "자전거 타고 가!"
9. 이벤트용 인터페이스: 직접 만든 이벤트 예시
이벤트 “리스너” 인터페이스를 만들어보자:
public interface ILoginListener
{
void OnLogin(string userName);
}
// 이벤트를 발생시키는 클래스
public class LoginManager
{
private List<ILoginListener> listeners = new();
public void Subscribe(ILoginListener listener) => listeners.Add(listener);
public void Login(string userName)
{
Console.WriteLine($"사용자 {userName} 로그인함.");
foreach (var listener in listeners)
listener.OnLogin(userName);
}
}
// 인터페이스 구현 클래스
public class WelcomeMessage : ILoginListener
{
public void OnLogin(string userName)
{
Console.WriteLine($"환영합니다, {userName}!");
}
}
LoginManager manager = new();
manager.Subscribe(new WelcomeMessage());
manager.Login("Vasya");
// 사용자 Vasya 로그인함.
// 환영합니다, Vasya!
면접에서: “직접 이벤트 시스템 만든다면 어떻게 할 거냐?”고 물으면, 리스너 인터페이스 얘기하면 됨!
10. 플러그인용 인터페이스(앱 확장성)
대형 앱은 플러그인 지원하는 경우가 많아. 인터페이스 덕분에 네 앱이 새로운 모듈을 “즉석에서” 불러와서 실행할 수 있어, 내부 구조 몰라도 됨.
public interface IPlugin
{
string Name { get; }
void Run();
}
// 네 앱:
public class PluginLoader
{
public void LoadAndRun(IEnumerable<IPlugin> plugins)
{
foreach (var plugin in plugins)
{
Console.WriteLine($"플러그인 실행: {plugin.Name}");
plugin.Run();
}
}
}
플러그인은 다른 개발자가 만들어도 됨 — 인터페이스만 맞추면 돼. 네 앱은 확장 가능해지는 거지!
GO TO FULL VERSION