인터페이스(Interface)
큰 규모의 프로그램일수록 뼈대를 구성하는 일이 중요합니다. 인터페이스를 사용하면 전체 프로그램의 설계도에 대한 명세서를 작성할 수 있습니다. 이번 강의에서는 프로그램의 표준 규약을 정하고 따를 수 있도록 강제하는 인터페이스(Interface)에 대해 학습하겠습니다. 인터페이스는 클래스에서 구현해야 하는 관련 기능에 대한 정의가 포함된 개념입니다.
> // 인터페이스: 클래스에서 구현해야 하는 관련 기능에 대한 정의가 포함된 개념
인터페이스(Interface)
인터페이스는 클래스 또는 구조체에 포함될 수 있는 관련 있는 메서드들을 묶어서 관리합니다. 인터페이스는 명세서(specification, 규약, 표준) 역할을 합니다. 인터페이스를 상속받아 그 내용을 구현하는 클래스는 인터페이스에 선언된 멤버(속성, 메서드 등)가 반드시 구현되어 있다는 보증을 합니다.
다음 내용은 인터페이스에 대한 내용을 추가로 정리한 것입니다. 간단히 읽고 넘어가세요.
인터페이스는
interface
키워드를 사용하여 만듭니다. 인터페이스에는 실행 가능한 코드와 데이터를 포함하고 있지 않습니다.추상 클래스와 같이 다른 클래스에게 멤버 이름을 미리 정의하고자 할 때 사용합니다. 추상 클래스와 다른 점은 멤버의 본문 내용을 구현하지 않고 멤버 이름만을 정의합니다.
인터페이스에는 메서드, 속성, 인덱서 및 이벤트를 정의할 수 있습니다.
현실 세계에서의 전 세계 표준과 같은 기능입니다.
단일 상속만을 지원하는 클래스와 달리 인터페이스를 통한 다중 상속이 가능합니다.
인터페이스 멤버는 액세스 한정자를 붙이지 않으며 항상
public
이고,virtual
및static
을 붙일 수 없습니다.인터페이스 내의 모든 멤버는 기본적으로
public
입니다.C#에서 인터페이스 이름은
ICar
,IFood
,IComputer
형태로 대문자I
로 시작합니다.인터페이스는 인스턴스화되지 않습니다. 클래스를 통해서 인스턴스화됩니다.
I인터페이스 i = new 클래스();
인터페이스는 계약(contract)의 의미가 강하며 속성, 메서드, 이벤트, 인덱서 등의 구조를 미리 정의합니다.
인터페이스로 특정 멤버가 반드시 구현되어야 함을 보증
처음으로 인터페이스를 만들어보겠습니다.
코드: InterfaceNote.cs
//[?] 인터페이스: 특정 멤버가 반드시 구현되어야 함을 보증
using System;
namespace InterfaceNote
{
//[1] ICar 인터페이스 선언
interface ICar
{
void Go(); //[A] 메서드 시그니처만 제공
}
//[2] ICar 인터페이스를 상속하는 Car 클래스 선언
class Car : ICar
{
public void Go() => Console.WriteLine(
"상속한 인터페이스에 정의된 모든 멤버를 반드시 구현해야한다.");
}
class InterfaceNote
{
static void Main()
{
var car = new Car();
car.Go();
}
}
}
상속한 인터페이스에 정의된 모든 멤버를 반드시 구현해야한다.
[1]
번 코드에서 ICar
인터페이스에는 Go()
메서드에 대한 시그니처만 선언되어 있습니다. ICar
인터페이스를 상속 받는 모든 클래스들은 강제적으로 Go()
메서드를 구현해야 합니다. 그렇지 않으면 무조건 에러가 발생합니다. 인터페이스는 이처럼 특정 기능을 반드시 구현하도록 강요할 수 있는 힘을 가질 수 있어 프로그램 코드에 대한 표준, 규약, 명세서 역할을 할 수 있습니다.
인터페이스를 상속하는 클래스에 메서드의 실제 내용 구현
인터페이스 기본 예제를 하나 더 살펴보겠습니다. IPerson
인터페이스는 Work()
메서드에 대한 정의만 가지고 있습니다. IPerson
인터페이스를 상속받는 Person
클래스는 반드시 Work()
클래스에 대한 내용을 구현해야 합니다. 이것이 인터페이스의 사용 규칙입니다.
코드: InterfaceExam.cs
using System;
// 인터페이스
interface IPerson
{
void Work();
}
// 클래스
class Person : IPerson
{
public void Work() => Console.WriteLine("일을 합니다.");
}
// 메인
class InterfaceExam
{
static void Main()
{
Person person = new Person();
person.Work();
}
}
일을 합니다.
인터페이스는 언제 많이 사용할까요? 일반적으로 웹 프로그래밍 또는 데이터베이스 프로그래밍에서는 인터페이스를 먼저 설계하고 이를 구현하는 하나 이상의 클래스를 만들기도 합니다. 이러한 부분은 C# 이후로 Windows Forms, WPF 또는 ASP.NET Core 등을 학습할 때 많이 사용되기에 지금은 인터페이스 사용에 대한 문법만 정리해두면 됩니다.
인터페이스 형식 개체에 인스턴스 담기
인터페이스를 만들고 이를 특정 클래스에 상속하면 해당 클래스는 반드시 부모 인터페이스에서 정의된 메서드의 실제 코드를 만들어야 합니다. 이렇게 만들어진 클래스는 부모 인터페이스 또는 자신의 클래스 이름으로 개체를 만들어 사용될 수 있습니다.
다음 코드를 살펴보세요.
코드: InterfacePractice.cs
using System;
// [1] 하나의 멤버를 갖는 인터페이스 정의
public interface IRepository
{
void Get();
}
// [2] 인터페이스를 상속하는 클래스 구현
public class Repository : IRepository
{
public void Get()
{
Console.WriteLine("Get() 메서드를 구현해야 합니다.");
}
}
class InterfacePractice
{
static void Main()
{
// [A] 인터페이스 형식 개체에 인스턴스 담기
IRepository repository = new Repository();
repository.Get();
}
}
Get() 메서드를 구현해야 합니다.
[1]
코드 영역에서 인터페이스를 하나 만들고 이를 [2]
코드 영역에서 상속하여 실제 구현체를 만듭니다. 이렇게 만들어진 코드는 [A]
와 같이 인터페이스 또는 클래스 이름으로 개체가 만들어지고 인스턴스 코드가 담겨 사용될 수 있습니다. 이러한 코드 모양은 리포지토리 패턴(Repository Pattern) 이름으로 자주 사용됩니다.
생성자의 매개 변수에 인터페이스 사용하기
생성자의 매개 변수에 인터페이스 형식을 사용하면 해당 인터페이스를 상속하는 모든 클래스의 인스턴스를 받을 수 있습니다.
이에 대한 내용을 예제로 살펴보겠습니다.
코드: InterfaceDemo.cs
using System;
namespace InterfaceDemo
{
// 배터리 표준(강제성)
interface IBattery
{
string GetName(); // 메서드 시그니처만 표시
}
class Good : IBattery
{
public string GetName() => "Good";
}
class Bad : IBattery
{
public string GetName() => "Bad";
}
class Car
{
private IBattery _battery;
//[1] 생성자의 매개 변수로 인터페이스 형식 지정
public Car(IBattery battery)
{
_battery = battery; // 넘어온 개체가 _battery 필드에 저장
}
public void Run() => Console.WriteLine(
"{0} 배터리를 장착한 자동차가 달립니다.", _battery.GetName());
}
class InterfaceDemo
{
static void Main()
{
//[A] 넘겨주는 개체에 따라서 배터리 이름이 다르게 표시
var good = new Car(new Good()); good.Run();
new Car(new Bad()).Run(); // 개체 만들기와 동시에 메서드 실행
}
}
}
Good 배터리를 장착한 자동차가 달립니다.
Bad 배터리를 장착한 자동차가 달립니다.
IBattery
인터페이스를 상속하는 Good
클래스와 Bad
클래스의 인스턴스는 [1]
번 코드에서처럼 IBattery
인터페이스 형식의 battery
매개 변수로 받을 수 있습니다. 이런식으로 생성자의 매개 변수로 인터페이스를 사용하면 해당 클래스의 생성자는 하나 이상의 개체를 받을 수 있는 융통성이 늘어납니다.
인터페이스를 사용한 다중 상속 구현하기
다중 상속은 하나의 클래스에 콤마를 구분으로 하나 이상의 인터페이스를 상속하는 것을 의미합니다. C#에서 클래스는 클래스에 대한 단일 상속만을 지원합니다. 대신 인터페이스는 클래스에 하나 이상의 인터페이스를 상속해 줄 수 있습니다.
코드: InterfaceInheritance.cs
using System;
namespace InterfaceInheritance
{
interface IAnimal
{
void Eat();
}
interface IDog
{
void Yelp();
}
class Dog : IAnimal, IDog // 인터페이스를 사용한 다중 상속
{
public void Eat() => Console.WriteLine("먹다.");
public void Yelp() => Console.WriteLine("짖다.");
}
class InterfaceInheritance
{
static void Main()
{
Dog dog = new Dog();
dog.Eat(); //[A] IAnimal 인터페이스 상속
dog.Yelp(); //[B] IDog 인터페이스 상속
}
}
}
먹다.
짖다.
Dog
클래스는 IAnimal
인터페이스와 IDog
인터페이스로부터 다중 상속을 받습니다. IAnimal
인터페이스에서 Eat()
메서드를 상속 받고 IDog
인터페이스로부터 Yelp()
메서드를 상속 받아서 2개의 메서드를 Dog
클래스에서 직접 내용을 구현하는 형태입니다.
명시적인 인터페이스 구현
인터페이스를 통한 다중 상속이 가능하기에 각각의 인터페이스에 동일한 멤버가 구현되어 있는 경우가 있습니다. 이러한 경우에는 명시적으로 어떤 인터페이스의 멤버를 실행할 것인지를 지정해 주어야 합니다.
코드: InterfaceExplicit.cs
using System;
interface IDog
{
void Eat();
}
interface ICat
{
void Eat();
}
class Pet : IDog, ICat
{
void IDog.Eat() => Console.WriteLine("Dog Eat"); // [1] 명시적으로 IDog 지정
void ICat.Eat() => Console.WriteLine("Cat Eat"); // [2] 명시적으로 ICat 지정
}
class InterfaceExplicit
{
static void Main()
{
Pet pet = new Pet();
((IDog)pet).Eat(); // [A] pet 개체를 IDog 형식으로 형식 변환
((ICat)pet).Eat(); // [B] pet 개체를 ICat 형식으로 형식 변환
IDog dog = new Pet();
dog.Eat();
ICat cat = new Pet();
cat.Eat();
}
}
Dog Eat
Cat Eat
Dog Eat
Cat Eat
[1]
번 코드와 같이 IDog.Eat()
메서드를 구현하면 IDog
인터페이스 형식의 개체에서 Eat()
메서드를 호출하면 이 메서드가 실행됩니다.
마찬가지로 [2]
번 코드와 같이 ICat.Eat()
메서드를 구현하면 ICat
개체에서 Eat()
메서드 호출할 때 실행됩니다. IDog
와 ICat
을 모두 상속하는 Pet
클래스의 인스턴스에서는 기본적으로 Eat()
메서드가 노출되지 않고 IDog
또는 ICat
으로 형식을 변환하면 해당 Eat()
메서드가 노출되어 실행이 가능합니다.
참고: Cast<T>()
메서드로 List<자식>
을 List<부모>
로 변환하기
List<T>
형태의 컬렉션 데이터를 부모 클래스 형태로 변경해야할 경우가 있습니다. 방법은 많이 있겠지만 LINQ에서 제공하는 ConvertAll()
메서드와 Cast<T>()
메서드를 사용하면 쉽게 변경이 가능합니다. 다음 코드를 살펴보세요. 참고로, 다음 코드는 내용을 이해하지 않아도 전혀 문제가 되지 않습니다.
코드: ListOfChildToListOfParent.cs
using System;
using System.Collections.Generic;
using System.Linq;
namespace ListOfChildToListOfParent
{
interface A { }
class B : A { }
class ListOfChildToListOfParent
{
static void Main()
{
List<A> convertAll = (new List<B>()).ConvertAll(x => (A)x);
List<A> castOf = (new List<B>()).Cast<A>().ToList();
Console.WriteLine(convertAll);
Console.WriteLine(castOf);
}
}
}
System.Collections.Generic.List`1[ListOfChildToListOfParent.A]
System.Collections.Generic.List`1[ListOfChildToListOfParent.A]
자식 클래스의 컬렉션 인스턴스를 부모 클래스의 컬렉션 인스턴스에 대입하려면 ConvertAll()
메서드 또는 Cast<T>()
메서드를 사용할 수 있습니다. 현업 프로그램을 작성하다보면 특정 인터페이스 또는 부모 클래스의 자식 클래스들의 값을 통합해서 사용하고자할 때 이 두 메서드가 유용하게 사용될 수 있습니다만 지금은 저런 메서드도 있구나 하고 넘어가도 됩니다.
인터페이스와 추상 클래스 비교
인터페이스와 추상클래스를 비교해서 살펴보겠습니다. 다음 항목을 간단히 읽고 넘어갑니다.
추상 클래스
- 구현된 코드가 들어옵니다. 즉, 메서드 시그니처만 있는게 아닌 사용가능한 실제 구현된 메서드도 들어옵니다.
- 단일 상속: 기본 클래스로부터 상속될 수 있습니다.
- 각각의 멤버는 액세스 한정자를 갖습니다.
- 필드, 속성, 생성자, 소멸자, 메서드, 이벤트, 인덱서 등을 갖습니다.
인터페이스
- 인터페이스는 규약입니다.
- 구현된 코드가 없습니다.
- 다중 상속: 여러 가지 인터페이스로부터 상속이 가능합니다.
- 모든 멤버는 자동으로
public
입니다. - 속성, 메서드, 이벤트와 대리자를 멤버로 갖습니다.
IEnumerator
인터페이스 사용해보기
이번에는 닷넷에 내장되어 있는 IEnumerator
인터페이스를 사용해보겠습니다.
코드: IEnumeratorDemo.cs
//[?] IEnumerator 인터페이스 사용해보기
using System;
using System.Collections;
class IEnumeratorDemo
{
static void Main()
{
string[] names = { "닷넷코리아", "VisualAcademy" };
//[1] foreach 문으로 출력
foreach (string name in names)
{
Console.WriteLine(name);
}
//[2] IEnumerator 인터페이스를 통한 데이터 출력: foreach문과 동일
IEnumerator list = names.GetEnumerator(); // 하나씩 열거
while (list.MoveNext()) // 값이 있는 동안 반복
{
Console.WriteLine(list.Current); // 현재 반복중인 데이터 출력
}
}
}
닷넷코리아
VisualAcademy
닷넷코리아
VisualAcademy
IEnumerator
인터페이스는 문자열 배열 등의 GetEnumerator()
메서드의 결괏값을 담아서 MoveNext()
메서드로 값이 있는지 확인하고 Current
속성을 통해서 현재 반복의 데이터를 가져다 사용할 수 있습니다. 물론, IEnumerator
인터페이스 개체를 굳이 이러한 메서드로 사용할 필요는 없습니다. 학습을 위해서 GetEnumerator()
, MoveNext()
, Current
등을 사용해 본 것입니다.
IDisposable
인터페이스 사용해보기
이번에는 닷넷에 내장되어 있는 IDisposable
인터페이스를 사용해보겠습니다. 이를 상속하는 클래스는 Dispose()
메서드를 구현해야하는데요. 이 메서드는 해당 클래스의 개체가 다 사용된 후에 마지막으로 호출되어 정리하는 역할을 합니다.
코드: IDisposableDemo.cs
// IDisposable 인터페이스: 마무리 기능을 자동으로 구현 + using 문과 함께 사용
using System;
class IDisposableDemo
{
static void Main()
{
Console.WriteLine("[1] 열기");
using (var t = new Toilet())
{
// 특정 프로세스 종료시 자동으로 닫기 수행
Console.WriteLine("[2] 사용");
}
}
}
public class Toilet : IDisposable
{
public void Dispose()
{
Console.WriteLine("[3] 닫기");
}
}
[1] 열기
[2] 사용
[3] 닫기
using
문은 IDisposable
인터페이스를 구현하는 개체를 올바르게 사용할 수 있도록 도와주는 구문입니다. using
문으로 개체를 묶어서 생성하면 해당 using
문이 종료되면 자동으로 Dispose()
메서드를 호출해서 정상 종료하도록 처리를 해줍니다.
인터페이스를 사용하여 멤버명 강제로 적용하기
인터페이스를 사용하면 특정 클래스들에게 특정 멤버를 강제로 구현하도록 명시할 수 있습니다. 자동차 클래스들 간의 관계에서 인터페이스는 바로 표준 설계라 보면 됩니다. 표준을 무시한 채 설계하다보면 자동차들이 들쑥날쑥하게 기능이 정의되겠지만, 표준을 미리 정의해 놓은 후 이를 지키도록(구현하도록) 인터페이스를 먼저 설계한 후 인터페이스대로 클래스를 구현하도록 지시하면 공통적이고 표준적인 내용은 함께 구현될 수 있기에 인터페이스를 통한 상속 기능은 프로그램이 복잡해질수록 그 가치가 올라갈 수 있습니다.
코드: InterfaceFriends.cs
using System;
namespace InterfaceFriends
{
// [1] 인터페이스: 설계 // Run() 이라는 단어 설계(명시)
public interface IStandard { void Run(); }
// [2] 추상 클래스: 설계 + 구현
public abstract class KS
{
public abstract void Back();
public void Left() => Console.WriteLine("좌회전");
}
// [3] 분할 클래스
public partial class MyCar : KS
{
public override void Back() => Console.WriteLine("후진");
}
public partial class MyCar : KS
{
public void Right() => Console.WriteLine("우회전");
}
// [4] 봉인(최종) 클래스
public sealed class Car : MyCar, IStandard
{
public void Run() => Console.WriteLine("전진");
}
// [5] 아래 코드는 에러(봉인 클래스는 상속 받을 수 없음)
// public class SelfCar : Car { }
class InterfaceFriends
{
static void Main()
{
Car cla = new Car();
cla.Run(); cla.Left(); cla.Right(); cla.Back();
}
}
}
전진
좌회전
우회전
후진
이번 예제는 인터페이스, 추상 클래스, 분할 클래스 및 봉인 클래스까지 상속에 사용되는 주요 기능들을 적용해 보았습니다.
장 요약
인터페이스는 프로그램의 표준 규약을 만드는데 사용됩니다. 이러한 인터페이스는 응용프로그램 제작시에 거의 필수로 사용되는 개념이기에 이번 강의의 내용을 기반으로 앞으로 계속해서 사용해보도록 하겠습니다.
인터페이스 퀴즈
퀴즈
(1) 다음 중 C#의 인터페이스에 대한 설명으로 잘못된 것을 고르세요.
a. 특정 인터페이스를 상속받는 모든 클래스는 해당 인터페이스의 멤버를 반드시 구현할 필요가 없다.
b. 인터페이스를 사용하여 다중 상속을 구현할 수 있다.
c. 인터페이스는 인스턴스화할 수 없다.
d. 인터페이스에는 메서드, 속성, 인덱서, 이벤트 등을 정의할 수 있다.
정답: a
인터페이스를 구현하는 모든 클래스는 해당 인터페이스의 멤버를 반드시 구현해야 합니다.