Program/C#

C# _ Interface

사막여유 2024. 11. 24. 21:42
728x90

오늘은 c#에 있는 Interface 키워드에 대해서  대해서 알고보고자 합니다.

 

목차

  • 인터페이스란?
    • 인터페이스의 기본 개념
    • 클래스와 인터페이스의 차이점
  • 인터페이스 구현하기
    • 기본 구현방법
    • 다중 인터페이스 구현 예시
    • 인터페이스 구현 시 주의사항
  • 실무에서 자주 사용되는 인터페이스 패턴
    • 의존성주입(DI)에서의 활용
    • Repository 패턴 예시
    • 전략 패턴 구현하기
  • C# 8.0 이후의 인터페이스 새로운 기능
    • 기본 구현 메서드
    • 정적 메서드
    • 프라이빗 메서드

 

 

인터페이스란?

 

인터페이스 기본 개념

인터페이스는 조금 추상적으로 느껴질 수 있지만, 실제로는 매우 실용적인 개념입니다.

쉽게 말해서 인터페이스는 '이런 기능은 반드시 있어야 해!' 라고 정의해놓은 설계도와 같습니다.

 

기본적인 인터페이스 코드를 한번 보겠습니다.

 

public interface IAnimal
{
    string Name { get; set; }
    void MakeSound();
    void Move();
}

 

위에서 인터페이스명을 I로 먼저 작성한 뒤 IAnimal 로 설정하듯이
C#에서 인터페이스를 정의하고 사용할 때는 명명규칙처럼 대문자 I로 시작하는 것이 관례입니다.

위에서 설명한대로 "이런 기능은 반드시 있어야해!"라고 했기 때문에 동물이라는 인터페이스를 만들게되면,
동물이라고 하면 대부분 소리를내고 움직이기 때문에 MakeSound() 메서드와 Move() 메서드가 반드시 필요하게 되는거죠.

 

클래스와 인터페이스의 차이점

 

얼핏보면 인터페이스의 구조는 클래스와 큰 차이가 없어 보이지만 크게 3가지의 차이점때문에 사용되고 있습니다.

1. 구현 여부
2. 다중 상속
3. 멤버 제한

 

첫번째로 구현여부 입니다.

클래스는 메서드를 정의한 이후 해당 메서드에 대한 구현을 하지만 
인터페이스는 클래스와 다르게 메서드를 실제로 구현하지 않습니다.

// 데이터베이스 작업을 위한 인터페이스
public interface IUserRepository
{
    User GetById(int id);
    void Save(User user);
}

// SQL Server를 사용하는 구현체
public class SqlUserRepository : IUserRepository
{
    public User GetById(int id)
    {
        // SQL Server 데이터베이스에서 사용자 조회
        return new User(); 
    }

    public void Save(User user)
    {
        // SQL Server 데이터베이스에 사용자 저장
    }
}

// MongoDB를 사용하는 구현체
public class MongoUserRepository : IUserRepository
{
    public User GetById(int id)
    {
        // MongoDB에서 사용자 조회
        return new User();
    }

    public void Save(User user)
    {
        // MongoDB에 사용자 저장
    }
}

 

구현부가 없는 이유는 위 코드에서 볼 수 있듯이 같은 메서드명을 가졌지만 실제 구현내용은 다를수도 있기 때문입니다.

예를들면 아래 이미지와 같습니다.

 

한가지의 목적을 가진것은 동일하지만 그 목적을 달성하기 위한 방법이다 다를 수 있기 때문에 구현을 하지 않는 것이죠.

 

만약 기후협약(인터페이스)에서 "이산화탄소를 이렇게 줄여야 한다"라고 구체적인 방법까지 정해버리면, 각 나라의 상황에 맞지 않을 수 있고 실제로 적용하기 어려울 수 있습니다.

 

대신 "이산화탄소를 10% 줄인다"라는 목표만 정해두면:

  1. 각 나라는 자신의 상황에 맞는 방식을 선택할 수 있고
  2. 다른 나라의 성공적인 방식을 참고해서 적용할 수도 있고
  3. 새로운 나라가 협약에 참여할 때도 유연하게 대응할 수 있습니다

프로그램에서도 마찬가지입니다:

  • 인터페이스는 "무엇을 해야 하는지"만 정의함으로써
  • 다양한 클래스들이 자신만의 방식으로 구현할 수 있고
  • 새로운 클래스가 추가될 때도 기존 코드를 변경하지 않아도 됩니다
  • 이렇게 코드의 재사용성과 유연성을 높일 수 있습니다.

즉, 인터페이스는 구현부를 비워둠으로써 오히려 더 많은 상황에서 재사용할 수 있게 되는 것입니다.

 

 

두번째로는 다중 상속입니다.

클래스는 한 개의 클래스만 상속받을 수 있는 반면, 인터페이스는 여러 개를 동시에 구현할 수 있습니다. 이러한 다중 상속이 필요한 실제 예를 살펴보겠습니다.

// 클래스 단일 상속의 한계
public class Bird { }
public class Fish { }

// 펭귄은 새이면서 수영도 가능해야 하는데...
// 아래 코드는 불가능합니다!
public class Penguin : Bird, Fish { } // 컴파일 에러!

// 인터페이스로 해결
public interface IFlyable
{
    void Fly();
}

public interface ISwimmable
{
    void Swim();
}

// 펭귄은 수영만 가능하도록 구현
public class Penguin : ISwimmable
{
    public void Swim() 
    { 
        Console.WriteLine("펭귄이 수영합니다"); 
    }
}

// 독수리는 나는 것만 가능하도록 구현
public class Eagle : IFlyable
{
    public void Fly() 
    { 
        Console.WriteLine("독수리가 납니다"); 
    }
}

// 오리는 수영과 비행 모두 가능하도록 구현
public class Duck : IFlyable, ISwimmable
{
    public void Fly() 
    { 
        Console.WriteLine("오리가 납니다"); 
    }
    
    public void Swim() 
    { 
        Console.WriteLine("오리가 수영합니다"); 
    }
}

 

이렇게 다중 상속이 필요한 이유는 아래와 같습니다.

  • 하나의 클래스가 여러 가지 능력이나 특성을 가질 수 있게 해줍니다.
  • 코드 재사용성이 높아지고, 기능을 조합하여 새로운 클래스를 만들 수 있습니다.
  • 각 기능을 독립적으로 정의하고 관리할 수 있습니다.

 

그런데 사실 이렇게 구현되었을 때 인터페이스에 정의된 메서드가 많아지게되면 
해당 인터페이스를 상속받아 만들어진 다중상속클래스에서 필요없는 메서드들도 불가피하게 구현해야하기 때문에 
문제가 발생할 수 있습니다.

따라서 이 때 필요한 개념이 "인터페이스 분리 원칙 ( Interface Segregation Pirnciple, ISP) " 입니다.

 

예를들어 잘못된 인터페이스 설계를 보게 되면

// 좋지 않은 예 - 너무 많은 기능이 하나의 인터페이스에 있는 경우
public interface IUserManager
{
    void CreateUser(User user);
    void DeleteUser(int userId);
    void UpdateUser(User user);
    void SendEmail(string email);
    void SendSMS(string phoneNumber);
    void GenerateReport();
    void ExportToExcel();
    void ExportToPDF();
    void ValidateUserData();
    void ProcessPayment(decimal amount);
    // ... 더 많은 메서드들
}

// 이 클래스는 사용하지 않는 메서드도 모두 구현해야 함
public class SimpleUserManager : IUserManager
{
    public void CreateUser(User user) { /* 구현 */ }
    public void DeleteUser(int userId) { /* 구현 */ }
    public void UpdateUser(User user) { /* 구현 */ }
    public void SendEmail(string email) 
    { 
        throw new NotImplementedException(); // 실제로는 필요 없는 기능
    }
    public void SendSMS(string phoneNumber) 
    { 
        throw new NotImplementedException(); // 실제로는 필요 없는 기능
    }
    // ... 필요 없는 나머지 메서드들도 모두 구현해야 함
}

 

위 코드와 같이 잘못된 설계의 인터페이스를 상속받은 클래스는 실제 사용하지도 않는 메서드들까지 모두 구현해야합니다.

// 좋은 예 - 기능별로 인터페이스를 분리
public interface IUserBasicManager
{
    void CreateUser(User user);
    void DeleteUser(int userId);
    void UpdateUser(User user);
}

public interface IUserNotificationManager
{
    void SendEmail(string email);
    void SendSMS(string phoneNumber);
}

public interface IUserReportManager
{
    void GenerateReport();
    void ExportToExcel();
    void ExportToPDF();
}

public interface IUserPaymentManager
{
    void ProcessPayment(decimal amount);
    void RefundPayment(string transactionId);
}

// 필요한 기능만 구현 가능
public class SimpleUserManager : IUserBasicManager
{
    public void CreateUser(User user) { /* 구현 */ }
    public void DeleteUser(int userId) { /* 구현 */ }
    public void UpdateUser(User user) { /* 구현 */ }
}

// 알림 기능이 필요한 클래스
public class NotificationUserManager : IUserBasicManager, IUserNotificationManager
{
    public void CreateUser(User user) { /* 구현 */ }
    public void DeleteUser(int userId) { /* 구현 */ }
    public void UpdateUser(User user) { /* 구현 */ }
    public void SendEmail(string email) { /* 구현 */ }
    public void SendSMS(string phoneNumber) { /* 구현 */ }
}

 

하지만 이렇게 애초에 설계가 잘 되어있는 인터페이스는 상속받게 될 클래스를 깔끔하고 유지보수하기 쉬워지게 만들 수 있게 도와줍니다.

 

그렇기 때문에 인터페이스를 구현할때에는 어떠한 역할을 할 것인지 에대한 구조가 철저하고 명확하게 설계되어야 합니다.

 

세 번째로는 멤버 제한입니다.

클래스는 필드, 생성자를 포함하여 메서드, 프로퍼티, 이벤트 등을 가질 수 있지만,
인터페이스는 필드, 생성자를 제외한 메서드, 프로퍼티, 이벤트, 인덱서만을 가질 수 있습니다.

이 맴버 제한에 대한 장점은 아래와 같습니다.

  • 구현 클래스의 자유도가 높아집니다. 필드나 생성자가 강제되지 않아 클래스가 자신만의 상태 관리 방식을 가질 수 이 있습니다.
  • 인터페이스가 "무엇을 해야 하는가" 에만 집중할 수 있게 해줍니다.
  • 테스트가 용이해집니다. 필드나 생서자 없이 동작만 정의되어 있기 때문에 Mock 객체 생성이 쉽습니다.

 

 

실무에서 자주 사용되는 인터페이스 패턴

 

실무에서 자주 사용되는 인터페이스 패턴은 아래 3가지 입니다.

1. Repository 패턴
2. 전략패턴
3. 의존성 주입 (DI) 활용

 

첫 번째로는 Repository 패턴입니다.

두 번째로는 전략 패턴입니다.

전략 패턴 전략 패턴은 알고리즘군을 정의하고 각각을 캡슐화하여 교체 가능하게 만드는 패턴입니다.
런타임에 알고리즘을 선택하여 사용할 수 있게 해줍니다.

 

전략 패턴을 사용하면 좋은 상황으로는 아래와 같습니다.

  • 비슷한 동작을 하지만 다르게 구현되어야 하는 경우
  • 런타임에 알고리즘을 선택해야 하는 경우
  • 조건문이 많이 사용되는 코드를 리팩토링할 때

 

 

public interface IDiscountStrategy
{
    decimal CalculateDiscount(decimal amount);
}

public class BlackFridayDiscount : IDiscountStrategy
{
    public decimal CalculateDiscount(decimal amount)
    {
        return amount * 0.3m; // 30% 할인
    }
}

public class PriceCalculator
{
    private readonly IDiscountStrategy _discountStrategy;
    
    public PriceCalculator(IDiscountStrategy discountStrategy)
    {
        _discountStrategy = discountStrategy;
    }
    
    public decimal CalculateFinalPrice(decimal originalPrice)
    {
        return originalPrice - _discountStrategy.CalculateDiscount(originalPrice);
    }
}

 

이와같이 전략패턴을 사용하게 될때의 장점으로는

  • 새로운 전략을 추가할 때 기존 코드를 변경하지 않아도 됩니다
  • 조건문 대신 객체를 사용하여 코드가 깔끔해집니다
  • 각 알고리즘을 독립적으로 테스트할 수 있습니다

 

 

 

 

세번째로는 의존성 주입입니다.

의존성 주입(DI) 활용 의존성 주입은 한 객체가 다른 객체를 직접 생성하는 대신, 외부에서 필요한 객체를 주입받아 사용하는 방식입니다.

728x90

'Program > C#' 카테고리의 다른 글

C# 유닛(단위)테스트(1)  (0) 2024.12.09
C# 디스크I/O  (1) 2024.12.07
C# 병렬처리3 _ Mutex  (0) 2024.11.23
C# 병렬처리2 _ Monitor  (0) 2024.11.22
C# 병렬처리1 _ lock  (0) 2024.11.20