1. 인터페이스 (Interface)
인터페이스는 클래스를 만들 때 특정 메서드를 반드시 구현하도록 강제하는 역할을 합니다. 바꿔 말하면 어떤 인터페이스를 상속받아 만들어진 클래스는 인터페이스에 명시된 메서드가 반드시 구현되어 있다는 것을 보증할 수 있습니다.
인터페이스는 interface키워드를 사용해 아래와 같은 방법으로 구현합니다.
interface ICar
{
void Drive();
}
ICar는 인터페이스의 식별자(이름)인데 반드시 그렇게 해야 하는것은 아니지만 관례상 인터페이스의 이름은 대문자 I로 시작합니다.
인터페이스 내부를 보면 Drive()라는 메서드의 이름만을 가지고 있습니다. 이것이 인터페이스의 특징으로 실제 구현되는 내용은 포함하지 않고 특정 메서드나, 이벤트, 인덱서, 프로퍼티의 이름만을 가지고 있게 됩니다. 또한 별도의 한정자를 사용할 수 없고 모든 것이 public으로 선언됩니다.
위와 같이 만들어진 인터페이스는 클래스를 구현할때 상속될 수 있으며 해당 클래스에서는 인터페이스에서 정의한 메서드의 실체를 구현해야 합니다.
참고로 인터페이스는 클래스와 달리 다중상속을 허용합니다. 몇 개의 인터페이스를 하나의 클래스에서 상속받고 있다 할지라도 어쨌건 해당 인터페이스의 구현체만 모두 정의해 주면 됩니다.
class Sedan : ICar
{
int speed = 0;
public void Drive()
{
speed = 100;
WriteLine($"{speed}으로 주행");
}
}
인터페이스 스스로는 객체를 생성할 수 없으나 자신을 상속하는 클래스로부터의 객체 참조는 가능하므로 다음과 같은 구현이 가능한데
class MyTestApp
{
static void Main(string[] args)
{
ICar car = new Sedan();
car.Drive();
}
}
이것은 클래스를 만들때 메서드를 그냥 선언하지 않고 중간에 인터페이스를 끼워 넣어 메서드를 번거롭게 구현해야 하는 중요한 이유이기도 합니다.
만약 내가 어떤 자동차를 원격으로 조정할 수 있는 리모컨 클래스를 만든다고 한다면 차량의 주행기능을 동작시키는 경우 주행 자체는 각 차량의 주행기능에 맡기고 싶습니다. 바로 이런 경우 인터페이스를 활용할 수 있는 것입니다.
class MyTestApp
{
static void Main(string[] args)
{
ICar car = new Sedan();
Remote remote = new Remote(car);
remote.Drive();
}
}
class Remote
{
ICar car;
public Remote(ICar car)
{
this.car = car;
}
public void Drive()
{
car.Drive();
}
}
class Sedan : ICar
{
int speed = 0;
public void Drive()
{
speed = 100;
WriteLine($"{speed}으로 주행");
}
}
인터페이스는 메서드의 이름만 가질뿐 메서드를 구현하는 구현체는 클래스에서 정의한다고 했는데 꼭 그렇지 않은 경우 도 있습니다. 필요에 따라서는 인터페이스도 구현체를 갖는 메서드를 가질 수 있습니다.
이런 경우는 대부분 기존 인터페이스에 새로운 메서드를 추가해야 하는 경우인데 예를 들어 아래와 같이 ICar 인터페이스에 Stop()이라는 새로운 메서드가 생겼다고 가정해 보겠습니다.
interface ICar
{
void Drive();
void Stop();
}
이렇게 되면 이미 해당 인터페이스를 상속받은 모든 클래스는 Stop() 메서드를 구현해야 합니다. 그렇지 않으면 인터페이스가 교체되는 순간 컴파일 오류가 발생할 것입니다.
하지만 기존에 클래스를 수정하는것이 현실적으로 어려운 상황이라면 Stop() 메서드의 실체를 인터페이스에서 구현해 주기만 하면 됩니다.
interface ICar
{
void Drive();
void Stop()
{
WriteLine("정지");
}
}
위와 같이 처리하면 오류를 발생시키지는 않습니다. 물론 기존 클래스에서도 Stop() 메서드는 호출할 수 없을 것입니다.
인터페이스를 위와 같이 수정한 경우라면 새롭게 작성되는 클래스에서는 해당 인터페이스를 상속받았을 때 인터페이스에 구현체가 존재하는 메서드라면 새로운 구현체를 생성하거나 아예 해당 메서드의 구현을 무시할 수 있는 선택권이 생기게 됩니다.
class Truck : ICar
{
int speed = 0;
public void Drive()
{
speed = 80;
WriteLine($"{speed}으로 주행");
}
public void Stop()
{
speed = 0;
WriteLine("트럭 정지");
}
}
인터페이스에서 프로퍼티를 생성하는 경우에는 실체없이 '자동 구현 프로퍼티'로 구현할 수 있습니다.
interface ICar
{
void Drive();
void Stop()
{
WriteLine("정지");
}
int Speed { get; set; }
}
물론 인터페이스를 상속하는 클래스에서는 같은 이름의 프로퍼티를 반드시 구현해야 합니다.
2. 추상 클래스(Abstract class)
일반적인 클래스와 동일하지만 객체를 생성할 수 없는 클래스입니다.
abstract class Car
{
int speed = 0;
public void Drive()
{
speed = 100;
WriteLine($"{speed}으로 주행");
}
public abstract void Stop();
}
추상클래스는 abstract키워드를 사용해 작성되는데 클래스의 구현 방법은 일반 클래스와 동일하며 거의 차이가 없습니다. 다만 스스로 객체를 생성할 수 없고 임의의 추상 클래스를 포함할 수 있다는 특징이 있는데 여기서 추상 클래스는 abstract로 정의된 메서드를 말합니다.
추상 클래스의 추상 메서드는 인터페이스에서의 메서드와 동일한 역할을 합니다. 즉, 추상 클래스를 상속받는 클래스는 추상 클래스 안에서 선언된 추상 메서드를 '반드시' 구현해야 합니다. 물론 예외적인 경우도 있는데 하나의 추상 클래스에서 다른 추상 클래스를 상속받는 경우입니다. 추상 클래스끼리의 상속은 메서드의 구현을 강제하지 않지만 최종적으로 추상 클래스를 상속받은 일반 클래스에서는 연결된 모든 추상 클래스의 추상 메서드를 구현해야 합니다.
반드시 구현되어야 한다는 특징 때문에 추상 메서드는 일반적인 메서드, 프로퍼티, 이벤트등과 달리 puhlic, protected, internal, protected internal 한정자로만 수식할 수 있습니다.
class Sedan : Car
{
public override void Stop()
{
WriteLine("정지");
}
}
추상 클래스의 용도는 명확합니다. 스스로 객체를 생성할 수 없으니 직접 사용할 수 없고 반드시 상속받아 사용되어야 합니다. 또한 추상 클래스를 상속받는 파생 클래스에서는 추상 클래스에 선언된 몇몇 추상 메서드를 반드시 구현해야 합니다. 추상 클래스는 바로 이러한 조건(상속과 구현)에서 사용되는 클래스인 것입니다.
추상클래스도 프로퍼티를 가질 수 있습니다. 하지만 추상클래스 자체에서 직접 구현되는 프로퍼티가 있을 수 있고 상속을 위해 정의되는 프로퍼티도 있을 수 있습니다. 이 두가지를 명확히 구분하기 위해 상속을 위한 프로퍼티의 경우에는 abstract 키워드를 붙여 줘야 합니다.
abstract class Car
{
int speed = 0;
public void Drive()
{
speed = 100;
WriteLine($"{speed}으로 주행");
}
public abstract void Stop();
abstract public string Color { get; set; }
}
따라서 예제의 Car 추상클래스를 상속받는 클래스에서는 Color 프로퍼티를 override하여 구현해야 합니다.
'.NET > C#' 카테고리의 다른 글
[C#] 배열, 컬렉션, 인덱서 (2) | 2021.10.13 |
---|---|
[C#] 프로퍼티(Property) (0) | 2021.10.07 |
[C#] 클래스(Class) (0) | 2021.10.06 |
[C#] 메서드 (0) | 2021.09.27 |
[C#] 제어문 (0) | 2021.09.24 |