개발자공부일기

Interface 본문

C#

Interface

JavaCPP 2025. 3. 24. 17:09

C#에서 인터페이스 (Interface)를 사용하는 이유

1. 다형성 (Polymorphism) 제공

  • 인터페이스를 사용하면, 여러 클래스가 동일한 메서드를 구현하더라도 그 메서드의 실행 방식은 다르게 할 수 있습니다. 즉, 인터페이스를 통해 여러 다른 타입의 객체들이 동일한 메서드를 호출할 수 있도록 하여 다형성을 구현합니다.

예를 들어:

public interface IShape
{
    void Draw();
}

public class Circle : IShape
{
    public void Draw()
    {
        Console.WriteLine("Drawing a circle");
    }
}

public class Square : IShape
{
    public void Draw()
    {
        Console.WriteLine("Drawing a square");
    }
}

위 코드에서 IShape 인터페이스를 구현한 Circle과 Square 클래스는 동일한 Draw() 메서드를 가지고 있지만 각기 다른 방식으로 동작합니다.

2. 유연한 코드 설계 (Flexible Design)

  • 인터페이스는 구현을 강제화하지 않고 계약만을 정의합니다. 이로 인해, 클래스는 자신의 방식대로 구현을 제공하면서도 동일한 인터페이스를 통해 접근할 수 있습니다. 이를 통해 코드 간의 의존성을 줄이고 더 유연한 구조를 만들 수 있습니다.

예를 들어, 다양한 데이터 저장 방식에 대해 동일한 인터페이스를 적용할 수 있습니다.

public interface IDataStore
{
    void SaveData(string data);
}

public class FileDataStore : IDataStore
{
    public void SaveData(string data)
    {
        // 파일에 데이터 저장
    }
}

public class DatabaseDataStore : IDataStore
{
    public void SaveData(string data)
    {
        // 데이터베이스에 데이터 저장
    }
}

이렇게 하면, 저장 방식을 바꾸거나 확장할 때 기존 코드에 큰 영향을 주지 않으면서 유연하게 기능을 확장할 수 있습니다.

3. 구현 강제화 (Enforcing Implementation)

  • 인터페이스는 해당 인터페이스를 구현하는 클래스가 반드시 특정 메서드를 구현하도록 강제합니다. 이는 클래스가 특정 계약을 준수하도록 보장하고, 코드의 일관성을 유지하는 데 도움이 됩니다.

예를 들어:

public interface IWorker
{
    void Work();
}

public class Manager : IWorker
{
    public void Work()
    {
        Console.WriteLine("Managing the team");
    }
}

public class Engineer : IWorker
{
    public void Work()
    {
        Console.WriteLine("Writing code");
    }
}

IWorker 인터페이스를 구현한 클래스들은 반드시 Work 메서드를 구현해야 하므로, 모든 작업자가 Work라는 메서드를 가지고 있다는 점에서 일관성을 보장합니다.

4. 다중 상속을 대체 (Multiple Inheritance)

  • C#은 다중 상속을 지원하지 않지만, 인터페이스를 사용하면 여러 인터페이스를 구현할 수 있습니다. 이를 통해 여러 기능을 한 클래스에 적용할 수 있게 됩니다.

예를 들어, 아래와 같이 한 클래스가 두 개의 인터페이스를 구현할 수 있습니다:

public interface IDriveable
{
    void Drive();
}

public interface IFlyable
{
    void Fly();
}

public class FlyingCar : IDriveable, IFlyable
{
    public void Drive()
    {
        Console.WriteLine("Driving a car");
    }

    public void Fly()
    {
        Console.WriteLine("Flying a car");
    }
}

5. 의존성 주입 (Dependency Injection)

  • 인터페이스는 의존성 주입 (Dependency Injection)을 활용할 때 유용합니다. 의존성 주입을 통해 객체의 생성과 관련된 의존성을 외부에서 주입할 수 있습니다. 이를 통해 테스트가 용이해지고, 코드 간의 결합도를 줄일 수 있습니다.

예를 들어, 다음과 같이 의존성 주입을 사용할 수 있습니다:

public class MyService
{
    private readonly IDataStore _dataStore;

    // 생성자에서 의존성 주입
    public MyService(IDataStore dataStore)
    {
        _dataStore = dataStore;
    }

    public void SaveData(string data)
    {
        _dataStore.SaveData(data);
    }
}

6. 코드의 유지보수성 향상 (Maintainability)

  • 인터페이스를 사용하면 코드의 변경이나 확장 시, 기존 코드를 많이 수정하지 않고도 새로운 기능을 추가할 수 있습니다. 또한, 코드의 의도가 명확하게 드러나기 때문에 나중에 다른 개발자들이 코드를 읽을 때 이해하기 쉽습니다.

 

 

근데 위 예제들을 보다보면 의문이 생긴다.

1번의 예문에서 아래처럼 인터페이스를 빼더라도 굉장히 자연스럽지 않은가? 아니 오히려 더 간단해보이기까지 한다.

public class Circle 
{
    public void Draw()
    {
        Console.WriteLine("Drawing a circle");
    }
}

public class Square 
{
    public void Draw()
    {
        Console.WriteLine("Drawing a square");
    }
}

 

하지만 인터페이스를 쓰는 이유가 단순히 클래스를 만들기 위함이 아니다. 아래 예시를 보자

public interface IDrawable
{
    void Draw();
}

public class Circle : IDrawable
{
    public void Draw()
    {
        Console.WriteLine("Drawing a circle");
    }
}

public class Square : IDrawable
{
    public void Draw()
    {
        Console.WriteLine("Drawing a square");
    }
}

public class Program
{
    public static void Main()
    {
        IDrawable[] shapes = new IDrawable[]
        {
            new Circle(),
            new Square()
        };

        foreach (var shape in shapes)
        {
            shape.Draw(); // 다형성을 활용해 동일한 메서드를 호출
        }
    }
}

 

이 예시처럼 도형들을 IDrawable로 묶어서 관리할때 인터페이스의 장점이 보이는데, 쓰지 않았을때와 비교해보자

 

인터페이스가 없는경우

public class Program
{
    public static void Main()
    {
        Circle circle = new Circle();
        Square square = new Square();
        Triangle triangle = new Triangle();
        
        circle.Draw();   // "Drawing a circle"
        square.Draw();   // "Drawing a square"
        triangle.Draw(); // "Drawing a triangle"
    }
}

 

하나하나 선언하고 호출해야 하지만

있는 경우

public class Program
{
    public static void Main()
    {
        IDrawable[] shapes = new IDrawable[]
        {
            new Circle(),
            new Square(),
            new Triangle()
        };

        // 각 도형을 반복문을 사용하여 처리
        foreach (var shape in shapes)
        {
            shape.Draw();  // 다형성을 활용해 같은 메서드 호출
        }
    }
}

그냥 IDrawable[]에 추가만 해주면 된다.

 

인터페이스를 사용하면 코드의 일관성을 유지하면서도 확장성유연성을 얻을 수 있다. 새로운 도형이 추가될 때마다 기존 코드를 수정하는 대신, 인터페이스만 구현하여 기존 코드를 거의 수정하지 않고도 새로운 기능을 쉽게 추가할 수 있다.

 

그리고 의문점이 하나 더 있는데 인터페이스와 추상클래스의 차이가 무엇인가? 또 어떤걸 써야하는가?

 

1. 인터페이스 (Interface)

인터페이스는 명세(계약)만을 정의하는 것입니다. 인터페이스에서 정의된 메서드는 모든 클래스에서 구현해야 합니다. 또한 인터페이스는 다중 상속을 지원합니다.

인터페이스를 사용해야 할 경우:

  • 다중 상속이 필요한 경우: C#에서 클래스는 단일 상속만 지원하지만, 인터페이스는 여러 개를 구현할 수 있습니다. 예를 들어, 하나의 클래스가 여러 가지 기능을 해야 할 경우, 여러 인터페이스를 구현할 수 있습니다.
  • 구체적인 구현을 제공하지 않고 계약만 정의하고 싶은 경우: 인터페이스는 구현을 가지지 않으며, 클래스가 이 계약을 구현해야 하므로 구현 방식에 구애받지 않고 자유로운 설계를 할 수 있습니다.
  • 구현을 강제하고자 할 때: 특정 메서드를 구현하도록 강제할 때 인터페이스를 사용합니다. 예를 들어, IDrawable 인터페이스는 모든 도형 클래스가 반드시 Draw() 메서드를 구현하도록 합니다.

예시:

public interface IDrawable
{
    void Draw();  // 구현 없이 계약만 정의
}

public class Circle : IDrawable
{
    public void Draw() 
    {
        Console.WriteLine("Drawing a circle");
    }
}

public class Square : IDrawable
{
    public void Draw() 
    {
        Console.WriteLine("Drawing a square");
    }
}

2. 추상 클래스 (Abstract Class)

추상 클래스는 부분적인 구현을 제공하는 클래스입니다. 추상 메서드는 자식 클래스에서 구현해야 하지만, 일반 메서드는 자식 클래스에서 구현 없이 사용할 수 있습니다. 추상 클래스는 단일 상속만 가능합니다.

추상 클래스를 사용해야 할 경우:

  • 공통된 기본 구현을 제공하고자 할 때: 공통된 기능을 자식 클래스에서 공유하고 싶을 때, 추상 클래스에서 그 구현을 제공할 수 있습니다. 자식 클래스는 그 구현을 상속하거나 추가적인 기능을 덧붙여서 사용할 수 있습니다.
  • 상속을 통해 공유할 공통 기능을 제공하고, 일부 메서드에 대해서만 자식 클래스에서 구현을 강제하고 싶을 때.
  • 인스턴스를 만들지 않고 공통 기능을 상속하고자 할 때: 추상 클래스는 인스턴스를 만들 수 없지만, 공통적인 상태나 메서드를 제공하기에 유용합니다.

예시:

public abstract class Shape
{
    public abstract void Draw();  // 자식 클래스에서 구현을 강제

    public void Move()  // 공통된 구현을 제공
    {
        Console.WriteLine("Moving the shape");
    }
}

public class Circle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a circle");
    }
}

public class Square : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a square");
    }
}

3. 인터페이스와 추상 클래스의 차이점 요약

 

특징 인터페이스 (Interface)  추상 클래스 (Abstract Class)
상속 방식 다중 상속 가능 단일 상속만 가능
구현 메서드 시그니처만 제공, 구현 없음 일부 메서드는 구현 가능, 일부는 추상 메서드로 구현 강제
상태 (필드) 필드를 가질 수 없음 (상수만 가능) 필드도 가질 수 있음
목적 여러 클래스에서 동일한 계약을 강제하고자 할 때 공통된 기본 구현을 제공하고, 일부는 자식 클래스에서 구현하도록 할 때
다형성 여러 개의 인터페이스를 구현 가능 하나의 클래스만 상속 가능

4. 언제 인터페이스를 사용하고 언제 추상 클래스를 사용해야 할까?

인터페이스를 사용해야 하는 경우:

  • 여러 클래스가 동일한 기능을 수행해야 할 때, 구현의 내용은 다르더라도 기능계약으로 정의하고 싶을 때
  • 다중 상속이 필요할 때
  • 인터페이스에서 구현을 제공하지 않기 때문에, 클래스들이 구현 방식을 자유롭게 선택해야 할 때

추상 클래스를 사용해야 하는 경우:

  • 여러 클래스가 공통적인 기본 기능을 가져야 할 때, 그 기본 구현을 추상 클래스에서 제공하고 자식 클래스에서 필요에 따라 오버라이딩할 수 있게 할 때
  • 상태(필드)를 공유하고자 할 때
  • 기본 구현추상 메서드를 함께 제공해야 할 때

결론

  • 인터페이스기능의 계약을 정의하고, 클래스들이 특정 기능을 구현하도록 강제하는 데 유용합니다.
  • 추상 클래스공통된 기능과 상태를 공유하고, 자식 클래스에서 일부 기능만 구현하도록 강제하는 데 유용합니다.