C# 기초 정리 6 - 인터페이스와 열거형

woollim·2024년 9월 23일
0

C#

목록 보기
6/11

1. 다중 상속을 사용하지 않는 이유

  1. 다이아몬드 문제(Diamond Problem):
    다중 상속을 허용하면 한 클래스가 두 개 이상의 부모 클래스로부터 동일한 멤버를 상속받을 수 있음. 이 경우, 같은 이름의 멤버를 가지고 있을 때 어떤 부모 클래스의 멤버를 사용해야 하는지 모호해짐. 이런 모호성을 해결하기 위한 규칙이 필요하게 되는데, 이로 인해 코드가 복잡해지고 가독성이 저하될 수 있음

  2. 설계의 복잡성 증가:
    다중 상속을 허용하면 클래스 간의 관계가 복잡해짐. 클래스가 다중 상속을 받을 경우, 어떤 클래스로부터 어떤 멤버를 상속받을지 결정해야 함. 이로 인해 클래스 간의 상속 관계를 파악하기 어려워지고 코드의 유지 보수성이 저하될 수 있음

  3. 이름 충돌과 충돌 해결의 어려움:
    다중 상속을 허용하면 여러 부모 클래스로부터 상속받은 멤버들이 이름이 충돌할 수 있음. 이러한 충돌을 해결하기 위해 충돌하는 멤버를 재정의해야 하거나 명시적으로 부모 클래스를 지정해야 할 수 있음. 이는 코드의 복잡성을 증가시키고 오류 발생 가능성을 높임

  4. 설계의 일관성과 단순성 유지:
    C#은 단일 상속을 통해 설계의 일관성과 단순성을 유지하고자 함. 단일 상속을 통해 클래스 간의 관계를 명확하게 만들고 코드의 가독성과 이해도를 높일 수 있음. 또한 인터페이스를 사용하여 다중 상속이 필요한 경우에도 유사한 기능을 구현할 수 있음



2. 인터페이스

○ 인터페이스를 사용하는 이유

  • 코드의 재사용성:
    인터페이스를 사용하면 다른 클래스에서 해당 인터페이스를 구현하여 동일한 기능을 공유할 수 있음. 인터페이스를 통해 다양한 클래스가 동일한 동작을 수행할 수 있으므로 코드의 재사용성이 향상 됨

  • 다중 상속 제공:
    C#에서는 클래스는 단일 상속만을 지원하지만, 인터페이스는 다중 상속을 지원함. 클래스가 여러 인터페이스를 구현함으로써 여러 개의 기능을 조합할 수 있음. 다중 상속을 통해 클래스는 더 다양한 동작을 수행할 수 있음

  • 유연한 설계:
    인터페이스를 사용하면 클래스와 인터페이스 간에 느슨한 결합을 형성할 수 있음. 클래스는 인터페이스를 구현하기만 하면 되므로, 클래스의 내부 구현에 대한 변경 없이 인터페이스의 동작을 변경하거나 새로운 인터페이스를 추가할 수 있음. 이는 유연하고 확장 가능한 소프트웨어 설계를 가능하게 함



○ 인터페이스 ( Interface )

  • 인터페이스 특징
    • 인터페이스란 클래스가 구현해야 하는 멤버들을 정의하는 것
    • 인터페이스는 클래스의 일종이 아니며, 클래스에 대한 제약 조건을 명시하는 것
    • 클래스가 인터페이스를 구현할 경우, 모든 인터페이스 멤버를 구현해야 함
    • 인터페이스는 다중 상속을 지원
    • 인터페이스에서 선언한 멤버(메서드, 프로퍼티, 이벤트 등)는 기본적으로 public으로 간주 됨. 또한 오직 public으로 선언해야 함
    • 구현 클래스는 상속받은 항목들을 public 아니먄 virtual로 구현해야 함
  • 인터페이스 구현
    • 인터페이스 및 멤버 정의하기
interface IMyInterface
{
    void Method1();
    int Method2(string str);
}
  • 인터페이스 상속 후 클래스에서 구현
