CodeGym /행동 /C# SELF /인터페이스 실제 사용법

인터페이스 실제 사용법

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

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();
        }
    }
}

플러그인은 다른 개발자가 만들어도 됨 — 인터페이스만 맞추면 돼. 네 앱은 확장 가능해지는 거지!

1
설문조사/퀴즈
인터페이스 사용하기, 레벨 24, 레슨 4
사용 불가능
인터페이스 사용하기
고급 인터페이스
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION