[C#] 상속, 다형성, 인터페이스

김유원·2024년 1월 4일
0
post-thumbnail

1. 상속

상속이란 기존의 클래스(부모 클래스 또는 상위클래스)를 확장하거나 재사용하여 새로운 클래스(자식 클래스 또는 하위클래스)를 생성하는 것

자식 클래스는 부모 클래스의 멤버(필드, 메서드, 프로퍼티 등)를 상속받아 사용할 수 있고, 부모 클래스의 기능을 확장하거나 수정하여 새로운 클래스를 정의할 수도 있다.


상속 예제
public class Animal
{
    public string Name { get; set; }
    public int Age { get; set; }

    public void Eat()
    {
        Console.WriteLine("Animal is eating");
    }

    public void Sleep()
    {
        Console.WriteLine("Animal is sleeping");
    }
}

public class Dog : Animal
{
    public void Bark()
    {
        Console.WriteLine("Dog is Bark");
    }
}

public class Cat : Animal
{
    public void Sleep()
    {
        Console.WriteLine("Cat is sleeping");
    }

    public void Meow()
    {
        Console.WriteLine("Cat is meow.");
    }
}

internal class Program
{
    static void Main(string[] args)
    {
        Dog dog = new Dog();
        dog.Name = "Bobby";
        dog.Age = 3;

        dog.Eat();
        dog.Sleep();
        dog.Bark();

        Cat cat = new Cat();
        cat.Name = "KKami";
        cat.Age = 10;

        cat.Eat();
        cat.Sleep();
        cat.Meow();
    }
}

class 명 옆에 : (상속할 클래스명) 을 붙이면 상속할 수 있다.

dog와 cat에 따로 Name과 Age, Eat() 등을 정의하지 않아도 Animal이라는 부모 클래스에 정의되어 있으므로 사용할 수 있다.

위의 코드를 실행하면 이러한 결과를 얻을 수 있다.

이 중 Sleep() 메서드에 집중해보면, Dog 클래스 에서는 Sleep() 메서드를 재정의 하지 않아 부모 클래스의 Animal is sleeping.이 그대로 출력되었고, Cat 클래스에서는 Sleep() 메서드를 재정의하여 Cat 클래스의 Sleep() 메서드인 Cat is sleeping.이 출력된 것을 볼 수 있다.

💡 여기서 드는 의문이 그렇다면 Animal 을 Cat 클래스로 생성하면 어떨까? 하는 것이다.

그래서 한번 작성해보았다. 그랬더니 이러한 오류가 생긴다.

즉, new Cat()으로 참조 해주었지만 생성된 것은 Animal(실형태)인 것이다. 따라서 Cat 클래스에서만 정의된 Meow()는 사용할 수가 없다.

따라서 kitty.Eat()과 kitty.Sleep()에 대해서는 Animal에 정의된 함수가 실행된다.

💡 반대로 Cat을 Animal 클래스로 생성해보는 것은 어떨까?

이 경우에는 암시적 형변환이 불가능한 사안이라는 오류가 뜬다. 즉, 명시적 변환이 필요하다는 것이다.

그래서 이러한 타입캐스팅을 통한 형변환을 시도했더니 수정 사항으로는 뜨지 않는다.

하지만 이를 실행하면 디버깅 오류가 뜬다.

이러한 방식으로는 형변환이 불가능하다는 것이다.

이를 통해 자식 클래스 객체를 부모 클래스로 생성하는 것은 불가능 하다는 것을 알 수 있었다. 혹시나 다른 형 변환 방법이 있는지 알게되면 추후 서술해보겠다.

2. 다형성

다형성이란 같은 타입이지만 다양한 동작을 수행할 수 있는 능력을 말한다.

객체 지향 프로그래밍의 4가지 특징 중 하나로, 이를 보장해주기 위하여 C#에서는 가상 메서드와 추상 클래스&메서드 등을 제공한다.

6일차 TIL에 가상 메서드와 오버로딩, 오버라이딩 등에 대해서는 가볍게 정리해두었는데, 좀 더 자세히 정리해볼까 한다.

1) 가상 메서드

가상 메서드는 기본적으로 부모 클래스에서 정의되고 자식 클래스에서 재정의할 수 있는 메서드이다.

public class Unit
{
    public virtual void Move()
    {
        Console.WriteLine("Unit 두발로 걷기");
    }

    public void Attack()
    {
        Console.WriteLine("Unit 공격");
    }
}

public class Marine : Unit
{

}

public class Zergling : Unit
{
    public override void Move()
    {
        Console.WriteLine("Zergling 네발로 걷기");
    }

}

static void Main(string[] args)
{
    // #1 참조형태와 실형태가 같을때
    Console.WriteLine("*** #1 ***");
    Marine marine = new Marine();
    marine.Move();
    marine.Attack();

    Zergling zergling = new Zergling();
    zergling.Move();
    zergling.Attack();

    // #2 참조형태와 실형태가 다를때
    Console.WriteLine("\n*** #2 ***");
    List<Unit> list = new List<Unit>();
    list.Add(new Marine());
    list.Add(new Zergling());

    foreach (Unit unit in list)
    {
        unit.Move();
    }

    Console.WriteLine("\n*** #3 ***");
    Unit unit1 = new Zergling();

    unit1.Attack();
    unit1.Move();
}

해당 코드를 실행하면 이러한 결과가 나온다.

#1 참조형태와 실형태가 같을 때를 살펴보면 재정의 된 Zergling 클래스의 Move()가 제대로 실행된 것을 볼 수 있다.

#2, #3 에서 참조 형태가 실형태와 다른 부분을 살펴보면 위의 일반 상속과 달리 참조 형태인 Zergling 에 재정의된 Move()가 실행되는 것을 볼 수 있다. 하지만 Zergling에만 Sleep() 함수를 정의하고 unit1.Sleep()을 실행하려고 하면 Unit 클래스에 정의되어있지 않아서 실행할 수 없다.

이처럼 실 형태와 참조 형태가 다른 경우에는 실형태로 생성되기는 했지만 가상 메서드만 참조 메서드로 실행되는 것을 알 수 있다.

2) 추상 클래스 & 메서드

추상 클래스는 직접적으로 인스턴스를 생성할 수 없는 클래스이다.

주로 상속을 위한 베이스 클래스로 사용되고, 추상 메서드를 포함할 수도 있습니다.

abstract class Shape
{
    public abstract void Draw();
}

class Circle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a circle");
    }
}

class Square : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a square");
    }
}

class Triangle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a triangle");
    }
}

static void Main(string[] args)
{
    List<Shape> list = new List<Shape>();
    list.Add(new Circle());
    list.Add(new Square());
    list.Add(new Triangle());
    //list.Add(new Shape());

    foreach (Shape shape in list)
    {
        shape.Draw();
    }
}

위를 실행하면 아래와 같은 결과가 출력된다.

모두 참조된 형태로 잘 실행되는 것을 볼 수 있다.

만약 추상 클래스에 구현된 메소드를 자식 클래스에서 구현하지 않으면 아래와 같은 오류가 생긴다.

또한 주석 처리된 list.Add(new Shape());와 같이 추상 클래스를 객체로 만들려 하면 아래와 같이 추상 형식 또는 인터페이스는 인스턴스를 만들 수 없다는 오류가 뜬다.

💡 그렇다면 추상 클래스 안에 추상 메서드가 아닌 그냥 메서드를 만들면 어떨까?

이처럼 Love() 메서드는 자식 클래스에서는 무조건적으로 재정의할 필요없다는 것을 알 수 있다. 이를 실행해보면,

static void Main(string[] args)
{
    List<Shape> list = new List<Shape>();
    list.Add(new Circle());
    list.Add(new Square());
    list.Add(new Triangle());

    foreach (Shape shape in list)
    {
        shape.Draw();
        shape.Love();
    }
}


추상 클래스에 구현된 일반 메서드가 잘 실행되는 것을 알 수 있다.

+) 오버로딩과 오버라이딩에 대해서는 TIL 6일차에서 충분히 설명한 것 같아 생략한다.

3. 인터페이스

C#에서는 다이아몬드 문제, 설계 복잡성 증가 등을 이유로 다중 상속을 지원하지 않으므로 인터페이스를 사용한다.

인터페이스란 클래스가 구현해야 하는 멤버들을 정의하는 것이다.

인터페이스는 클래스의 일종이 아니므로 C#에서 다중 상속을 제공한다. 유니티의 IPointer 시리즈도 인터페이스에 해당한다. 인터페이스를 사용하면 코드의 재사용성이 좋아지고 유연한 설계에 큰 도움을 주므로 적절히 활용하는 것이 좋다.

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(string[] args)
{
    Player player = new Player();
    Item item = new Item { Name = "Health Potion" };
    player.UseItem(item);
}

당연히 실행 결과는 아이템 Health Potion을 사용했습니다. 가 출력된다.

여기서 주요하게 살펴볼 것은 바로 Player 클래스의 UseItem(IUsable item) 메서드이다. 이 부분을 살펴보면 매개변수로 IUsable 을 참조하는 것을 알 수 있는데, 이것은 IUsable 인터페이스를 상속한 모든 클래스Player의 UserItem 메소드에 사용될 수 있다는 뜻이다. 만약 Weapon이라는 클래스에도 IUsable을 상속받았다면, player.UserItem(weapon); 도 당연하게 사용 가능하다는 이야기다.


다중 상속 같은 경우는 다음과 같이 활용될 수 있다.
// 인터페이스 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);
}

다양한 인터페이스로부터 다중 상속을 받으면 당연히 각 인터페이스에 포함된 멤버들을 전부 포함하여야 하고, 이를 활용하여 다양한 기능의 확장을 추구할 수 있다.




여기까지만 보면 인터페이스가 추상 클래스보다 더 유연하고 사용성이 좋다는 생각이 든다. 그래서 각각의 특징과 장단점을 정리해보겠다.

인터페이스

장점

  • 인터페이스는 추상적인 동작만 정의하고, 구현을 갖지 않음
  • 다중 상속이 가능하며, 여러 클래스가 동일한 인터페이스를 구현할 수 있음
  • 클래스들 간의 결합도를 낮추고, 확장성 향상
  • 코드의 재사용성과 확장성을 향상

단점

  • 인터페이스를 구현하는 클래스가 모든 동작을 구현해야 한다는 의무를 가지기 때문에 작업량이 증가할 수 있음

추상 클래스

장점

  • 추상 클래스는 일부 동작의 구현을 가지며, 추상 메서드를 포함할 수 있음
  • 단일 상속만 가능하며, 다른 클래스와 함께 상속 계층 구조를 형성할 수 있음
  • 공통된 동작을 추상화하여 코드의 중복을 방지하고, 확장성을 제공
  • 구현된 동작을 이미 가지고 있기 때문에, 하위 클래스에서 재정의하지 않아도 될 경우 유용

단점

  • 클래스이기 때문에 다중 상속이 불가능
  • 상속을 통해 밀접하게 결합된 클래스들을 형성하므로 유연성이 제한될 수 있음

따라서 종합하면 인터페이스는 모든 동작을 구현해야 해서 작업량 증가 우려가 있지만 추상클래스는 그럴 필요 없어 유용하고, 추상 클래스는 다중 상속이 불가능하지만 인터페이스는 가능하다. 또한 인터페이스는 클래스들을 느슨하게 연결할 수 있지만, 추상 클래스는 클래스들을 상속을 통해 밀접하게 결합시키므로 서로 장단점을 잘 확인하고 때에 맞는 것을 사용해야 할 것 같다.
profile
개발 공부 블로그

1개의 댓글

comment-user-thumbnail
2024년 1월 5일

미니프로젝트 결과물의 서면 피드백이 나왔습니다. oh모르겠조 노션에 피드백 내용을 올렸으니 확인 부탁드립니다. :)

답글 달기

관련 채용 정보