class MyClass : IMyInterface
{
    public void Method1()
    {
        // 구현
    }

    public int Method2(string str)
    {
        // 구현
        return 0;
    }
}
  • 인터페이스 구현 예제
// 인터페이스 정의하기
public interface IMovable
{
    void Move(int x, int y); // 이동 메서드 선언
}
// 인터페이스를 구현하는 클래스 생성하기
public class Player : IMovable
{
    public void Move(int x, int y)
    {
        // 플레이어의 이동 구현
    }
}

public class Enemy : IMovable
{
    public void Move(int x, int y)
    {
        // 적의 이동 구현
    }
}
//인터페이스를 사용하여 객체 이동하기
IMovable movableObject1 = new Player();
IMovable movableObject2 = new Enemy();

movableObject1.Move(5, 0); // 플레이어 이동
movableObject2.Move(1, 9); // 적 이동
  • 아이템 사용 구현 예제
// 아이템을 사용할 수 있는 인터페이스
public interface IUsable
{
    void Use();
}

// 아이템 클래스
public class Item : IUsable
{
    public string Name { get; set; }

    public void Use()
    {
        Console.WriteLine("아이템 {0}을 사용했습니다.", Name);
    }
}

// 플레이어 클래스
public class Player
{
    public void UseItem(IUsable item)
    {
        item.Use();
    }
}

// 게임 실행
static void Main()
{
    Player player = new Player();
    Item item = new Item { Name = "Health Potion" };
    player.UseItem(item);
}
  • 다중 상속 구현 예제
// 인터페이스 1
public interface IItemPickable
{
    void PickUp();
}

// 인터페이스 2
public interface IDroppable
{
    void Drop();
}

// 아이템 클래스
public class Item : IItemPickable, IDroppable
{
    public string Name { get; set; }

    public void PickUp()
    {
        Console.WriteLine("아이템 {0}을 주웠습니다.", Name);
    }

    public void Drop()
    {
        Console.WriteLine("아이템 {0}을 버렸습니다.", Name);
    }
}

// 플레이어 클래스
public class Player
{
    public void InteractWithItem(IItemPickable item)
    {
        item.PickUp();
    }

    public void DropItem(IDroppable item)
    {
        item.Drop();
    }
}

// 게임 실행
static void Main()
{
    Player player = new Player();
    Item item = new Item { Name = "Sword" };

    // 아이템 주울 수 있음
    player.InteractWithItem(item);

    // 아이템 버릴 수 있음
    player.DropItem(item);
}

○ 인터페이스 관련 의문점 정리

Q1. 인터페이스를 상속받은 클래스는 객체 선언할때 앞에 인터페이스명이 와야 하는지?

  • 아니다. 하지만 인터페이스를 통해 객체를 다루고 싶을 때는 그 인터페이스 타입으로 객체를 선언 할 수 있음
  • 인터페이스 타입을 사용할지, 클래스 타입을 사용할지는 상황에 따라 선택할 수 있음
public interface IAnimal
{
    void Speak();
}

public class Dog : IAnimal
{
    public void Speak()
    {
        Console.WriteLine("Bark");
    }
}

public class Cat : IAnimal
{
    public void Speak()
    {
        Console.WriteLine("Meow");
    }
}
IAnimal myDog = new Dog();
IAnimal myCat = new Cat();

myDog.Speak();  // 출력: Bark
myCat.Speak();  // 출력: Meow
Dog myDog = new Dog();
myDog.Speak();  // 출력: Bark
  • 인터페이스를 통해 변수를 선언하면, 그 변수가 다양한 클래스의 객체를 가질 수 있다는 장점이 있음
  • 하나의 변수로 다양한 클래스의 객체를 가질 수 있는 유연성이 생김
public interface IAnimal
{
    void Speak();
}

public class Dog : IAnimal
{
    public void Speak()
    {
        Console.WriteLine("Bark");
    }
}

public class Cat : IAnimal
{
    public void Speak()
    {
        Console.WriteLine("Meow");
    }
}

