4. 인터페이스(Interface)
인터페이스의 궁극적인 목적은 Type을 연결하는 데 있습니다. Type을 Interface기반으로 구현하게 되면 반드시 특정 기능을 지원하기 위한 장치(메서드나 필드 등)가 존재함을 약속할 수 있는데 이는 종종 '계약'에 비유되기도 합니다.
인터페이스는 직접 작성할 수 있지만 .NET에는 특정 기능을 위한 몇 가지 인터페이스가 이미 마련되어 있습니다.
인터페이스 | 구현되는 메서드 | 기능 |
IComparable | CompareTo(type) | Type의 인스턴스를 대상으로 순서나 정렬을 구현하기 위한 비교메서드입니다. |
IComparer | Compare(type, type) | 두번째 Type이 첫번째 Type의 인스턴스를 정렬하거나 정렬하기 위한 메서드를 구현합니다. |
IDisposable | Dispose() | 비관리 리소스에 대해 종료자를 기다리는 것보다 더 효휼적으로 메모리에서 해제하기 위한 소거 메서드를 정의합니다. |
IFormattable | ToString(format, culture) | 객체의 값을 문자열로 표현할때 특정 문화권에 해당하는 형식을 적용하기 위한 메서드를 정의합니다. |
IFormatter | Serialize(stream, object) Deserialize(stream) |
저장및 전송을 위해 byte 스트림으로 부터 객체를 변환하는 메서드를 정의합니다. |
IFormatProvider | GetFormat(type) | 특정 언어및 문화권에 기반한 입력을 형식화 하기위한 메서드를 정의합니다. |
위 인터페이스를 예로 특정 객체를 정렬하기 위한 비교를 수행해야 한다면 선택할 수 있는 가장 일반적인 인터페이스로 IComparable를 들 수 있습니다. 해당 인터페이스는 CompareTo()라는 메서드를 구현해야 함을 명시하고 있는데
namespace System
{
public interface IComparable
{
int CompareTo(object? obj);
}
public interface IComparable<in T>
{
int CompareTo(T? other);
}
}
객체 자체를 비교하는 메서드와 제네릭을 통해 Type을 지정하는 메서드 2개가 존재하고 있습니다. 그리고 둘 다 반환형 식이 int형입니다.
따라서 만약 문자열을 비교대상으로 한다면 문자열이 비교대상보다 더 적으면 -1을 크다면 1을 반환하도록 할 수 있고 정수형인 경우에도 숫자가 더 적으면 -1을, 더 크면 1을 반환하도록 할 수 있습니다. 이와 같은 동작을 구현하는 메서드를 마련한다면 Array와 Collection에서 정렬을 수행할 수 있게 되는 것입니다.
IComparable의 CompareTo()메서드를 구현해 보기 위해 우선 아래와 같은 Class를 생성합니다.
namespace mylibrary;
public class Student
{
public string? Name { get; set; }
}
학생(Student)은 이름(Name)을 가지고 있어서 이 이름을 기준으로 학생이 속한 반에서 번호를 부여받는다고 가정해 보겠습니다.
해당 Class에서 IComparable의 CompareTo()메서드를 구현하려면 다음과 같이 Class가 IComparable인터페이스를 상속받도록 해야 하며 IComparable의 CompareTo() 이름으로 비교 로직을 구현합니다.
namespace mylibrary;
public class Student : IComparable<Student>
{
public string? Name { get; set; }
public int CompareTo(Student? std)
{
if (Name is null)
return 0;
return Name.CompareTo(std?.Name);
}
}
예제에서 IComparable은 제네릭을 구현해 반드시 Student Type만을 비교대상으로 할 수 있도록 하고 있으며 CompareTo메서드에서는 이름이 null인 경우를 제외하고 직접 Name값을 비교하는 메서드를 호출하도록 하고 있습니다.
이제 만들어진 Student를 참조해 직접 정렬을 시도하기 위해 Student의 인스턴스 객체를 여러개 만들고 정렬 전의 값과 정렬 후의 값을 비교해 봅니다.
using mylibrary;
namespace myapp;
class Program
{
static void Main(string[] args)
{
Student[] students = {
new() { Name = "나길동" },
new() { Name = "가길동" },
new() { Name = "라길동" },
new() { Name = "다길동" }
};
Console.WriteLine("정렬 전-------------------");
foreach(var studnet in students)
Console.WriteLine(studnet.Name);
Array.Sort(students);
Console.WriteLine("정렬 후-------------------");
foreach(var studnet in students)
Console.WriteLine(studnet.Name);
}
}
// 정렬 전-------------------
// 나길동
// 가길동
// 라길동
// 다길동
// 정렬 후-------------------
// 가길동
// 나길동
// 다길동
// 라길동
Array의 Sort()메서드 입장에서는 Student객체가 이미 IComparable인터페이스를 구현하고 있으므로 Student가 정렬을 수행하기 위한 CompareTo() 메서드를 가지고 있음을 자연스럽게 알 수 있게 됩니다.
만약 Student가 IComparable의 상속이 없으면 내부에 CompareTo()가 구현되어 있는지와는 상관없이 runtime오류를 발생시키게 됩니다. Array의 Sort()메서드는 IComparable를 상속받는 Type만을 필요로 하며 IComparable인터페이스가 해당 Type이 정렬이 가능한지를 판단하는 중요한 '계약'이기 때문입니다.
정렬을 수행하기 위한 인터페이스로 IComparer도 존재하는데 이 인터페이스는 비교를 위한 별도의 Type을 정의하고 정렬을 수행할 Type과 함께 2개의 Type을 통해 정렬을 수행하는 Compare() 메서드를 구현해야 하는 인터페이스입니다. 다시 말해 정렬 알고리즘을 직접 구현하여 비교방식으로 적용하는 것입니다.
namespace mylibrary;
public class StudentCompare : IComparer<Student>
{
public string? Name { get; set; }
public int Compare(Student? std1, Student? std2)
{
if (std1 is null || std2 is null)
return 0;
int result = std1.Name?.Length.CompareTo(std1.Name.Length) ?? 0;
if (result == 0)
return std1.Name?.CompareTo(std2.Name) ?? 0;
else
return result;
}
}
위 예제에서 StudentCompare는 IComparer인터페이스를 상속받아 Compare()메서드를 구현하여 Student Type을 어떤 식으로 비교할지를 정의하고 있습니다. Compare() 메서드를 보면 2개의 Student인스턴스 객체에서 Name값의 길이를 우선 비교하되 길이가 같으면 Name자체의 값을 비교하도록 하고 있습니다.
Array.Sort(students, new StudentCompare());
실제 정렬을 수행하기 위한 Sort에서는 두번째 매개변수로 위에서 정의한 StudentCompare의 인스턴스를 전달해 주면 해당 Type에서 지정한 방식으로 비교를 수행해 각각의 Item을 정렬할 것입니다.
● 인터페이스의 명시적 구현과 암시적 구현
인터페이스의 암시적구현은 인터페이스를 구현함에 있어서 가장 일반적인 방법입니다. 다만 구현 자체는 다르지만 같은 이름과 매개변수를 갖는 경우에는 인터페이스를 명시적으로 구현해야 합니다.
예를 들어 Drive()라는 메서드를 정의하고 있는 ISedan과 ITruck이라는 인터페이스가 있고
public interface ISedan
{
void Drive();
}
public interface ITruck
{
void Drive();
}
이 2개의 인터페이스를 상속받은 Type에서는 아래와 같이 하나의 메서드를 암시적으로 구현함으로써 각각의 인터페이스에 대한 '계약'을 만족시킬 수 있지만
public class Car : ISedan, ITruck
{
public void Drive()
{
}
}
각 인터페이스에서 정의하고 있는 메서드를 하나씩 모두 구현해야 한다면 인터페이스 이름을 지정해 명시적으로 구현하되 메서드의 특징은 달리해야 합니다.
public class Car : ISedan, ITruck
{
public void Drive()
{
}
void ITruck.Drive()
{
}
}
해당 Type을 구현하는 경우에도 Drive()메서드를 호출하는 경우 명시적으로 구현되지 않은 쪽의 인터페이스에 대한 메서드를 호출하게 되지만 다른 인터페이스를 통해 메서드를 호출해야 한다면 해당 인터페이스를 명시하여 호출하도록 합니다.
static void Main(string[] args)
{
Car c = new();
c.Drive(); //ISedan의 Drive()를 호출
((ITruck)c).Drive();
//혹은
ITruck truck = c as ITruck;
truck.Drive();
}
● 기본 구현 인터페이스
기본 구현 인터페이스는 C# 8.0에서 소개된 것으로 일단 만들어진 인터페이스를 통해 어떤 Type이 구현된 상태라도 해당 인터페이스에 새로운 Member를 추가하는 것이 가능하도록 한 것입니다.
예를 들어 아래와 같은 인터페이스가 있고
namespace mylibrary;
public interface Car
{
void Drive();
void Stop();
}
이를 상속해 새로운 Type을 구현했다면
public class Sedan : ICar
{
public void Drive()
{
Console.WriteLine("주행 중...");
}
public void Stop()
{
Console.WriteLine("정지");
}
}
기존 인터페이스에 새로운 Member의 추가가 발생하게 되면 이를 상속한 모든 Type에서 오류를 발생시킬 수 있기에 사실상 Member의 추가가 쉽지 않은 상태였습니다.
하지만 인터페이스에 기본구현을 사용하게 되면 기존 Type에 영향을 주지 않더라도 새로운 Member를 추가시킬 수 있게 됩니다.
public interface ICar
{
void Drive();
void Stop();
void Parking()
{
Console.WriteLine("주차");
}
}
기본 구현 인터페이스는 말그대로 구현된 메서드를 가지는 것으로 인터페이스에 대한 근본적인 혼란 때문에(Member의 특징만 가질 뿐 멤버의 실체는 상속받는 Type에서 처리한다는 개념) 해당 기능을 좋아하지 않는 사람도 있지만 메서드의 추가를 위해 새로운 인터페이스를 새롭게 생성해야 하는 것보다는 좀 더 유연하게 대처 가능한 방법이라고 보이며 Java나 Swift 같은 언어에서는 이미 비슷한 기능을 구현하고 있습니다.
또한 기본구현 인터페이스는 플랫폼 상의 근본적인 변화를 필요로 하는 탓에 .NET 5이상, .NET Core 3.0이상, .NET Standard 2.1 이상에서만 지원하며 .NET Framework에서는 지원하지 않습니다.
인터페이스를 위와 같이 수정한 후 실제 Type에는 Parking()메서드를 구현하지 않아도 Compile에는 전혀 영향을 주지 않습니다. 물론 향후에라도 기존과 같이 Parking() 메서드를 Type에서 구현하는 것 또한 가능합니다.
'.NET > C#' 카테고리의 다른 글
[C#] 인터페이스(Interface)와 상속(Inheriting) - 5. NULL (0) | 2022.06.24 |
---|---|
[C#] 인터페이스(Interface)와 상속(Inheriting) - 4. 참조타입과 값타입 (0) | 2022.06.24 |
[C#] 인터페이스(Interface)와 상속(Inheriting) - 2. 제네릭(generic) (0) | 2022.06.24 |
[C#] 인터페이스(Interface)와 상속(Inheriting) - 1. 메서드(Method)와 이벤트(Event) (0) | 2022.06.24 |
[C#] C#과 OOP(Object-Oriented Programming) - 7. 레코드(Record) (0) | 2022.06.24 |