CodeGym /행동 /C# SELF /인터페이스 계약

인터페이스 계약

C# SELF
레벨 23, 레슨 3
사용 가능

1. 소개

자, 이제 인터페이스가 진짜 강력한 계약이라는 걸 알았지. 그럼 인터페이스 레벨에서 프로그래밍이 뭔지 궁금하지? 이건 설계 철학인데, 이렇게 말해: "구현체가 아니라 추상화에 의존해라."

다시 비유로 가보자. 너가 커피를 만들고 싶어. 그래서 커피머신을 샀어. 근데 그게 특정 모델 "Super-Puper-Machine V3000"이든 "Mega-Automat Barista 5000"이든 상관없잖아? 중요한 건 그 기계가 "커피를 만든다"는 거지 — 즉, "계약" ICoffeeMaker를 구현한다는 거야. 그냥 "커피 만들기" 버튼 누르면 커피가 나와. 그게 원두든, 캡슐이든, 뭐든 상관없지. 사용자 입장에선 그게 어떻게 돌아가는지 별로 신경 안 써.

코드로 보면, 네 메서드에서 Document나 Image 같은 구체적인 타입을 요구하는 대신, IPrintable을 요구하는 거야. 네 코드는 어떤 객체든 이 인터페이스만 구현하면 타입이 뭔지 몰라도 잘 동작해.

인터페이스 계약 — 이건 어떤 클래스든 인터페이스를 구현하면 그 안의 모든 멤버(메서드, 프로퍼티, 이벤트 등)를 꼭 정의해야 한다는 보장이야. 프로그램 입장에선, 인터페이스를 구현한 객체는 "특정 기능 세트"를 지원한다는 걸 믿고 쓸 수 있어. 내부가 어떻게 생겼는지 몰라도 상관없지.

예를 들어, 너랑 친구가 항상 로비에 있는 빨간 소파에서 만나기로 약속했다고 해보자. 그 친구가 엘리베이터를 타든, 계단을 오르든, 벽을 타고 뛰든 상관없어. 중요한 건 거기 있다는 거지. 인터페이스는 코드에서 "빨간 소파" 같은 거야.

예시


public interface IPrintable
{
    void Print();
}

계약: IPrintable을 구현하는 애는 누구든 자기 자신을 화면에 출력할 수 있어야 해. 그 방법은 각자 알아서.

2. 시스템 파트 간의 상호작용 포인트로서의 계약

실전에서 어떻게 쓰이나

시스템은 여러 명이, 여러 시점에, 여러 목적을 갖고 만든 수십 개의 클래스로 이뤄질 수 있어. 근데 다 같은 계약(즉, 같은 인터페이스)을 구현하면, 전부 똑같이 다룰 수 있지.

실제 앱 예시: 고객 데이터베이스를 생각해봐. Customer 클래스, Employee 클래스, Contractor 클래스가 있어. 각각 자기 방식대로 정보를 저장하지만, 만약 전부 IContactInfo 인터페이스를 구현한다면, 이 인터페이스가 GetEmail()과 GetPhoneNumber() 메서드를 요구한다고 치자. 그러면 외부 코드는 객체 타입이 뭔지 신경 안 써도 되고, 이메일이랑 전화번호만 꺼내면 돼.


public interface IContactInfo
{
    string GetEmail();
    string GetPhoneNumber();
}

public class Customer : IContactInfo
{
    public string Email { get; set; }
    public string Phone { get; set; }

    public string GetEmail() => Email;
    public string GetPhoneNumber() => Phone;
}

// Employee랑 Contractor도 비슷하게...

이제 회사가 연락할 수 있는 모든 사람의 데이터를 출력하려면 (누군지 상관없이) IContactInfo 리스트를 돌면서 필요한 메서드만 호출하면 돼 — 끝.

3. "인터페이스 레벨에서 프로그래밍"이란?

인터페이스 레벨에서 프로그래밍 — 이건 구체적인 클래스가 아니라 인터페이스(즉, 계약)에만 의존해서 코드를 짠다는 뜻이야. 인터페이스만 맞추면, 그걸 구현하는 클래스가 뭐든 상관없어.

왜 중요할까?

  • 확장성: 기존 코드 안 건드리고 새 타입을 쉽게 추가할 수 있어.
  • 테스트 용이성: 테스트할 때 mock(가짜 객체)로 쉽게 바꿔치기 할 수 있어.
  • 유연성: 구현체는 매일 바뀌어도 인터페이스는 그대로라서 코드가 안정적이야.
  • 아키텍처 깔끔함: 모듈끼리 결합도가 낮아서 재사용하기 좋아.

예시 — 익숙한 프로그램: "은행 계좌"

전에 예제 앱에서 BankAccount라는 추상 클래스에 Withdraw()라는 추상 메서드가 있었고, 구체적인 타입(SavingsAccount, CheckingAccount)이 세부 구현을 했었지.

이제 인터페이스를 하나 추가해보자 — 예를 들어, 잔액 정보를 출력하는 용도로:


public interface IBalanceReporter
{
    void ReportBalance();
}

public abstract class BankAccount : IBalanceReporter
{
    public double Balance { get; set; }

    public abstract void Withdraw(double amount);

    public void ReportBalance()
    {
        Console.WriteLine($"현재 잔액: {Balance} 유로");
    }
}

이제 IBalanceReporter로 작업하는 애들은 계좌 타입이 뭔지 몰라도 ReportBalance()만 호출하면 돼.

4. 계약을 통한 공통 처리와 다형성

예시: 공통 핸들러

인터페이스로 작업하면, 객체 타입에 상관없이 쓸 수 있는 범용 메서드를 만들 수 있어:


static void PrintAllBalances(IBalanceReporter[] accounts)
{
    foreach (var reporter in accounts)
    {
        reporter.ReportBalance();
    }
}

이 리스트엔 IBalanceReporter를 구현한 아무 객체나 들어갈 수 있어: SavingsAccount, CheckingAccount, 심지어 MockAccountForTesting 같은 것도. 별다른 트릭 없이 그냥 잘 돌아감.

다이어그램: 인터페이스 계약이 주는 것


+-------------------+      구현함     +-------------------+
|   BankAccount     |  <---------------- |  IBalanceReporter |
|  (SavingsAccount) |                    |-------------------|
|  (CheckingAccount)|                    | + ReportBalance() |
+-------------------+                    +-------------------+
         |                                   ^
         |                                   |
         +-----------+-----------------------+
                     |
        계약을 구현하는 아무 클래스나
다이어그램: 인터페이스 계약이 상호작용 포인트가 됨

5. 계약, 비즈니스 로직, 그리고 아키텍처 설계

인터페이스 계약은 클래스가 무엇을 할 수 있어야 하는지와 어떻게 구현하는지를 명확히 분리해. 그래서 소프트웨어 아키텍트들은 시스템 로직을 인터페이스로 설계하는 걸 좋아하지. 보통 먼저 인터페이스(계약)를 만들고, 구현체는 나중에 붙여.

실생활 예시: 결제 시스템

전자지갑, 카드, PayPal, 암호화폐... 각각 세부사항은 다르지만, 추상화해서 IPaymentProvider 인터페이스를 만들면:


public interface IPaymentProvider
{
    void Pay(decimal amount);
    bool Refund(decimal amount);
}

이 인터페이스로 동작하는 코드는 카드든 계좌든 뭘로 결제하든 신경 안 써. 아키텍처에도, 실제 서비스에도 편하지: 새로운 결제 시스템을 붙여도 기존 코드는 안 건드려도 돼.

6. 비즈니스 로직을 계약으로 추출하기

계약 덕분에 핵심 비즈니스 규칙은 인터페이스 레벨로 빼고, 세부 구현(예: 한도 체크, 캐시백 계산 등)은 각 클래스에 맡길 수 있어.

또 다른 예시


public interface ILogger
{
    void LogInfo(string message);
    void LogError(string message);
}

public class ConsoleLogger : ILogger
{
    public void LogInfo(string message)  => Console.WriteLine($"INFO: {message}");
    public void LogError(string message) => Console.WriteLine($"ERROR: {message}");
}

public class FileLogger : ILogger
{
    public void LogInfo(string message)  => /* 파일에 기록 */;
    public void LogError(string message) => /* 파일에 기록 */;
}

그리고 클라이언트 코드는 이렇게 쓸 수 있어 (어떤 로거를 쓰든 상관없음):


void DoWork(ILogger logger)
{
    logger.LogInfo("작업 시작됨.");
    // ... 뭔가 작업 ...
    logger.LogError("작업이 잘못됨.");
}

이게 바로 인터페이스 레벨에서 프로그래밍이야: 클라이언트 코드는 계약(인터페이스)에만 의존하고, 구체적인 구현체엔 신경 안 써.

7. 실수와 흔한 함정

초보자들이 자주 하는 실수: 코드를 구체적인 클래스에 딱 붙여서 짜는 거야, 인터페이스가 아니라. 그러면 이런 문제가 생겨:

  • 뭔가 바꾸거나 테스트하려면 코드 엄청 많이 고쳐야 해.
  • 새 타입을 추가하려면 여기저기 다 수정해야 해.
  • 모듈끼리 너무 "붙어" 있어서 유연성이 떨어져.

반대로, 처음부터 인터페이스로 시스템을 짜고, 파라미터도 인터페이스로 넘기면 결합도 낮고 유연한 구조가 돼.

핵심 팁: 모듈 간 상호작용을 "구현체"가 아니라 "계약"끼리의 상호작용으로 생각해봐.

2
과제
C# SELF, 레벨 23, 레슨 3
잠금
인터페이스 수준에서의 프로그래밍
인터페이스 수준에서의 프로그래밍
2
과제
C# SELF, 레벨 23, 레슨 3
잠금
인터페이스를 통한 범용 리포트 처리기
인터페이스를 통한 범용 리포트 처리기
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION