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. 실수와 흔한 함정
초보자들이 자주 하는 실수: 코드를 구체적인 클래스에 딱 붙여서 짜는 거야, 인터페이스가 아니라. 그러면 이런 문제가 생겨:
- 뭔가 바꾸거나 테스트하려면 코드 엄청 많이 고쳐야 해.
- 새 타입을 추가하려면 여기저기 다 수정해야 해.
- 모듈끼리 너무 "붙어" 있어서 유연성이 떨어져.
반대로, 처음부터 인터페이스로 시스템을 짜고, 파라미터도 인터페이스로 넘기면 결합도 낮고 유연한 구조가 돼.
핵심 팁: 모듈 간 상호작용을 "구현체"가 아니라 "계약"끼리의 상호작용으로 생각해봐.
GO TO FULL VERSION