public class Cow : IAnimal
{
    public void Speak()
    {
        Console.WriteLine("Moo");
    }
}

class Program
{
    static void Main(string[] args)
    {
        IAnimal animal;  // IAnimal 인터페이스 타입으로 변수 선언
		// animal 변수는 IAnimal 인터페이스 타입으로 선언되었지만,
        // Dog, Cat, Cow 클래스의 객체를 모두 가질 수 있음
        
        animal = new Dog();
        animal.Speak();  // 출력: Bark

        animal = new Cat();
        animal.Speak();  // 출력: Meow

        animal = new Cow();
        animal.Speak();  // 출력: Moo
    }
}
  • 위 예시처럼 인터페이스 타입 변수를 사용하면 그 변수가 인터페이스를 구현한 다양한 클래스의 인스턴스를 가질 수 있게 됨

Q2. 인터페이스 상속 활용 방향

  • 새 기능이 추가될 때 인터페이스가 추가 상속 됨
  • 확장은 클래스 사용 권장
  • 단순히 기능을 추가한다면 인터페이스 사용 권장


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

○ 인터페이스 vs 추상클래스

  • 인터페이스의 특징과 장단점
    • 인터페이스는 추상적인 동작만 정의하고, 구현을 갖지 않음
    • 다중 상속이 가능하며, 여러 클래스가 동일한 인터페이스를 구현할 수 있음
    • 클래스들 간의 결합도를 낮추고, 유연한 상호작용을 가능하게 함
    • 코드의 재사용성과 확장성을 향상
    • 단점 : 인터페이스를 구현하는 클래스가 모든 동작을 구현해야 한다는 의무를 가지기 때문에 작업량이 증가할 수 있음
  • 추상 클래스의 특징과 장단점
    • 추상 클래스는 일부 동작의 구현을 가지며, 추상 메서드를 포함할 수 있움
    • 단일 상속만 가능하며, 다른 클래스와 함께 상속 계층 구조를 형성할 수 있음
    • 공통된 동작을 추상화하여 코드의 중복을 방지하고, 확장성을 제공
    • 구현된 동작을 가지고 있기 때문에, 하위 클래스에서 재정의하지 않아도 될 경우 유용
    • 단점 : 다중 상속이 불가능하고, 상속을 통해 밀접하게 결합된 클래스들을 형성하므로 유연성이 제한될 수 있음

○ 상세비교

  • 기능 정의 및 구현
    • 추상클래스
      추상 클래스는 추상 메서드(구현이 없는 메서드)와 구현된 메서드(일반 메서드)를 모두 포함할 수 있음. 즉, 일부 기능은 직접 구현하고, 일부는 자식 클래스에서 구현하도록 강제할 수 있음
    • 인터페이스
      인터페이스는 기본적으로 모든 메서드가 구현 없이 정의만 되어 있으며, 구현은 인터페이스를 구현하는 클래스에서 해야 함. C# 8.0부터는 인터페이스에서도 디폴트 메서드 구현을 허용하지만, 일반적으로 인터페이스는 순수한 계약(contract)임
// 추상클래스 예시
public abstract class Animal
{
    public abstract void Speak(); // 자식 클래스에서 반드시 구현해야 함
    
    public void Sleep() // 구현된 메서드
    {
        Console.WriteLine("Sleeping...");
    }
}
// 인터페이스 예시
public interface IAnimal
{
    void Speak(); // 구현 없이 선언만
}
  • 상속 및 구현
    • 추상클래스
      C#에서는 단일 상속만 지원. 즉, 한 클래스는 오직 하나의 추상 클래스만 상속받을 수 있음
    • 인터페이스
      클래스는 여러 개의 인터페이스를 다중 구현할 수 있음. 이를 통해 다중 상속의 제한을 우회할 수 있음
// 인터페이스 예시
public class Dog : IAnimal, IRunnable
{
    public void Speak()
    {
        Console.WriteLine("Bark");
    }
    
    public void Run()
    {
        Console.WriteLine("Running...");
    }
}
  • 필드 및 프로퍼티
    • 추상클래스
      추상 클래스는 필드, 프로퍼티, 생성자 등을 가질 수 있음. 이로 인해 상태(데이터)를 관리할 수 있음
    • 인터페이스
      인터페이스는 필드를 가질 수 없음. 오직 메서드, 프로퍼티, 이벤트 등을 정의할 수 있음. C# 8.0 이후로는 디폴트 구현을 가진 프로퍼티를 허용하지만, 필드를 직접 선언할 수는 없음
// 추상클래스 예시
public abstract class Animal
{
    public string Name { get; set; }  // 프로퍼티
}
// 인터페이스 예시
public interface IAnimal
{
    string Name { get; set; }  // 프로퍼티만 선언 가능
}
  • 접근 제한자
    • 추상클래스
      추상 클래스의 메서드는 public, protected, internal 등 다양한 접근 제한자를 가질 수 있음. 즉, 접근 범위를 세밀하게 조정할 수 있음
    • 인터페이스
      인터페이스 내의 모든 멤버는 기본적으로 public. 인터페이스 내에서는 다른 접근 제한자를 사용할 수 없음
// 추상클래스 예시
public abstract class Animal
{
    protected abstract void Speak();  // protected 접근자 가능
}
  • 사용 목적

    • 추상클래스
      추상 클래스는 기본적인 기능을 일부 구현하고, 그 외의 추가적인 기능은 하위 클래스에서 확장하거나 구현하도록 설계 됨. 일반적으로 클래스 간에 공통된 동작을 공유하면서도, 일부는 자식 클래스가 구체적으로 정의해야 할 때 사용
    • 인터페이스
      인터페이스는 구체적인 구현 없이 동작을 정의하는 역할. 클래스 간에 공통적인 기능을 제공하면서도, 클래스가 반드시 특정 기능을 구현하도록 강제할 때 사용. 다중 상속이 필요하거나, 다양한 클래스가 동일한 계약을 따라야 할 때 유용
  • 성능

    • 추상클래스
      추상 클래스는 약간 더 빠를 수 있음. 이는 클래스가 이미 구현된 메서드를 상속할 수 있어, 메서드 호출 시 인터페이스보다 다소 유리할 수 있기 때문
    • 인터페이스
      인터페이스는 다중 구현이 가능하고, 더 유연하지만, 인터페이스 메서드 호출은 약간의 오버헤드가 발생할 수 있음
  • 요약

    • 추상 클래스는 상속을 통해 기본적인 동작을 공유하면서 구체적인 동작을 자식 클래스에 맡기고자 할 때 유용하며, 인터페이스는 여러 클래스가 동일한 계약을 따라야 할 때, 특히 다중 상속이 필요한 경우에 유용


4. 열거형 (Enums)

○ 사용하는 이유

  1. 가독성:
    열거형을 사용하면 일련의 연관된 상수들을 명명할 수 있음. 이를 통해 코드의 가독성이 향상되고, 상수를 사용할 때 실수로 잘못된 값을 할당하는 것을 방지할 수 있음
  2. 자기 문서화(Self-documenting):
    열거형은 의미 있는 이름을 사용하여 상수를 명명할 수 있음. 이를 통해 코드의 가독성이 향상되며, 상수의 의미를 명확하게 설명할 수 있음
  3. 스위치 문과의 호환성:
    열거형은 스위치 문과 함께 사용될 때 유용. 열거형을 사용하면 스위치 문에서 다양한 상수 값에 대한 분기를 쉽게 작성할 수 있음

○ 열거형 구현

  • 열거형 구현, 사용
// 열거형 구현
enum MyEnum
{
    Value1,
    Value2,
    Value3
}
열거형 사용
MyEnum myEnum = MyEnum.Value1;
  • 열거형 상수 값 지정
enum MyEnum
{
    Value1 = 10,
    Value2,   // 11
    Value3 = 20
}
  • 열거형 형변환
int intValue = (int)MyEnum.Value1;  // 열거형 값을 정수로 변환
MyEnum enumValue = (MyEnum)intValue;  // 정수를 열거형으로 변환
  • 스위치문과의 사용
switch(enumValue)
{
    case MyEnum.Value1:
        // Value1에 대한 처리
        break;
    case MyEnum.Value2:
        // Value2에 대한 처리
        break;
    case MyEnum.Value3:
        // Value3에 대한 처리
        break;
    default:
        // 기본 처리
        break;
}
  • 사용예시
public enum Month
{
    Jan = 1,
    Feb,
    Mar,
    Apr,
    May,
    Jun,
    Jul,
    Aug,
    Sep,
    Oct,
    Nov,
    Dec
}
public static void ProsessMonth(int month)
{
    if (month >= (int)Month.Jan && month <= (int)Month.Dec)
    {
        Month selectMonth = (Month)month;
        Console.WriteLine("선택한 월은 {0}입니다.", selectMonth);
    }
    else
    {
        Console.WriteLine("올바른 월을 입력해주세요.");
    }
}
int userInput = 7;
ProsessMonth(userInput);  // 선택한 월은 Jan입니다


5. C++와 C#의 열거형 차이점

○ 기본 자료형

  • C++ : C++의 열거형은 기본적으로 정수형(int) 값과 연관됨. 명시적으로 자료형을 지정할 수 없고, 열거형 상수는 int로 취급됨
enum Color { Red, Green, Blue };
Color myColor = Red;  // 정수값 0으로 취급
  • C# : C#에서는 열거형의 기본 자료형도 int이지만, byte, short, long 등 다른 정수형으로도 지정할 수 있음
enum Color : byte { Red, Green, Blue };
Color myColor = Color.Red;  // 기본적으로 byte로 처리됨

○ 범위 지정

  • C++ : C++의 열거형은 네임스페이스 없이 전역 범위에 존재 함. 즉, 열거형 값은 네임스페이스 없이 바로 사용할 수 있음
enum Color { Red, Green, Blue };
int myColor = Red;  // Red를 바로 사용 가능
  • C# : C#에서는 열거형 값을 사용할 때, 반드시 열거형 이름을 함께 사용해야 함. Color.Red처럼 열거형 타입을 명시적으로 써야 함
enum Color { Red, Green, Blue };
int myColor = Red;  // Red를 바로 사용 가능

○ 타입 안정성

  • C++ : C++ 열거형은 기본적으로 타입 안전성이 약함. 열거형 값이 정수로 변환되기 때문에, 다른 정수와 혼합하여 사용할 수 있음
enum Color { Red, Green, Blue };
int num = Red;  // 암시적 형 변환 가능
  • C# : C#의 열거형은 타입이 엄격하게 관리 됨. 열거형과 정수를 명시적으로 변환해야 하며, 암시적 형 변환은 불가능
int num = (int)Color.Red;  // 명시적 캐스팅 필요

○ 플래그 사용

  • C++ : C++의 열거형은 기본적으로 플래그 비트 연산에 최적화되어 있지 않음. 직접 비트 연산을 해야 하며, 플래그 열거형 기능은 없음
enum Permission { Read = 1, Write = 2, Execute = 4 };
  • C# : C#은 [Flags] 속성을 사용하여 플래그 열거형을 쉽게 구현할 수 있음. 이를 통해 비트 연산을 더 직관적으로 처리할 수 있음
[Flags]
enum Permission { Read = 1, Write = 2, Execute = 4 };
Permission myPermission = Permission.Read | Permission.Write;

○ 열거형 값의 범위

  • C++ : C++ 열거형은 기본적으로 첫 번째 값에서 시작하여 1씩 증가하는 정수 값을 가짐. 그러나 C++11부터는 열거형의 기저 타입을 지정할 수 있음
enum Color : unsigned int { Red = 1, Green = 2, Blue = 3 };
  • C# : C#도 기본적으로 값이 0부터 시작하여 1씩 증가하지만, 기저 타입을 명시적으로 지정할 수 있음
enum Color : byte { Red = 1, Green = 2, Blue = 3 };

○ 요약

  • C++ : 열거형은 전역 범위에 존재하며, 암시적 정수 변환이 가능하지만 타입 안전성이 낮음
  • C# : 열거형은 타입이 더 엄격하게 관리되며, 타입 이름을 명시적으로 사용해야 하고, 다양한 정수형으로 기저 타입을 지정할 수 있음. Flags 속성을 통해 비트 연산을 쉽게 할 수 있음


6. C++에서 다중 상속이 가능한 이유

○ C++에서 다중 상속이 가능한 이유

  • C++은 객체지향 프로그래밍의 유연성을 강조하며, 그 중 하나로 다중 상속을 허용
  • 다중 상속은 다양한 클래스로부터 기능을 물려받아, 복잡한 상속 구조를 만들 수 있음
class A {
public:
    void foo() {
        std::cout << "A's foo" << std::endl;
    }
};

class B {
public:
    void bar() {
        std::cout << "B's bar" << std::endl;
    }
};

class C : public A, public B {
    // A와 B 둘 다 상속받음
};

int main() {
    C obj;
    obj.foo();  // A의 메서드 호출
    obj.bar();  // B의 메서드 호출
}
  • 장점 : 여러 클래스로부터 필요한 기능을 물려받을 수 있어 코드의 재사용성을 극대화할 수 있음
  • 단점 : 다이아몬드 문제(Diamond Problem) 부모 클래스 간에 동일한 멤버가 있을 경우, 어느 부모로부터 상속받을지를 명시하지 않으면 모호성이 생김. 이를 해결하기 위해 C++은 가상 상속(virtual inheritance) 같은 복잡한 메커니즘을 도입했지만, 코드 관리가 어려워질 수 있음
class A {
public:
    void foo() {
        std::cout << "A's foo" << std::endl;
    }
};

class B : public A {};
class C : public A {};
class D : public B, public C {};

int main() {
    D obj;
    obj.foo();  // 다이아몬드 문제: A의 foo가 B를 통해서인지 C를 통해서인지를 모호함
}

○ C#에서 다중 상속을 지원하지 않는 이유

  • C#의 설계 철학은 단순함과 유지보수 용이성에 중점을 둠. 다중 상속은 코드 복잡성을 증가시키고, 앞서 언급한 다이아몬드 문제 같은 모호성을 초래할 수 있기 때문에 이를 피하기로 결정함
  • 대신 C#은 인터페이스를 통해 다중 상속의 장점을 구현할 수 있게 설계됨. 인터페이스는 클래스가 구현해야 할 메서드와 속성을 정의하며, 하나의 클래스가 여러 개의 인터페이스를 구현할 수 있음. 이를 통해 다중 상속의 장점인 여러 기능을 결합하는 방식이 가능
public interface IAnimal {
    void Speak();
}

public interface IRunnable {
    void Run();
}

public class Dog : IAnimal, IRunnable {
    public void Speak() {
        Console.WriteLine("Bark");
    }

    public void Run() {
        Console.WriteLine("Running");
    }
}
  • C#의 단일 상속 설계 이유:
    • 복잡성 감소 : 다중 상속의 모호성과 복잡성을 방지하여 코드가 더 명확하고 유지보수가 용이함
    • 인터페이스 사용 : 인터페이스를 통해 다중 상속의 기능적 요구를 해결하고, 실제 구현은 클래스 내에서 처리함

○ 요약

  • C#과 C++에서 다중 상속에 대한 차이는 두 언어의 설계 철학과 복잡성 관리 방식에서 비롯 됨
  • C++은 유연성과 강력한 기능성을 위해 다중 상속을 지원하지만, 그로 인해 복잡성(특히 다이아몬드 문제)이 발생할 수 있음
  • C#은 코드의 명확성과 유지보수성을 고려해 다중 상속을 제한하고, 대신 인터페이스를 사용하여 다중 상속과 유사한 기능을 제공

0개의 댓글