개발자공부일기
Interface 본문
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. 언제 인터페이스를 사용하고 언제 추상 클래스를 사용해야 할까?
인터페이스를 사용해야 하는 경우:
- 여러 클래스가 동일한 기능을 수행해야 할 때, 구현의 내용은 다르더라도 기능을 계약으로 정의하고 싶을 때
- 다중 상속이 필요할 때
- 인터페이스에서 구현을 제공하지 않기 때문에, 클래스들이 구현 방식을 자유롭게 선택해야 할 때
추상 클래스를 사용해야 하는 경우:
- 여러 클래스가 공통적인 기본 기능을 가져야 할 때, 그 기본 구현을 추상 클래스에서 제공하고 자식 클래스에서 필요에 따라 오버라이딩할 수 있게 할 때
- 상태(필드)를 공유하고자 할 때
- 기본 구현과 추상 메서드를 함께 제공해야 할 때
결론
- 인터페이스는 기능의 계약을 정의하고, 클래스들이 특정 기능을 구현하도록 강제하는 데 유용합니다.
- 추상 클래스는 공통된 기능과 상태를 공유하고, 자식 클래스에서 일부 기능만 구현하도록 강제하는 데 유용합니다.